Merge pull request #1795 from pietervdvn/feature/json-editor

JSON editor in Studio
This commit is contained in:
Pieter Vander Vennet 2024-02-22 09:55:16 +01:00 committed by GitHub
commit 1048299b03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 208 additions and 39 deletions

45
package-lock.json generated
View file

@ -44,6 +44,7 @@
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"mangrove-reviews-typescript": "^1.1.0", "mangrove-reviews-typescript": "^1.1.0",
"maplibre-gl": "^3.5.0", "maplibre-gl": "^3.5.0",
"monaco-editor": "^0.46.0",
"nano-markdown": "^1.2.2", "nano-markdown": "^1.2.2",
"opening_hours": "^3.6.0", "opening_hours": "^3.6.0",
"osm-auth": "^2.2.0", "osm-auth": "^2.2.0",
@ -68,6 +69,7 @@
"@babeard/svelte-heroicons": "^2.0.0-rc.0", "@babeard/svelte-heroicons": "^2.0.0-rc.0",
"@babel/polyfill": "^7.10.4", "@babel/polyfill": "^7.10.4",
"@babel/preset-env": "7.13.8", "@babel/preset-env": "7.13.8",
"@monaco-editor/loader": "^1.4.0",
"@parcel/service-worker": "^2.6.0", "@parcel/service-worker": "^2.6.0",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
"@sveltejs/vite-plugin-svelte": "^2.0.2", "@sveltejs/vite-plugin-svelte": "^2.0.2",
@ -2355,6 +2357,18 @@
"gl-style-validate": "dist/gl-style-validate.mjs" "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": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "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" "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": { "node_modules/monotone-convex-hull-2d": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz", "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": ">=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": { "node_modules/std-env": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz",
@ -15357,6 +15382,15 @@
"sort-object": "^3.0.3" "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": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "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": { "monotone-convex-hull-2d": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz", "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==", "integrity": "sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ==",
"optional": true "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": { "std-env": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz",

View file

@ -143,6 +143,7 @@
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"mangrove-reviews-typescript": "^1.1.0", "mangrove-reviews-typescript": "^1.1.0",
"maplibre-gl": "^3.5.0", "maplibre-gl": "^3.5.0",
"monaco-editor": "^0.46.0",
"nano-markdown": "^1.2.2", "nano-markdown": "^1.2.2",
"opening_hours": "^3.6.0", "opening_hours": "^3.6.0",
"osm-auth": "^2.2.0", "osm-auth": "^2.2.0",
@ -167,6 +168,7 @@
"@babeard/svelte-heroicons": "^2.0.0-rc.0", "@babeard/svelte-heroicons": "^2.0.0-rc.0",
"@babel/polyfill": "^7.10.4", "@babel/polyfill": "^7.10.4",
"@babel/preset-env": "7.13.8", "@babel/preset-env": "7.13.8",
"@monaco-editor/loader": "^1.4.0",
"@parcel/service-worker": "^2.6.0", "@parcel/service-worker": "^2.6.0",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
"@sveltejs/vite-plugin-svelte": "^2.0.2", "@sveltejs/vite-plugin-svelte": "^2.0.2",

View file

@ -777,6 +777,10 @@ video {
float: left; float: left;
} }
.m-8 {
margin: 2rem;
}
.m-4 { .m-4 {
margin: 1rem; margin: 1rem;
} }
@ -789,10 +793,6 @@ video {
margin: 0px; margin: 0px;
} }
.m-8 {
margin: 2rem;
}
.m-2 { .m-2 {
margin: 0.5rem; margin: 0.5rem;
} }
@ -896,6 +896,10 @@ video {
margin-right: 4rem; margin-right: 4rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 { .mt-4 {
margin-top: 1rem; margin-top: 1rem;
} }
@ -928,10 +932,6 @@ video {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -1127,14 +1127,14 @@ video {
height: 50%; height: 50%;
} }
.h-7 {
height: 1.75rem;
}
.h-3 { .h-3 {
height: 0.75rem; height: 0.75rem;
} }
.h-7 {
height: 1.75rem;
}
.h-11 { .h-11 {
height: 2.75rem; height: 2.75rem;
} }
@ -1163,6 +1163,10 @@ video {
height: 20rem; height: 20rem;
} }
.h-5\/6 {
height: 83.333333%;
}
.h-56 { .h-56 {
height: 14rem; height: 14rem;
} }
@ -1233,14 +1237,14 @@ video {
width: 1rem; width: 1rem;
} }
.w-7 {
width: 1.75rem;
}
.w-3 { .w-3 {
width: 0.75rem; width: 0.75rem;
} }
.w-7 {
width: 1.75rem;
}
.w-11 { .w-11 {
width: 2.75rem; width: 2.75rem;
} }
@ -1266,6 +1270,14 @@ video {
width: 4rem; width: 4rem;
} }
.w-5\/6 {
width: 83.333333%;
}
.w-1\/6 {
width: 16.666667%;
}
.w-min { .w-min {
width: -webkit-min-content; width: -webkit-min-content;
width: min-content; width: min-content;

View file

@ -28,6 +28,9 @@
window.setTimeout(() => tabElements[tab.data].click(), 50) window.setTimeout(() => tabElements[tab.data].click(), 50)
} }
} }
export function getTab() {
return tab
}
</script> </script>
<div class="tabbedgroup flex h-full w-full"> <div class="tabbedgroup flex h-full w-full">

View file

@ -13,14 +13,15 @@
import SchemaBasedInput from "./SchemaBasedInput.svelte" import SchemaBasedInput from "./SchemaBasedInput.svelte"
import FloatOver from "../Base/FloatOver.svelte" import FloatOver from "../Base/FloatOver.svelte"
import TagRenderingInput from "./TagRenderingInput.svelte" import TagRenderingInput from "./TagRenderingInput.svelte"
import FromHtml from "../Base/FromHtml.svelte"
import AllTagsPanel from "../Popup/AllTagsPanel.svelte" import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
import QuestionPreview from "./QuestionPreview.svelte" import QuestionPreview from "./QuestionPreview.svelte"
import ShowConversionMessages from "./ShowConversionMessages.svelte" import ShowConversionMessages from "./ShowConversionMessages.svelte"
import RawEditor from "./RawEditor.svelte"
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
export let state: EditLayerState export let state: EditLayerState
export let backToStudio: () => void export let backToStudio: () => void
let messages = state.messages let messages = state.messages
let hasErrors = messages.mapD( let hasErrors = messages.mapD(
@ -59,7 +60,7 @@
} }
let requiredFields = ["id", "name", "description", "source"] let requiredFields = ["id", "name", "description", "source"]
let currentlyMissing = state.configuration.map((config) => { let currentlyMissing = configuration.map((config) => {
if (!config) { if (!config) {
return [] return []
} }
@ -184,33 +185,34 @@
<Region configs={perRegion["expert"]} {state} /> <Region configs={perRegion["expert"]} {state} />
</div> </div>
<div slot="title5">Configuration file</div> <div slot="title5">Configuration file</div>
<div slot="content5"> <div slot="content5" class="flex h-full flex-col">
<div> <div>
Below, you'll find the raw configuration file in `.json`-format. This is mostly for 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>
<div class="literal-code">
<FromHtml src={JSON.stringify($configuration, null, " ").replaceAll("\n", "</br>")} />
</div>
<ShowConversionMessages messages={$messages} /> <ShowConversionMessages messages={$messages} />
<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> <div>
The testobject (which is used to render the questions in the 'information panel' item The testobject (which is used to render the questions in the 'information panel'
has the following tags: item has the following tags:
</div> </div>
<AllTagsPanel tags={state.testTags} /> <AllTagsPanel tags={state.testTags} />
</div> </div>
</div>
</div>
</TabbedGroup> </TabbedGroup>
</div> </div>
{#if $highlightedItem !== undefined} {#if $highlightedItem !== undefined}
<FloatOver on:close={() => highlightedItem.setData(undefined)}> <FloatOver on:close={() => highlightedItem.setData(undefined)}>
<div> <div>
<TagRenderingInput <TagRenderingInput path={$highlightedItem.path} {state} />
path={$highlightedItem.path} <!--
{state} schema={$highlightedItem.schema} -->
schema={$highlightedItem.schema}
/>
</div> </div>
</FloatOver> </FloatOver>
{/if} {/if}

View file

@ -6,6 +6,7 @@
import TabbedGroup from "../Base/TabbedGroup.svelte" import TabbedGroup from "../Base/TabbedGroup.svelte"
import ShowConversionMessages from "./ShowConversionMessages.svelte" import ShowConversionMessages from "./ShowConversionMessages.svelte"
import Region from "./Region.svelte" import Region from "./Region.svelte"
import RawEditor from "./RawEditor.svelte"
export let state: EditThemeState export let state: EditThemeState
let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0) let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
@ -50,7 +51,7 @@
</div> </div>
<div class="m4 h-full overflow-y-auto"> <div class="m4 h-full overflow-y-auto">
{Object.keys(perRegion).join(";")} <!-- {Object.keys(perRegion).join(";")} -->
<TabbedGroup> <TabbedGroup>
<div slot="title0">Basic properties</div> <div slot="title0">Basic properties</div>
<div slot="content0"> <div slot="content0">
@ -73,12 +74,15 @@
</div> </div>
<div slot="title4">Configuration file</div> <div slot="title4">Configuration file</div>
<div slot="content4"> <div slot="content4" class="flex h-full flex-col">
<div class="literal-code"> <div>
{JSON.stringify($config)} 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> </div>
<ShowConversionMessages messages={$messages} /> <ShowConversionMessages messages={$messages} />
<div class="literal-code h-full w-full">
<RawEditor {state} />
</div>
</div> </div>
</TabbedGroup> </TabbedGroup>
</div> </div>

View 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" />