forked from MapComplete/MapComplete
		
	Merge pull request #1795 from pietervdvn/feature/json-editor
JSON editor in Studio
This commit is contained in:
		
						commit
						1048299b03
					
				
					 7 changed files with 208 additions and 39 deletions
				
			
		
							
								
								
									
										45
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -44,6 +44,7 @@ | |||
|         "lz-string": "^1.4.4", | ||||
|         "mangrove-reviews-typescript": "^1.1.0", | ||||
|         "maplibre-gl": "^3.5.0", | ||||
|         "monaco-editor": "^0.46.0", | ||||
|         "nano-markdown": "^1.2.2", | ||||
|         "opening_hours": "^3.6.0", | ||||
|         "osm-auth": "^2.2.0", | ||||
|  | @ -68,6 +69,7 @@ | |||
|         "@babeard/svelte-heroicons": "^2.0.0-rc.0", | ||||
|         "@babel/polyfill": "^7.10.4", | ||||
|         "@babel/preset-env": "7.13.8", | ||||
|         "@monaco-editor/loader": "^1.4.0", | ||||
|         "@parcel/service-worker": "^2.6.0", | ||||
|         "@rollup/plugin-json": "^6.0.0", | ||||
|         "@sveltejs/vite-plugin-svelte": "^2.0.2", | ||||
|  | @ -2355,6 +2357,18 @@ | |||
|         "gl-style-validate": "dist/gl-style-validate.mjs" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@monaco-editor/loader": { | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", | ||||
|       "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "state-local": "^1.0.6" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "monaco-editor": ">= 0.21.0 < 1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@nodelib/fs.scandir": { | ||||
|       "version": "2.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||||
|  | @ -9377,6 +9391,11 @@ | |||
|         "url": "https://github.com/chalk/supports-color?sponsor=1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/monaco-editor": { | ||||
|       "version": "0.46.0", | ||||
|       "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", | ||||
|       "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==" | ||||
|     }, | ||||
|     "node_modules/monotone-convex-hull-2d": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz", | ||||
|  | @ -11499,6 +11518,12 @@ | |||
|         "node": ">=0.1.14" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/state-local": { | ||||
|       "version": "1.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", | ||||
|       "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "node_modules/std-env": { | ||||
|       "version": "3.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", | ||||
|  | @ -15357,6 +15382,15 @@ | |||
|         "sort-object": "^3.0.3" | ||||
|       } | ||||
|     }, | ||||
|     "@monaco-editor/loader": { | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", | ||||
|       "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "state-local": "^1.0.6" | ||||
|       } | ||||
|     }, | ||||
|     "@nodelib/fs.scandir": { | ||||
|       "version": "2.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||||
|  | @ -20688,6 +20722,11 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "monaco-editor": { | ||||
|       "version": "0.46.0", | ||||
|       "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", | ||||
|       "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==" | ||||
|     }, | ||||
|     "monotone-convex-hull-2d": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz", | ||||
|  | @ -22222,6 +22261,12 @@ | |||
|       "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "state-local": { | ||||
|       "version": "1.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", | ||||
|       "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "std-env": { | ||||
|       "version": "3.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", | ||||
|  |  | |||
|  | @ -143,6 +143,7 @@ | |||
|     "lz-string": "^1.4.4", | ||||
|     "mangrove-reviews-typescript": "^1.1.0", | ||||
|     "maplibre-gl": "^3.5.0", | ||||
|     "monaco-editor": "^0.46.0", | ||||
|     "nano-markdown": "^1.2.2", | ||||
|     "opening_hours": "^3.6.0", | ||||
|     "osm-auth": "^2.2.0", | ||||
|  | @ -167,6 +168,7 @@ | |||
|     "@babeard/svelte-heroicons": "^2.0.0-rc.0", | ||||
|     "@babel/polyfill": "^7.10.4", | ||||
|     "@babel/preset-env": "7.13.8", | ||||
|     "@monaco-editor/loader": "^1.4.0", | ||||
|     "@parcel/service-worker": "^2.6.0", | ||||
|     "@rollup/plugin-json": "^6.0.0", | ||||
|     "@sveltejs/vite-plugin-svelte": "^2.0.2", | ||||
|  |  | |||
|  | @ -777,6 +777,10 @@ video { | |||
|   float: left; | ||||
| } | ||||
| 
 | ||||
| .m-8 { | ||||
|   margin: 2rem; | ||||
| } | ||||
| 
 | ||||
| .m-4 { | ||||
|   margin: 1rem; | ||||
| } | ||||
|  | @ -789,10 +793,6 @@ video { | |||
|   margin: 0px; | ||||
| } | ||||
| 
 | ||||
| .m-8 { | ||||
|   margin: 2rem; | ||||
| } | ||||
| 
 | ||||
| .m-2 { | ||||
|   margin: 0.5rem; | ||||
| } | ||||
|  | @ -896,6 +896,10 @@ video { | |||
|   margin-right: 4rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .mt-4 { | ||||
|   margin-top: 1rem; | ||||
| } | ||||
|  | @ -928,10 +932,6 @@ video { | |||
|   margin-right: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .ml-1 { | ||||
|   margin-left: 0.25rem; | ||||
| } | ||||
|  | @ -1127,14 +1127,14 @@ video { | |||
|   height: 50%; | ||||
| } | ||||
| 
 | ||||
| .h-7 { | ||||
|   height: 1.75rem; | ||||
| } | ||||
| 
 | ||||
| .h-3 { | ||||
|   height: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .h-7 { | ||||
|   height: 1.75rem; | ||||
| } | ||||
| 
 | ||||
| .h-11 { | ||||
|   height: 2.75rem; | ||||
| } | ||||
|  | @ -1163,6 +1163,10 @@ video { | |||
|   height: 20rem; | ||||
| } | ||||
| 
 | ||||
| .h-5\/6 { | ||||
|   height: 83.333333%; | ||||
| } | ||||
| 
 | ||||
| .h-56 { | ||||
|   height: 14rem; | ||||
| } | ||||
|  | @ -1233,14 +1237,14 @@ video { | |||
|   width: 1rem; | ||||
| } | ||||
| 
 | ||||
| .w-7 { | ||||
|   width: 1.75rem; | ||||
| } | ||||
| 
 | ||||
| .w-3 { | ||||
|   width: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .w-7 { | ||||
|   width: 1.75rem; | ||||
| } | ||||
| 
 | ||||
| .w-11 { | ||||
|   width: 2.75rem; | ||||
| } | ||||
|  | @ -1266,6 +1270,14 @@ video { | |||
|   width: 4rem; | ||||
| } | ||||
| 
 | ||||
| .w-5\/6 { | ||||
|   width: 83.333333%; | ||||
| } | ||||
| 
 | ||||
| .w-1\/6 { | ||||
|   width: 16.666667%; | ||||
| } | ||||
| 
 | ||||
| .w-min { | ||||
|   width: -webkit-min-content; | ||||
|   width: min-content; | ||||
|  |  | |||
|  | @ -28,6 +28,9 @@ | |||
|       window.setTimeout(() => tabElements[tab.data].click(), 50) | ||||
|     } | ||||
|   } | ||||
|   export function getTab() { | ||||
|     return tab | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <div class="tabbedgroup flex h-full w-full"> | ||||
|  |  | |||
|  | @ -13,14 +13,15 @@ | |||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte" | ||||
|   import FloatOver from "../Base/FloatOver.svelte" | ||||
|   import TagRenderingInput from "./TagRenderingInput.svelte" | ||||
|   import FromHtml from "../Base/FromHtml.svelte" | ||||
|   import AllTagsPanel from "../Popup/AllTagsPanel.svelte" | ||||
|   import QuestionPreview from "./QuestionPreview.svelte" | ||||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte" | ||||
|   import RawEditor from "./RawEditor.svelte" | ||||
| 
 | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw | ||||
| 
 | ||||
|   export let state: EditLayerState | ||||
| 
 | ||||
|   export let backToStudio: () => void | ||||
|   let messages = state.messages | ||||
|   let hasErrors = messages.mapD( | ||||
|  | @ -59,7 +60,7 @@ | |||
|   } | ||||
| 
 | ||||
|   let requiredFields = ["id", "name", "description", "source"] | ||||
|   let currentlyMissing = state.configuration.map((config) => { | ||||
|   let currentlyMissing = configuration.map((config) => { | ||||
|     if (!config) { | ||||
|       return [] | ||||
|     } | ||||
|  | @ -184,33 +185,34 @@ | |||
|           <Region configs={perRegion["expert"]} {state} /> | ||||
|         </div> | ||||
|         <div slot="title5">Configuration file</div> | ||||
|         <div slot="content5"> | ||||
|         <div slot="content5" class="flex h-full flex-col"> | ||||
|           <div> | ||||
|             Below, you'll find the raw configuration file in `.json`-format. This is mostly for | ||||
|             debugging purposes | ||||
|             debugging purposes, but you can also edit the file directly if you want. | ||||
|           </div> | ||||
|           <div class="literal-code"> | ||||
|             <FromHtml src={JSON.stringify($configuration, null, "  ").replaceAll("\n", "</br>")} /> | ||||
|           </div> | ||||
| 
 | ||||
|           <ShowConversionMessages messages={$messages} /> | ||||
|           <div> | ||||
|             The testobject (which is used to render the questions in the 'information panel' item | ||||
|             has the following tags: | ||||
|           </div> | ||||
|           <div class="flex h-full w-full flex-row justify-between overflow-y-auto"> | ||||
|             <div class="literal-code h-full w-5/6 overflow-y-auto"> | ||||
|               <RawEditor {state} /> | ||||
|             </div> | ||||
|             <div class="h-full w-1/6"> | ||||
|               <div> | ||||
|                 The testobject (which is used to render the questions in the 'information panel' | ||||
|                 item has the following tags: | ||||
|               </div> | ||||
| 
 | ||||
|           <AllTagsPanel tags={state.testTags} /> | ||||
|               <AllTagsPanel tags={state.testTags} /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </TabbedGroup> | ||||
|     </div> | ||||
|     {#if $highlightedItem !== undefined} | ||||
|       <FloatOver on:close={() => highlightedItem.setData(undefined)}> | ||||
|         <div> | ||||
|           <TagRenderingInput | ||||
|             path={$highlightedItem.path} | ||||
|             {state} | ||||
|             schema={$highlightedItem.schema} | ||||
|           /> | ||||
|           <TagRenderingInput path={$highlightedItem.path} {state} /> | ||||
|           <!--  | ||||
|             schema={$highlightedItem.schema} --> | ||||
|         </div> | ||||
|       </FloatOver> | ||||
|     {/if} | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
|   import TabbedGroup from "../Base/TabbedGroup.svelte" | ||||
|   import ShowConversionMessages from "./ShowConversionMessages.svelte" | ||||
|   import Region from "./Region.svelte" | ||||
|   import RawEditor from "./RawEditor.svelte" | ||||
| 
 | ||||
|   export let state: EditThemeState | ||||
|   let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0) | ||||
|  | @ -50,7 +51,7 @@ | |||
|   </div> | ||||
| 
 | ||||
|   <div class="m4 h-full overflow-y-auto"> | ||||
|     {Object.keys(perRegion).join(";")} | ||||
|     <!-- {Object.keys(perRegion).join(";")} --> | ||||
|     <TabbedGroup> | ||||
|       <div slot="title0">Basic properties</div> | ||||
|       <div slot="content0"> | ||||
|  | @ -73,12 +74,15 @@ | |||
|       </div> | ||||
| 
 | ||||
|       <div slot="title4">Configuration file</div> | ||||
|       <div slot="content4"> | ||||
|         <div class="literal-code"> | ||||
|           {JSON.stringify($config)} | ||||
|       <div slot="content4" class="flex h-full flex-col"> | ||||
|         <div> | ||||
|           Below, you'll find the raw configuration file in `.json`-format. This is mostly for | ||||
|           debugging purposes, but you can also edit the file directly if you want. | ||||
|         </div> | ||||
| 
 | ||||
|         <ShowConversionMessages messages={$messages} /> | ||||
|         <div class="literal-code h-full w-full"> | ||||
|           <RawEditor {state} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </TabbedGroup> | ||||
|   </div> | ||||
|  |  | |||
							
								
								
									
										101
									
								
								src/UI/Studio/RawEditor.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/UI/Studio/RawEditor.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,101 @@ | |||
| <script lang="ts"> | ||||
|   import { onDestroy, onMount } from "svelte" | ||||
|   import EditLayerState, { EditThemeState } from "./EditLayerState" | ||||
|   import loader from "@monaco-editor/loader" | ||||
|   import type * as Monaco from "monaco-editor/esm/vs/editor/editor.api" | ||||
|   import layerSchemaJSON from "../../../Docs/Schemas/LayerConfigJson.schema.json" | ||||
|   import layoutSchemaJSON from "../../../Docs/Schemas/LayoutConfigJson.schema.json" | ||||
| 
 | ||||
|   export let state: EditLayerState | EditThemeState | ||||
| 
 | ||||
|   let container: HTMLDivElement | ||||
|   let monaco: typeof Monaco | ||||
|   let editor: Monaco.editor.IStandaloneCodeEditor | ||||
|   let model: Monaco.editor.ITextModel | ||||
| 
 | ||||
|   function save() { | ||||
|     try { | ||||
|       const newConfig = JSON.parse(editor.getValue()) | ||||
|       state.configuration.setData(newConfig) | ||||
|     } catch (e) { | ||||
|       console.error(e) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Catch keyboard shortcuts | ||||
|   onMount(() => { | ||||
|     const handler = (e: KeyboardEvent) => { | ||||
|       if (e.key === "s" && (e.ctrlKey || e.metaKey)) { | ||||
|         e.preventDefault() | ||||
|         save() | ||||
|       } | ||||
|     } | ||||
|     window.addEventListener("keydown", handler) | ||||
|     return () => window.removeEventListener("keydown", handler) | ||||
|   }) | ||||
| 
 | ||||
|   onMount(async () => { | ||||
|     const monacoEditor = await import("monaco-editor") | ||||
|     loader.config({ | ||||
|       monaco: monacoEditor.default, | ||||
|     }) | ||||
| 
 | ||||
|     monaco = await loader.init() | ||||
| 
 | ||||
|     // Determine schema based on the state | ||||
|     let schemaUri: string | ||||
|     if (state instanceof EditLayerState) { | ||||
|       schemaUri = "https://mapcomplete.org/schemas/layerconfig.json" | ||||
|     } else { | ||||
|       schemaUri = "https://mapcomplete.org/schemas/layoutconfig.json" | ||||
|     } | ||||
| 
 | ||||
|     monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ | ||||
|       validate: true, | ||||
|       schemas: [ | ||||
|         { | ||||
|           uri: schemaUri, | ||||
|           fileMatch: ["file.json"], | ||||
|           schema: | ||||
|             schemaUri === "https://mapcomplete.org/schemas/layerconfig.json" | ||||
|               ? layerSchemaJSON | ||||
|               : layoutSchemaJSON, | ||||
|         }, | ||||
|       ], | ||||
|     }) | ||||
| 
 | ||||
|     let modelUri = monaco.Uri.parse("inmemory://inmemory/file.json") | ||||
| 
 | ||||
|     // Create a new model | ||||
|     model = monaco.editor.createModel( | ||||
|       JSON.stringify(state.configuration.data, null, "  "), | ||||
|       "json", | ||||
|       modelUri | ||||
|     ) | ||||
| 
 | ||||
|     editor = monaco.editor.create(container, { | ||||
|       model, | ||||
|       automaticLayout: true, | ||||
|     }) | ||||
| 
 | ||||
|     // When the editor is changed, update the configuration, but only if the user hasn't typed for 500ms and the JSON is valid | ||||
|     let timeout: number | ||||
|     editor.onDidChangeModelContent(() => { | ||||
|       clearTimeout(timeout) | ||||
|       timeout = setTimeout(() => { | ||||
|         save() | ||||
|       }, 500) | ||||
|     }) | ||||
|   }) | ||||
| 
 | ||||
|   onDestroy(() => { | ||||
|     if (editor) { | ||||
|       editor.dispose() | ||||
|     } | ||||
|     if (model) { | ||||
|       model.dispose() | ||||
|     } | ||||
|   }) | ||||
| </script> | ||||
| 
 | ||||
| <div bind:this={container} class="h-full w-full" /> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue