forked from MapComplete/MapComplete
		
	Studio: add slideshow, add useability tweaks
This commit is contained in:
		
							parent
							
								
									2df9aa8564
								
							
						
					
					
						commit
						8bc555fbe0
					
				
					 26 changed files with 440 additions and 316 deletions
				
			
		|  | @ -28,6 +28,18 @@ async function prepareFile(url: string): Promise<string> { | |||
|     if (fs.existsSync(filePath)) { | ||||
|         return fs.readFileSync(filePath, "utf8") | ||||
|     } | ||||
|     while (url.startsWith("/")) { | ||||
|         url = url.slice(1) | ||||
|     } | ||||
|     const sliced = url.split("/").slice(1) | ||||
|     if (!sliced) { | ||||
|         return | ||||
|     } | ||||
|     const backupFile = path.join(STATIC_PATH, ...sliced) | ||||
|     console.log("Using bakcup path", backupFile) | ||||
|     if (fs.existsSync(backupFile)) { | ||||
|         return fs.readFileSync(backupFile, "utf8") | ||||
|     } | ||||
|     return null | ||||
| } | ||||
| 
 | ||||
|  | @ -51,7 +63,9 @@ http.createServer(async (req, res) => { | |||
|             for (let i = 1; i < paths.length; i++) { | ||||
|                 const p = paths.slice(0, i) | ||||
|                 const dir = STATIC_PATH + p.join("/") | ||||
|                 console.log("Checking if", dir, "exists...") | ||||
|                 if (!fs.existsSync(dir)) { | ||||
|                     console.log("Creating new directory", dir) | ||||
|                     fs.mkdirSync(dir) | ||||
|                 } | ||||
|             } | ||||
|  | @ -61,22 +75,28 @@ http.createServer(async (req, res) => { | |||
|             res.end() | ||||
|             return | ||||
|         } | ||||
|         if (req.url.endsWith("/overview")) { | ||||
| 
 | ||||
|         const url = new URL(`http://127.0.0.1/` + req.url) | ||||
|         if (url.pathname.endsWith("overview")) { | ||||
|             console.log("Giving overview") | ||||
|             let userId = url.searchParams.get("userId") | ||||
|             const allFiles = ScriptUtils.readDirRecSync(STATIC_PATH) | ||||
|                 .filter((p) => p.endsWith(".json") && !p.endsWith("license_info.json")) | ||||
|                 .filter( | ||||
|                     (p) => | ||||
|                         p.endsWith(".json") && | ||||
|                         !p.endsWith("license_info.json") && | ||||
|                         (p.startsWith("layers") || | ||||
|                             p.startsWith("themes") || | ||||
|                             userId !== undefined || | ||||
|                             p.startsWith(userId)) | ||||
|                 ) | ||||
|                 .map((p) => p.substring(STATIC_PATH.length + 1)) | ||||
|             res.writeHead(200, { "Content-Type": MIME_TYPES.json }) | ||||
|             res.write(JSON.stringify({ allFiles })) | ||||
|             res.end() | ||||
|             return | ||||
|         } | ||||
|         if (!fs.existsSync(STATIC_PATH + req.url)) { | ||||
|             res.writeHead(404, { "Content-Type": MIME_TYPES.html }) | ||||
|             res.write("<html><body><p>Not found...</p></body></html>") | ||||
|             res.end() | ||||
|             return | ||||
|         } | ||||
| 
 | ||||
|         const file = await prepareFile(req.url) | ||||
|         if (file === null) { | ||||
|             res.writeHead(404, { "Content-Type": MIME_TYPES.html }) | ||||
|  |  | |||
|  | @ -435,7 +435,6 @@ class MappedStore<TIn, T> extends Store<T> { | |||
|      * const mapped = src.map(i => i * 2) | ||||
|      * src.setData(3) | ||||
|      * mapped.data // => 6
 | ||||
|      * | ||||
|      */ | ||||
|     get data(): T { | ||||
|         if (!this._callbacksAreRegistered) { | ||||
|  |  | |||
|  | @ -666,25 +666,29 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> { | |||
|         } | ||||
| 
 | ||||
|         if (json.freeform) { | ||||
|             const c = context.enters("freeform", "render") | ||||
|             if (json.render === undefined) { | ||||
|                 c.err( | ||||
|                     "This tagRendering allows to set a freeform, but does not define a way to `render` this value" | ||||
|                 context | ||||
|                     .enter("render") | ||||
|                     .err( | ||||
|                         "This tagRendering allows to set a value to key " + | ||||
|                             json.freeform.key + | ||||
|                             ", but does not define a `render`. Please, add a value here which contains `{" + | ||||
|                             json.freeform.key + | ||||
|                             "}`" | ||||
|                     ) | ||||
|             } else { | ||||
|                 const render = new Translation(<any>json.render) | ||||
| 
 | ||||
|                 for (const ln in render.translations) { | ||||
|                     if (ln.startsWith("_")) { | ||||
|                         continue | ||||
|                     } | ||||
|                     const txt: string = render.translations[ln] | ||||
|                     if (txt === "") { | ||||
|                         c.err(" Rendering for language " + ln + " is empty") | ||||
|                         context.enter("render").err(" Rendering for language " + ln + " is empty") | ||||
|                     } | ||||
|                     if ( | ||||
|                         txt.indexOf("{" + json.freeform.key + "}") >= 0 || | ||||
|                         txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") | ||||
|                         txt.indexOf("&LBRACE" + json.freeform.key + "&RBRACE") >= 0 | ||||
|                     ) { | ||||
|                         continue | ||||
|                     } | ||||
|  | @ -721,8 +725,10 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> { | |||
|                     ) { | ||||
|                         continue | ||||
|                     } | ||||
|                     c.err( | ||||
|                         `The rendering for language ${ln} does not contain the freeform key {${json.freeform.key}}. This is a bug, as this rendering should show exactly this freeform key!\nThe rendering is ${txt} ` | ||||
|                     context | ||||
|                         .enter("render") | ||||
|                         .err( | ||||
|                             `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!` | ||||
|                         ) | ||||
|                 } | ||||
|             } | ||||
|  | @ -783,7 +789,7 @@ export class ValidateLayer extends Conversion< | |||
|     private readonly _path?: string | ||||
|     private readonly _isBuiltin: boolean | ||||
|     private readonly _doesImageExist: DoesImageExist | ||||
|     private _studioValidations: boolean | ||||
|     private readonly _studioValidations: boolean | ||||
| 
 | ||||
|     constructor( | ||||
|         path: string, | ||||
|  | @ -816,7 +822,7 @@ export class ValidateLayer extends Conversion< | |||
|         } | ||||
| 
 | ||||
|         if (json.id === undefined) { | ||||
|             context.err(`Not a valid layer: id is undefined: ${JSON.stringify(json)}`) | ||||
|             context.enter("id").err(`Not a valid layer: id is undefined`) | ||||
|         } | ||||
| 
 | ||||
|         if (json.source === undefined) { | ||||
|  | @ -922,6 +928,21 @@ export class ValidateLayer extends Conversion< | |||
|                     "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." | ||||
|                 ) | ||||
|             } | ||||
| 
 | ||||
|             { | ||||
|                 // Check for multiple, identical builtin questions - usability for studio users
 | ||||
|                 const duplicates = Utils.Duplicates( | ||||
|                     <string[]>json.tagRenderings.filter((tr) => typeof tr === "string") | ||||
|                 ) | ||||
|                 for (let i = 0; i < json.tagRenderings.length; i++) { | ||||
|                     const tagRendering = json.tagRenderings[i] | ||||
|                     if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) { | ||||
|                         context | ||||
|                             .enters("tagRenderings", i) | ||||
|                             .err(`This builtin question is used multiple times (${tagRendering})`) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (json["builtin"] !== undefined) { | ||||
|  |  | |||
|  | @ -15,11 +15,18 @@ import { Translatable } from "./Translatable" | |||
|  */ | ||||
| export interface LayerConfigJson { | ||||
|     /** | ||||
|      * The id of this layer. | ||||
|      * This should be a simple, lowercase, human readable string that is used to identify the layer. | ||||
|      * | ||||
|      * group: Basic | ||||
|      * question: What is the identifier of this layer? | ||||
|      * | ||||
|      * This should be a simple, lowercase, human readable string that is used to identify the layer. | ||||
|      *  A good ID is: | ||||
|      *  - a noun | ||||
|      *  - written in singular | ||||
|      *  - describes the object | ||||
|      *  - in english | ||||
|      *  - only has lowercase letters, numbers or underscores. Do not use a space or a dash | ||||
|      * | ||||
|      * type: id | ||||
|      * group: Basic | ||||
|      */ | ||||
|     id: string | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,8 +26,7 @@ | |||
|   } | ||||
|   const apiState = state.osmConnection.apiIsOnline | ||||
| </script> | ||||
| <slot /> | ||||
| <!-- | ||||
| 
 | ||||
| {#if $badge} | ||||
|   {#if !ignoreLoading && $loadingStatus === "loading"} | ||||
|     <slot name="loading"> | ||||
|  | @ -43,4 +42,4 @@ | |||
|   {:else if $loadingStatus === "not-attempted"} | ||||
|     <slot name="not-logged-in" /> | ||||
|   {/if}  | ||||
| {/if} --> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ $: documentation = TagUtils.modeDocumentation[mode]; | |||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} /> | ||||
| <BasicTagInput bind:mode={mode} {dropdownFocussed} {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} on:submit /> | ||||
| {#if $dropdownFocussed} | ||||
|   <div class="border border-dashed border-black p-2 m-2"> | ||||
|   <b>{documentation.name}</b> | ||||
|  |  | |||
|  | @ -19,4 +19,4 @@ let tag: UIEventSource<string | TagConfigJson> = value | |||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} /> | ||||
| <FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} on:submit/> | ||||
|  |  | |||
|  | @ -5,20 +5,14 @@ | |||
|   import { createEventDispatcher, onDestroy } from "svelte"; | ||||
|   import ValidatedInput from "../ValidatedInput.svelte"; | ||||
| 
 | ||||
|   export let value: UIEventSource<string> = new UIEventSource<string>(""); | ||||
|   export let value: UIEventSource<Record<string, string>> = new UIEventSource<Record<string, string>>({}); | ||||
|    | ||||
|   export let args: string[] = [] | ||||
|    | ||||
|   let prefix = args[0] | ||||
|   let postfix = args[1] | ||||
|   let prefix = args[0] ?? "" | ||||
|   let postfix = args[1] ?? "" | ||||
| 
 | ||||
|   let translations: UIEventSource<Record<string, string>> = value.sync((s) => { | ||||
|     try { | ||||
|       return JSON.parse(s); | ||||
|     } catch (e) { | ||||
|       return {}; | ||||
|     } | ||||
|   }, [], v => JSON.stringify(v)); | ||||
|   let translations: UIEventSource<Record<string, string>> = value | ||||
| 
 | ||||
|   const allLanguages: string[] = LanguageUtils.usedLanguagesSorted; | ||||
|   let currentLang = new UIEventSource("en"); | ||||
|  | @ -28,6 +22,9 @@ | |||
|   function update() { | ||||
|     const v = currentVal.data; | ||||
|     const l = currentLang.data; | ||||
|     if(translations.data === "" || translations.data === undefined){ | ||||
|       translations.data = {} | ||||
|     } | ||||
|     if (translations.data[l] === v) { | ||||
|       return; | ||||
|     } | ||||
|  | @ -37,6 +34,9 @@ | |||
| 
 | ||||
|   onDestroy(currentLang.addCallbackAndRunD(currentLang => { | ||||
|     console.log("Applying current lang:", currentLang); | ||||
|     if(!translations.data){ | ||||
|       translations.data = {} | ||||
|     } | ||||
|     translations.data[currentLang] = translations.data[currentLang] ?? ""; | ||||
|     currentVal.setData(translations.data[currentLang]); | ||||
|   })); | ||||
|  |  | |||
|  | @ -27,14 +27,13 @@ | |||
| 
 | ||||
|   let properties = { feature, args: args ?? [] }; | ||||
|   let dispatch = createEventDispatcher<{ | ||||
|     selected, | ||||
|     submit | ||||
|     selected | ||||
|   }>(); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if type === "translation" } | ||||
|   <TranslationInput {value} on:submit={() => dispatch("submit")} {args} /> | ||||
|   <TranslationInput {value} on:submit {args} /> | ||||
| {:else if type === "direction"} | ||||
|   <DirectionInput {value} mapProperties={InputHelpers.constructMapProperties(properties)} /> | ||||
| {:else if type === "date"} | ||||
|  | @ -44,9 +43,9 @@ | |||
| {:else if type === "image"} | ||||
|   <ImageHelper { value } /> | ||||
| {:else if type === "tag"} | ||||
|   <TagInput { value } /> | ||||
|   <TagInput { value } on:submit /> | ||||
| {:else if type === "simple_tag"} | ||||
|   <SimpleTagInput { value } {args} /> | ||||
|   <SimpleTagInput { value } {args} on:submit /> | ||||
| {:else if type === "opening_hours"} | ||||
|   <OpeningHoursInput { value } /> | ||||
| {:else if type === "wikidata"} | ||||
|  |  | |||
|  | @ -109,7 +109,7 @@ | |||
|      * Dispatches the submit, but only if the value is valid | ||||
|      */ | ||||
|     function sendSubmit(){ | ||||
|         if(feedback.data){ | ||||
|         if(feedback?.data){ | ||||
|             console.log("Not sending a submit as there is feedback") | ||||
|         } | ||||
|         dispatch("submit")  | ||||
|  |  | |||
|  | @ -259,7 +259,6 @@ | |||
|               value={freeformInput} | ||||
|               on:selected={() => (selectedMapping = config.mappings?.length)} | ||||
|               on:submit={onSave} | ||||
|               submit={onSave} | ||||
|             /> | ||||
|           </label> | ||||
|         {/if} | ||||
|  |  | |||
|  | @ -1336,6 +1336,7 @@ export default class SpecialVisualizations { | |||
|                                 const tr = typeof v === "string" ? JSON.parse(v) : v | ||||
|                                 return new Translation(tr).SetClass("font-bold") | ||||
|                             } catch (e) { | ||||
|                                 console.error("Cannot create a translation for", v, "due to", e) | ||||
|                                 return JSON.stringify(v) | ||||
|                             } | ||||
|                         }) | ||||
|  |  | |||
|  | @ -0,0 +1,28 @@ | |||
| <script lang="ts"> | ||||
|   import Marker from "../Map/Marker.svelte"; | ||||
|   import NextButton from "../Base/NextButton.svelte"; | ||||
|   import { createEventDispatcher } from "svelte"; | ||||
|   import { AllSharedLayers } from "../../Customizations/AllSharedLayers"; | ||||
| 
 | ||||
|   export let layerIds : { id: string }[] | ||||
|   const dispatch = createEventDispatcher<{layerSelected: string}>() | ||||
| 
 | ||||
|   function fetchIconDescription(layerId): any { | ||||
|     return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon; | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if layerIds.length > 0} | ||||
|   <slot name="title"/> | ||||
| <div class="flex flex-wrap"> | ||||
|   {#each Array.from(layerIds) as layer} | ||||
|     <NextButton clss="small" on:click={() => dispatch("layerSelected", layer.id)}> | ||||
|       <div class="w-4 h-4 mr-1"> | ||||
|         <Marker icons={fetchIconDescription(layer.id)} /> | ||||
|       </div> | ||||
|       {layer.id} | ||||
|     </NextButton> | ||||
|   {/each} | ||||
| </div> | ||||
|   {/if} | ||||
|  | @ -11,6 +11,7 @@ | |||
|   import type { ConversionMessage } from "../../Models/ThemeConfig/Conversion/Conversion"; | ||||
|   import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; | ||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
| 
 | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||
| 
 | ||||
|  | @ -25,7 +26,6 @@ | |||
|    * Blacklist of regions for the general area tab | ||||
|    * These are regions which are handled by a different tab | ||||
|    */ | ||||
|   const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title", "linerendering", "pointrendering"]; | ||||
|   const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group)); | ||||
| 
 | ||||
|   const perRegion: Record<string, ConfigMeta[]> = {}; | ||||
|  | @ -33,12 +33,7 @@ | |||
|     perRegion[region] = layerSchema.filter(meta => meta.hints.group === region); | ||||
|   } | ||||
| 
 | ||||
|   const baselayerRegions: string[] = ["Basic", "presets", "filters"]; | ||||
|   for (const baselayerRegion of baselayerRegions) { | ||||
|     if (perRegion[baselayerRegion] === undefined) { | ||||
|       console.error("BaseLayerRegions in editLayer: no items have group '" + baselayerRegion + "\""); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const title: Store<string> = state.getStoreFor(["id"]); | ||||
|   const wl = window.location; | ||||
|   const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout="; | ||||
|  | @ -46,18 +41,46 @@ | |||
|   function firstPathsFor(...regionNames: string[]): Set<string> { | ||||
|     const pathNames = new Set<string>(); | ||||
|     for (const regionName of regionNames) { | ||||
|       const region: ConfigMeta[] = perRegion[regionName] | ||||
|       const region: ConfigMeta[] = perRegion[regionName]; | ||||
|       for (const configMeta of region) { | ||||
|         pathNames.add(configMeta.path[0]) | ||||
|         pathNames.add(configMeta.path[0]); | ||||
|       } | ||||
|     } | ||||
|     return pathNames; | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   function configForRequiredField(id: string): ConfigMeta{ | ||||
|     let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id) | ||||
|     config = Utils.Clone(config) | ||||
|     config.required = true | ||||
|     console.log(">>>", config) | ||||
|     config.hints.ifunset = undefined | ||||
|     return  config | ||||
|   } | ||||
| 
 | ||||
|   let requiredFields = ["id", "name", "description"]; | ||||
|   let currentlyMissing = state.configuration.map(config => { | ||||
|     const missing = []; | ||||
|     for (const requiredField of requiredFields) { | ||||
|       if (!config[requiredField]) { | ||||
|         missing.push(requiredField); | ||||
|       } | ||||
|     } | ||||
|     return missing; | ||||
|   }); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="w-full flex justify-between"> | ||||
| {#if $currentlyMissing.length > 0} | ||||
| 
 | ||||
|   {#each requiredFields as required} | ||||
|     <SchemaBasedInput {state} | ||||
|                       schema={configForRequiredField(required)} | ||||
|                       path={[required]} /> | ||||
|   {/each} | ||||
| {:else} | ||||
|   <div class="w-full flex justify-between my-2"> | ||||
|     <slot /> | ||||
|     <h3>Editing layer {$title}</h3> | ||||
|     {#if $hasErrors > 0} | ||||
|  | @ -72,39 +95,52 @@ | |||
|   <div class="m4"> | ||||
|     <TabbedGroup> | ||||
|       <div slot="title0" class="flex">General properties | ||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor(...baselayerRegions)} {state} /> | ||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> | ||||
|       </div> | ||||
|       <div class="flex flex-col" slot="content0"> | ||||
|       {#each baselayerRegions as region} | ||||
|         <Region {state} configs={perRegion[region]} title={region} /> | ||||
|       {/each} | ||||
|         <Region {state} configs={perRegion["Basic"]} /> | ||||
| 
 | ||||
|       </div> | ||||
| 
 | ||||
| 
 | ||||
|       <div slot="title1" class="flex">Information panel (questions and answers) | ||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /></div> | ||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /> | ||||
|       </div> | ||||
|       <div slot="content1"> | ||||
|         <Region configs={perRegion["title"]} {state} title="Popup title" /> | ||||
|         <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> | ||||
|         <Region configs={perRegion["editing"]} {state} title="Other editing elements" /> | ||||
|       </div> | ||||
| 
 | ||||
|     <div slot="title2" class="flex">Rendering on the map | ||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /></div> | ||||
|       <div slot="title2"> | ||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("presets")} {state} /> | ||||
|         Creating a new point | ||||
|       </div> | ||||
| 
 | ||||
|       <div slot="content2"> | ||||
|         <Region {state} configs={perRegion["presets"]} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div slot="title3" class="flex">Rendering on the map | ||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("linerendering","pointrendering")} {state} /> | ||||
|       </div> | ||||
|       <div slot="content3"> | ||||
|         <Region configs={perRegion["linerendering"]} {state} /> | ||||
|         <Region configs={perRegion["pointrendering"]} {state} /> | ||||
|       </div> | ||||
| 
 | ||||
|     <div slot="title3" class="flex">Advanced functionality | ||||
|       <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /></div> | ||||
|     <div slot="content3"> | ||||
|       <div slot="title4" class="flex">Advanced functionality | ||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced","expert")} {state} /> | ||||
|       </div> | ||||
|       <div slot="content4"> | ||||
|         <Region configs={perRegion["advanced"]} {state} /> | ||||
|         <Region configs={perRegion["expert"]} {state} /> | ||||
|       </div> | ||||
|     <div slot="title4">Configuration file</div> | ||||
|     <div slot="content4"> | ||||
|       <div slot="title5">Configuration file</div> | ||||
|       <div slot="content5"> | ||||
|         <div> | ||||
|           Below, you'll find the raw configuration file in `.json`-format. | ||||
|         This is mostly for debugging purposes | ||||
|           This is mosSendertly for debugging purposes | ||||
|         </div> | ||||
|         <div class="literal-code"> | ||||
|           {JSON.stringify($configuration, null, "  ")} | ||||
|  | @ -123,3 +159,4 @@ | |||
|     </TabbedGroup> | ||||
| 
 | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| import { ConfigMeta } from "./configMeta" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import { QueryParameters } from "../../Logic/Web/QueryParameters" | ||||
| import { | ||||
|     ConversionContext, | ||||
|     ConversionMessage, | ||||
|  | @ -16,25 +14,29 @@ import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Jso | |||
| import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||
| import StudioServer from "./StudioServer" | ||||
| import { Utils } from "../../Utils" | ||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| 
 | ||||
| /** | ||||
|  * Sends changes back to the server | ||||
|  */ | ||||
| export class LayerStateSender { | ||||
|     constructor(layerState: EditLayerState) { | ||||
|         layerState.configuration.addCallback(async (config) => { | ||||
|             const id = config.id | ||||
|         const layerId = layerState.configuration.map((config) => config.id) | ||||
|         layerState.configuration | ||||
|             .mapD((config) => JSON.stringify(config, null, "  ")) | ||||
|             .stabilized(100) | ||||
|             .addCallbackD(async (config) => { | ||||
|                 const id = layerId.data | ||||
|                 if (id === undefined) { | ||||
|                     console.warn("No id found in layer, not updating") | ||||
|                     return | ||||
|                 } | ||||
|             await layerState.server.updateLayer(<LayerConfigJson>config) | ||||
|                 await layerState.server.updateLayer(id, config) | ||||
|             }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class EditLayerState { | ||||
|     public readonly osmConnection: OsmConnection | ||||
|     public readonly schema: ConfigMeta[] | ||||
| 
 | ||||
|     public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> } | ||||
|  | @ -44,17 +46,14 @@ export default class EditLayerState { | |||
|     >({}) | ||||
|     public readonly messages: Store<ConversionMessage[]> | ||||
|     public readonly server: StudioServer | ||||
|     // Needed for the special visualisations
 | ||||
|     public readonly osmConnection: OsmConnection | ||||
|     private readonly _stores = new Map<string, UIEventSource<any>>() | ||||
| 
 | ||||
|     constructor(schema: ConfigMeta[], server: StudioServer) { | ||||
|     constructor(schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection) { | ||||
|         this.schema = schema | ||||
|         this.server = server | ||||
|         this.osmConnection = new OsmConnection({ | ||||
|             oauth_token: QueryParameters.GetQueryParameter( | ||||
|                 "oauth_token", | ||||
|                 undefined, | ||||
|                 "Used to complete the login" | ||||
|             ), | ||||
|         }) | ||||
|         this.osmConnection = osmConnection | ||||
|         this.featureSwitches = { | ||||
|             featureSwitchIsDebugging: new UIEventSource<boolean>(true), | ||||
|         } | ||||
|  | @ -118,7 +117,6 @@ export default class EditLayerState { | |||
|         return entry | ||||
|     } | ||||
| 
 | ||||
|     private readonly _stores = new Map<string, UIEventSource<any>>() | ||||
|     public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> { | ||||
|         const key = path.join(".") | ||||
| 
 | ||||
|  | @ -139,7 +137,9 @@ export default class EditLayerState { | |||
|         value: Store<any>, | ||||
|         noInitialSync: boolean = false | ||||
|     ): () => void { | ||||
|         const unsync = value.addCallback((v) => this.setValueAt(path, v)) | ||||
|         const unsync = value.addCallback((v) => { | ||||
|             this.setValueAt(path, v) | ||||
|         }) | ||||
|         if (!noInitialSync) { | ||||
|             this.setValueAt(path, value.data) | ||||
|         } | ||||
|  | @ -180,6 +180,7 @@ export default class EditLayerState { | |||
| 
 | ||||
|     public setValueAt(path: ReadonlyArray<string | number>, v: any) { | ||||
|         let entry = this.configuration.data | ||||
|         console.log("Setting value at", path, v) | ||||
|         const isUndefined = | ||||
|             v === undefined || | ||||
|             v === null || | ||||
|  | @ -197,15 +198,35 @@ export default class EditLayerState { | |||
|             } | ||||
|             entry = entry[breadcrumb] | ||||
|         } | ||||
| 
 | ||||
|         const lastBreadcrumb = path.at(-1) | ||||
|         if (isUndefined) { | ||||
|             if (entry && entry[lastBreadcrumb]) { | ||||
|                 console.log("Deleting", lastBreadcrumb, "of", path.join(".")) | ||||
|                 delete entry[lastBreadcrumb] | ||||
|                 this.configuration.ping() | ||||
|             } | ||||
|         } else { | ||||
|         } else if (entry[lastBreadcrumb] !== v) { | ||||
|             console.log("Assigning and pinging at", path) | ||||
|             entry[lastBreadcrumb] = v | ||||
|         } | ||||
|             this.configuration.ping() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public messagesFor(path: ReadonlyArray<string | number>): Store<ConversionMessage[]> { | ||||
|         return this.messages.map((msgs) => { | ||||
|             if (!msgs) { | ||||
|                 return [] | ||||
|             } | ||||
|             return msgs.filter((msg) => { | ||||
|                 const pth = msg.context.path | ||||
|                 for (let i = 0; i < Math.min(pth.length, path.length); i++) { | ||||
|                     if (pth[i] !== path[i]) { | ||||
|                         return false | ||||
|                     } | ||||
|                 } | ||||
|                 return true | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -110,7 +110,7 @@ | |||
|       {/if} | ||||
|       <div class="border border-black"> | ||||
|         {#if isTagRenderingBlock} | ||||
|           <TagRenderingInput path={path.concat(value)} {state} {schema} > | ||||
|           <TagRenderingInput path={[...path, (value)]} {state} {schema} > | ||||
|             <button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit" | ||||
|                     on:click={() => {del(value)}}> | ||||
|               <TrashIcon class="w-4 h-4" /> | ||||
|  |  | |||
|  | @ -21,12 +21,12 @@ | |||
|   const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema); | ||||
|   let type = schema.hints.typehint ?? "string"; | ||||
| 
 | ||||
|   let rendervalue = ((schema.hints.inline ?? schema.path.join(".")) + " <b>{translated(value)}</b>"); | ||||
|   let rendervalue = (schema.hints.inline ?? schema.path.join(".")) + (isTranslation ? " <b>{translated(value)}</b>": " <b>{value}</b>"); | ||||
|   | ||||
|   if(schema.type === "boolean"){ | ||||
|     rendervalue = undefined | ||||
|   } | ||||
|   if(schema.hints.typehint === "tag") { | ||||
|   if(schema.hints.typehint === "tag" || schema.hints.typehint === "simple_tag") { | ||||
|     rendervalue = "{tags()}" | ||||
|   } | ||||
|    | ||||
|  | @ -61,12 +61,12 @@ | |||
|   if (schema.hints.default) { | ||||
|     configJson.mappings = [{ | ||||
|       if: "value=", // We leave this blank | ||||
|       then: schema.path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "") | ||||
|       then: path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "") | ||||
|     }]; | ||||
|   } else if (!schema.required) { | ||||
|     configJson.mappings = [{ | ||||
|       if: "value=", | ||||
|       then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "") | ||||
|       then: path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "") | ||||
|     }]; | ||||
|   } | ||||
| 
 | ||||
|  | @ -109,15 +109,7 @@ | |||
|   } | ||||
|   let config: TagRenderingConfig; | ||||
|   let err: string = undefined; | ||||
|   let messages = state.messages.mapD(msgs => msgs.filter(msg => { | ||||
|     const pth = msg.context.path; | ||||
|     for (let i = 0; i < Math.min(pth.length, path.length); i++) { | ||||
|       if (pth[i] !== path[i]) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|     return true; | ||||
|   })); | ||||
|   let messages = state.messagesFor(path) | ||||
|   try { | ||||
|     config = new TagRenderingConfig(configJson, "config based on " + schema.path.join(".")); | ||||
|   } catch (e) { | ||||
|  | @ -130,7 +122,7 @@ | |||
|     onDestroy(state.register(path, tags.map(tgs => { | ||||
|       const v = tgs["value"]; | ||||
|       if (typeof v !== "string") { | ||||
|         return v; | ||||
|         return { ... v }; | ||||
|       } | ||||
|       if (schema.type === "boolan") { | ||||
|         return v === "true" || v === "yes" || v === "1"; | ||||
|  | @ -140,7 +132,6 @@ | |||
|           return true; | ||||
|         } | ||||
|         if (v === "false" || v === "no" || v === "0") { | ||||
|           console.log("Setting false..."); | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
|  | @ -173,7 +164,7 @@ | |||
|       {/each} | ||||
|     {/if} | ||||
|     {#if window.location.hostname === "127.0.0.1"} | ||||
|       <span class="subtle">{schema.path.join(".")} {schema.hints.typehint}</span> | ||||
|       <span class="subtle">{path.join(".")} {schema.hints.typehint}</span> | ||||
|     {/if} | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ | |||
|   import type { JsonSchemaType } from "./jsonSchema"; | ||||
|   // @ts-ignore | ||||
|   import nmd from "nano-markdown"; | ||||
|   import { writable } from "svelte/store"; | ||||
| 
 | ||||
|   /** | ||||
|    * If 'types' is defined: allow the user to pick one of the types to input. | ||||
|  | @ -127,14 +126,14 @@ | |||
|     possibleTypes.sort((a, b) => b.optionalMatches - a.optionalMatches); | ||||
|     possibleTypes.sort((a, b) => b.matchingPropertiesCount - a.matchingPropertiesCount); | ||||
|     if (possibleTypes.length > 0) { | ||||
|       chosenOption = possibleTypes[0].index | ||||
|       chosenOption = possibleTypes[0].index; | ||||
|       tags.setData({ chosen_type_index: "" + chosenOption }); | ||||
| 
 | ||||
|     } | ||||
|   } else if (defaultOption !== undefined) { | ||||
|     tags.setData({ chosen_type_index: "" + defaultOption }); | ||||
|   } else { | ||||
|     chosenOption = defaultOption | ||||
|     chosenOption = defaultOption; | ||||
|   } | ||||
| 
 | ||||
|   if (hasBooleanOption >= 0 || lastIsString) { | ||||
|  | @ -154,7 +153,7 @@ | |||
|   let subSchemas: ConfigMeta[] = []; | ||||
| 
 | ||||
|   let subpath = path; | ||||
|   const store = state.getStoreFor(path) | ||||
|   const store = state.getStoreFor(path); | ||||
|   onDestroy(tags.addCallbackAndRun(tags => { | ||||
|     if (tags["value"] !== undefined && tags["value"] !== "") { | ||||
|       chosenOption = undefined; | ||||
|  | @ -170,7 +169,7 @@ | |||
|       for (const key of type?.required ?? []) { | ||||
|         o[key] ??= {}; | ||||
|       } | ||||
|       store.setData(o) | ||||
|       store.setData(o); | ||||
|     } | ||||
|     if (!type) { | ||||
|       return; | ||||
|  | @ -191,6 +190,7 @@ | |||
|       subSchemas.push(...(state.getSchema([...cleanPath, crumble]))); | ||||
|     } | ||||
|   })); | ||||
|   let messages = state.messagesFor(path); | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
|  | @ -209,5 +209,9 @@ | |||
|       <SchemaBasedInput {state} schema={subschema} | ||||
|                         path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput> | ||||
|     {/each} | ||||
|   {:else if $messages.length > 0} | ||||
|     {#each $messages as msg} | ||||
|       <div class="alert">{msg.message}</div> | ||||
|     {/each} | ||||
|   {/if} | ||||
| </div> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|   export let path: (string | number)[] = []; | ||||
|   export let schema: ConfigMeta; | ||||
| 
 | ||||
|   let value = new UIEventSource<string>("{}"); | ||||
|   let value = new UIEventSource<string>({}); | ||||
|   console.log("Registering translation to path", path) | ||||
|   state.register(path, value.mapD(v => JSON.parse(value.data  ))); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,23 +1,51 @@ | |||
| import { Utils } from "../../Utils" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| 
 | ||||
| export default class StudioServer { | ||||
|     private readonly url: string | ||||
|     private readonly _userId: Store<number> | ||||
| 
 | ||||
|     constructor(url: string) { | ||||
|     constructor(url: string, userId: Store<number>) { | ||||
|         this.url = url | ||||
|         this._userId = userId | ||||
|     } | ||||
| 
 | ||||
|     public async fetchLayerOverview(): Promise<Set<string>> { | ||||
|     public async fetchLayerOverview(): Promise< | ||||
|         { | ||||
|             id: string | ||||
|             owner: number | ||||
|         }[] | ||||
|     > { | ||||
|         const uid = this._userId.data | ||||
|         let uidQueryParam = "" | ||||
|         if (this._userId.data !== undefined) { | ||||
|             uidQueryParam = "?userId=" + uid | ||||
|         } | ||||
|         const { allFiles } = <{ allFiles: string[] }>( | ||||
|             await Utils.downloadJson(this.url + "/overview") | ||||
|             await Utils.downloadJson(this.url + "/overview" + uidQueryParam) | ||||
|         ) | ||||
|         const layers = allFiles | ||||
|             .filter((f) => f.startsWith("layers/")) | ||||
|             .map((l) => l.substring(l.lastIndexOf("/") + 1, l.length - ".json".length)) | ||||
|             .filter((layerId) => Constants.priviliged_layers.indexOf(<any>layerId) < 0) | ||||
|         return new Set<string>(layers) | ||||
|         const layerOverview: { | ||||
|             id: string | ||||
|             owner: number | undefined | ||||
|         }[] = [] | ||||
|         for (let file of allFiles) { | ||||
|             let owner = undefined | ||||
|             if (file.startsWith("" + uid)) { | ||||
|                 owner = uid | ||||
|                 file = file.substring(file.indexOf("/") + 1) | ||||
|             } | ||||
|             if (!file.startsWith("layers/")) { | ||||
|                 continue | ||||
|             } | ||||
|             const id = file.substring(file.lastIndexOf("/") + 1, file.length - ".json".length) | ||||
|             if (Constants.priviliged_layers.indexOf(<any>id) > 0) { | ||||
|                 continue | ||||
|             } | ||||
|             layerOverview.push({ id, owner }) | ||||
|         } | ||||
|         return layerOverview | ||||
|     } | ||||
| 
 | ||||
|     async fetchLayer(layerId: string): Promise<LayerConfigJson> { | ||||
|  | @ -28,8 +56,7 @@ export default class StudioServer { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async updateLayer(config: LayerConfigJson) { | ||||
|         const id = config.id | ||||
|     async updateLayer(id: string, config: string) { | ||||
|         if (id === undefined || id === "") { | ||||
|             return | ||||
|         } | ||||
|  | @ -38,11 +65,13 @@ export default class StudioServer { | |||
|             headers: { | ||||
|                 "Content-Type": "application/json;charset=utf-8", | ||||
|             }, | ||||
|             body: JSON.stringify(config, null, "  "), | ||||
|             body: config, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public layerUrl(id: string) { | ||||
|         return `${this.url}/layers/${id}/${id}.json` | ||||
|         const uid = this._userId.data | ||||
|         const uidStr = uid !== undefined ? "/" + uid : "" | ||||
|         return `${this.url}${uidStr}/layers/${id}/${id}.json` | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -139,7 +139,7 @@ if (!initialTag) { | |||
|   <div class="border-l-4 border-black flex flex-col ml-1 pl-1"> | ||||
|     {#each $basicTags as basicTag (basicTag)} | ||||
|       <div class="flex"> | ||||
|         <BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} /> | ||||
|         <BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} on:submit /> | ||||
|         {#if $basicTags.length + $expressions.length > 1} | ||||
|           <button class="border border-black rounded-full w-fit h-fit p-0" | ||||
|                   on:click={() => removeTag(basicTag)}> | ||||
|  |  | |||
|  | @ -111,7 +111,7 @@ | |||
|     <div class="flex h-fit "> | ||||
| 
 | ||||
|         <ValidatedInput feedback={feedbackKey} placeholder="The key of the tag" type="key" | ||||
|                         value={keyValue}></ValidatedInput> | ||||
|                         value={keyValue} on:submit></ValidatedInput> | ||||
|         <select bind:value={mode} on:focusin={() => dropdownFocussed.setData(true)} on:focusout={() => dropdownFocussed.setData(false)}> | ||||
|             {#each modes as option} | ||||
|                 <option value={option}> | ||||
|  | @ -120,7 +120,7 @@ | |||
|             {/each} | ||||
|         </select> | ||||
|         <ValidatedInput feedback={feedbackValue} placeholder="The value of the tag" type="string" | ||||
|                         value={valueValue}></ValidatedInput> | ||||
|                         value={valueValue} on:submit></ValidatedInput> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if $feedbackKey} | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ export let silent: boolean | |||
| </script> | ||||
| 
 | ||||
| <div class="m-2"> | ||||
|     <TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly}> | ||||
|     <TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly} on:submit> | ||||
|         <slot name="delete" slot="delete"/> | ||||
|     </TagExpression> | ||||
| </div> | ||||
|  |  | |||
|  | @ -58,7 +58,6 @@ tags.addCallbackAndRunD(tgs => { | |||
| 
 | ||||
| let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]); | ||||
| 
 | ||||
| $: console.log("Allow questions:", $allowQuestions) | ||||
| const topLevelItems: Record<string, ConfigMeta> = {}; | ||||
| for (const item of questionableTagRenderingSchemaRaw) { | ||||
|   if (item.path.length === 1) { | ||||
|  | @ -81,7 +80,6 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch | |||
| </script> | ||||
| 
 | ||||
| {#if typeof value === "string"} | ||||
| 
 | ||||
|   <div class="flex low-interaction"> | ||||
|     <TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state} | ||||
|                           {tags} /> | ||||
|  | @ -92,12 +90,9 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch | |||
|     <div class="flex justify-end"> | ||||
|       <slot name="upper-right" /> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if $allowQuestions} | ||||
|       <SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} /> | ||||
|       <SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} /> | ||||
|       {:else} | ||||
|        | ||||
|     {/if} | ||||
|     {#each ($mappings ?? []) as mapping, i (mapping)} | ||||
|       <div class="flex interactive w-full"> | ||||
|  |  | |||
|  | @ -2,13 +2,10 @@ | |||
| 
 | ||||
| 
 | ||||
|   import NextButton from "./Base/NextButton.svelte"; | ||||
|   import { UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import ValidatedInput from "./InputElement/ValidatedInput.svelte"; | ||||
|   import { Store, UIEventSource } from "../Logic/UIEventSource"; | ||||
|   import EditLayerState from "./Studio/EditLayerState"; | ||||
|   import EditLayer from "./Studio/EditLayer.svelte"; | ||||
|   import Loading from "../assets/svg/Loading.svelte"; | ||||
|   import Marker from "./Map/Marker.svelte"; | ||||
|   import { AllSharedLayers } from "../Customizations/AllSharedLayers"; | ||||
|   import StudioServer from "./Studio/StudioServer"; | ||||
|   import LoginToggle from "./Base/LoginToggle.svelte"; | ||||
|   import { OsmConnection } from "../Logic/Osm/OsmConnection"; | ||||
|  | @ -17,53 +14,54 @@ | |||
|   import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json"; | ||||
|   import If from "./Base/If.svelte"; | ||||
|   import BackButton from "./Base/BackButton.svelte"; | ||||
|   import ChooseLayerToEdit from "./Studio/ChooseLayerToEdit.svelte"; | ||||
|   import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"; | ||||
|   import FloatOver from "./Base/FloatOver.svelte"; | ||||
|   import Walkthrough from "./Walkthrough/Walkthrough.svelte"; | ||||
|   import * as intro from "../assets/studio_introduction.json"; | ||||
|   import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini"; | ||||
|   import type { ConfigMeta } from "./Studio/configMeta"; | ||||
| 
 | ||||
|   export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org"; | ||||
|   const studio = new StudioServer(studioUrl); | ||||
|   let layersWithErr = UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview()); | ||||
|   let layers = layersWithErr.mapD(l => l.success); | ||||
|   let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined; | ||||
| 
 | ||||
|   let osmConnection = new OsmConnection(new OsmConnection({ | ||||
|     oauth_token: QueryParameters.GetQueryParameter( | ||||
|       "oauth_token", | ||||
|       undefined, | ||||
|       "Used to complete the login" | ||||
|     ) | ||||
|   })); | ||||
|   const createdBy = osmConnection.userDetails.data.name; | ||||
|   const uid = osmConnection.userDetails.map(ud => ud?.uid); | ||||
|   const studio = new StudioServer(studioUrl, uid); | ||||
| 
 | ||||
|   let layersWithErr = uid.bind(uid => UIEventSource.FromPromiseWithErr(studio.fetchLayerOverview())); | ||||
|   let layers: Store<{ owner: number }[]> = layersWithErr.mapD(l => l.success); | ||||
|   let selfLayers = layers.mapD(ls => ls.filter(l => l.owner === uid.data), [uid]); | ||||
|   let otherLayers = layers.mapD(ls => ls.filter(l => l.owner !== uid.data), [uid]); | ||||
| 
 | ||||
|   let state: undefined | "edit_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined; | ||||
| 
 | ||||
|   let initialLayerConfig: { id: string }; | ||||
|   let newLayerId = new UIEventSource<string>(""); | ||||
|   /** | ||||
|    * Also used in the input field as 'feedback', hence not a mappedStore as it must be writable | ||||
|    */ | ||||
|   let layerIdFeedback = new UIEventSource<string>(undefined); | ||||
|   newLayerId.addCallbackD(layerId => { | ||||
|     if (layerId === "") { | ||||
|       return; | ||||
|     } | ||||
|     if (layers.data?.has(layerId)) { | ||||
|       layerIdFeedback.setData("This id is already used"); | ||||
|     } | ||||
|   }, [layers]); | ||||
| 
 | ||||
| 
 | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||
| 
 | ||||
|   let editLayerState = new EditLayerState(layerSchema, studio); | ||||
|   let editLayerState = new EditLayerState(layerSchema, studio, osmConnection); | ||||
|   let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id); | ||||
| 
 | ||||
|   function fetchIconDescription(layerId): any { | ||||
|     return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon; | ||||
|   let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true")); | ||||
| 
 | ||||
|   async function editLayer(event: Event) { | ||||
|     const layerId = event.detail; | ||||
|     state = "loading"; | ||||
|     initialLayerConfig = await studio.fetchLayer(layerId); | ||||
|     state = "editing_layer"; | ||||
|   } | ||||
| 
 | ||||
|   async function createNewLayer() { | ||||
|     if (layerIdFeedback.data !== undefined) { | ||||
|       console.warn("There is still some feedback - not starting to create a new layer"); | ||||
|       return; | ||||
|     } | ||||
|     state = "loading"; | ||||
|     const id = newLayerId.data; | ||||
|     const createdBy = osmConnection.userDetails.data.name; | ||||
| 
 | ||||
| 
 | ||||
|     try { | ||||
| 
 | ||||
|       const loaded = await studio.fetchLayer(id); | ||||
|       initialLayerConfig = loaded ?? { | ||||
|         id, credits: createdBy, | ||||
|     initialLayerConfig = { | ||||
|       credits: createdBy, | ||||
|       minzoom: 15, | ||||
|       pointRendering: [ | ||||
|         { | ||||
|  | @ -79,19 +77,9 @@ | |||
|         color: "blue" | ||||
|       }] | ||||
|     }; | ||||
|     } catch (e) { | ||||
|       initialLayerConfig = { id, credits: createdBy }; | ||||
|     } | ||||
|     state = "editing_layer"; | ||||
|   } | ||||
| 
 | ||||
|   let osmConnection = new OsmConnection(new OsmConnection({ | ||||
|     oauth_token: QueryParameters.GetQueryParameter( | ||||
|       "oauth_token", | ||||
|       undefined, | ||||
|       "Used to complete the login" | ||||
|     ) | ||||
|   })); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
|  | @ -125,13 +113,14 @@ | |||
|       </NextButton> | ||||
|     </div> | ||||
|     {#if state === undefined} | ||||
|       <div class="m-4"> | ||||
|         <h1>MapComplete Studio</h1> | ||||
|         <div class="w-full flex flex-col"> | ||||
| 
 | ||||
|           <NextButton on:click={() => state = "edit_layer"}> | ||||
|             Edit an existing layer | ||||
|           </NextButton> | ||||
|         <NextButton on:click={() => state = "new_layer"}> | ||||
|           <NextButton on:click={() => createNewLayer()}> | ||||
|             Create a new layer | ||||
|           </NextButton> | ||||
|           <!-- | ||||
|  | @ -142,49 +131,23 @@ | |||
|             Create a new theme | ||||
|           </NextButton> | ||||
|           --> | ||||
|           <NextButton clss="small" on:click={() => {showIntro.setData(true)} }> | ||||
|             <QuestionMarkCircleIcon class="w-6 h-6" /> | ||||
|             Show the introduction again | ||||
|           </NextButton> | ||||
|         </div> | ||||
|       </div> | ||||
|     {:else if state === "edit_layer"} | ||||
| 
 | ||||
|       <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton> | ||||
|       <h3>Choose a layer to edit</h3> | ||||
|       <div class="flex flex-wrap"> | ||||
|         {#each Array.from($layers) as layerId} | ||||
|           <NextButton clss="small" on:click={async () => { | ||||
|         state = "loading" | ||||
|         initialLayerConfig = await studio.fetchLayer(layerId) | ||||
|         state = "editing_layer" | ||||
|        }}> | ||||
|             <div class="w-4 h-4 mr-1"> | ||||
|               <Marker icons={fetchIconDescription(layerId)} /> | ||||
|             </div> | ||||
|             {layerId} | ||||
|           </NextButton> | ||||
|         {/each} | ||||
|       </div> | ||||
|     {:else if state === "new_layer"} | ||||
|        | ||||
|       <div class="interactive flex m-2 rounded-2xl flex-col p-2"> | ||||
|         <h3>Enter the ID for the new layer</h3> | ||||
|         A good ID is: | ||||
|         <ul> | ||||
|           <li>a noun</li> | ||||
|           <li>singular</li> | ||||
|           <li>describes the object</li> | ||||
|           <li>in English</li> | ||||
|         </ul> | ||||
|         <div class="m-2 p-2 w-full"> | ||||
| 
 | ||||
|           <ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} on:submit={() => createNewLayer()} /> | ||||
|         </div> | ||||
|         {#if $layerIdFeedback !== undefined} | ||||
|           <div class="alert"> | ||||
|             {$layerIdFeedback} | ||||
|           </div> | ||||
|         {:else } | ||||
|           <NextButton clss="primary" on:click={() => createNewLayer()}> | ||||
|             Create layer {$newLayerId} | ||||
|           </NextButton> | ||||
|         {/if} | ||||
|       <div class="flex flex-col m-4"> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio | ||||
|         </BackButton> | ||||
|         <h2>Choose a layer to edit</h2> | ||||
|         <ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}> | ||||
|           <h3 slot="title">Your layers</h3> | ||||
|         </ChooseLayerToEdit> | ||||
|         <h3>Official layers</h3> | ||||
|         <ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} /> | ||||
|       </div> | ||||
|     {:else if state === "loading"} | ||||
|       <div class="w-8 h-8"> | ||||
|  | @ -192,8 +155,19 @@ | |||
|       </div> | ||||
|     {:else if state === "editing_layer"} | ||||
|       <EditLayer {initialLayerConfig} state={editLayerState}> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio</BackButton> | ||||
|         <BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio | ||||
|         </BackButton> | ||||
|       </EditLayer> | ||||
|     {/if} | ||||
|   </LoginToggle> | ||||
| </If> | ||||
| 
 | ||||
| 
 | ||||
| {#if $showIntro} | ||||
|   <FloatOver> | ||||
|     <div class="flex p-4 h-full"> | ||||
|       <Walkthrough pages={intro.sections} on:done={() => {showIntro.setData(false)}} /> | ||||
|     </div> | ||||
|   </FloatOver> | ||||
| 
 | ||||
| {/if} | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue