forked from MapComplete/MapComplete
		
	Studio: add previews of the questions, edit them in floatover
This commit is contained in:
		
							parent
							
								
									8bc555fbe0
								
							
						
					
					
						commit
						ac6e38a256
					
				
					 12 changed files with 391 additions and 159 deletions
				
			
		|  | @ -2400,6 +2400,7 @@ | |||
|     }, | ||||
|     { | ||||
|       "id": "sugar_free", | ||||
|       "labels": ["diets"], | ||||
|       "question": { | ||||
|         "en": "Does this shop have a sugar free offering?", | ||||
|         "de": "Verkauft das Geschäft zuckerfreie Produkte?" | ||||
|  | @ -2441,6 +2442,7 @@ | |||
|     }, | ||||
|     { | ||||
|       "id": "lactose_free", | ||||
|       "labels": ["diets"], | ||||
|       "question": { | ||||
|         "en": "Does {title()} have a lactose-free offering?", | ||||
|         "de": "Verkauft {title()} laktosefreie Produkte?" | ||||
|  | @ -2478,6 +2480,7 @@ | |||
|     }, | ||||
|     { | ||||
|       "id": "gluten_free", | ||||
|       "labels": ["diets"], | ||||
|       "question": { | ||||
|         "en": "Does this shop have a gluten free offering?", | ||||
|         "de": "Verkauft das Geschäft glutenfreie Produkte?" | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
| 
 | ||||
| <div | ||||
|   class="absolute top-0 right-0 h-screen w-screen p-4 md:p-6" | ||||
|   style="background-color: #00000088" | ||||
|   style="background-color: #00000088; z-index: 20" | ||||
|   on:click={() => {dispatch("close")}} | ||||
| > | ||||
|   <div class="content normal-background" on:click|stopPropagation={() => {}}> | ||||
|  |  | |||
|  | @ -15,8 +15,8 @@ | |||
|    * If set, 'loading' will act as if we are already logged in. | ||||
|    */ | ||||
|   export let ignoreLoading: boolean = false | ||||
|   let loadingStatus = state.osmConnection.loadingStatus | ||||
|   let badge = state.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true) | ||||
|   let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in") | ||||
|   let badge = state?.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true) | ||||
|   const t = Translations.t.general | ||||
|   const offlineModes: Partial<Record<OsmServiceState, Translation>> = { | ||||
|     offline: t.loginFailedOfflineMode, | ||||
|  | @ -24,7 +24,7 @@ | |||
|     unknown: t.loginFailedUnreachableMode, | ||||
|     readonly: t.loginFailedReadonlyMode, | ||||
|   } | ||||
|   const apiState = state.osmConnection.apiIsOnline | ||||
|   const apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online") | ||||
| </script> | ||||
| 
 | ||||
| {#if $badge} | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
|    */ | ||||
| 
 | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import { Store } from "../../Logic/UIEventSource" | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource"; | ||||
|   import type { OsmTags } from "../../Models/OsmFeature" | ||||
|   import LoginToggle from "../Base/LoginToggle.svelte" | ||||
|   import Translations from "../i18n/Translations" | ||||
|  | @ -28,14 +28,14 @@ | |||
|   export let labelText: string = undefined | ||||
|   const t = Translations.t.image | ||||
| 
 | ||||
|   let licenseStore = state.userRelatedState.imageLicense | ||||
|   let licenseStore = state?.userRelatedState?.imageLicense ?? new ImmutableStore("CC0") | ||||
| 
 | ||||
|   function handleFiles(files: FileList) { | ||||
|     for (let i = 0; i < files.length; i++) { | ||||
|       const file = files.item(i) | ||||
|       console.log("Got file", file.name) | ||||
|       try { | ||||
|         state.imageUploadManager.uploadImageAndApply(file, tags) | ||||
|         state.imageUploadManager?.uploadImageAndApply(file, tags) | ||||
|       } catch (e) { | ||||
|         alert(e) | ||||
|       } | ||||
|  |  | |||
|  | @ -35,9 +35,9 @@ | |||
|   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); | ||||
| 
 | ||||
|   // Will be bound if a freeform is available | ||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) | ||||
|   let selectedMapping: number = undefined | ||||
|   let checkedMappings: boolean[] | ||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]); | ||||
|   let selectedMapping: number = undefined; | ||||
|   let checkedMappings: boolean[]; | ||||
| 
 | ||||
|   /** | ||||
|    * Prepares and fills the checkedMappings | ||||
|  | @ -58,40 +58,40 @@ | |||
|       (checkedMappings === undefined || | ||||
|         checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) | ||||
|     ) { | ||||
|       const seenFreeforms = [] | ||||
|       TagUtils.FlattenMultiAnswer() | ||||
|       const seenFreeforms = []; | ||||
|       TagUtils.FlattenMultiAnswer(); | ||||
|       checkedMappings = [ | ||||
|         ...confg.mappings.map((mapping) => { | ||||
|           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs) | ||||
|           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs); | ||||
|           if (matches && confg.freeform) { | ||||
|             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()) | ||||
|             seenFreeforms.push(newProps[confg.freeform.key]) | ||||
|             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()); | ||||
|             seenFreeforms.push(newProps[confg.freeform.key]); | ||||
|           } | ||||
|           return matches | ||||
|         }), | ||||
|       ] | ||||
|           return matches; | ||||
|         }) | ||||
|       ]; | ||||
| 
 | ||||
|       if (tgs !== undefined && confg.freeform) { | ||||
|         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [] | ||||
|         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []; | ||||
|         for (const seenFreeform of seenFreeforms) { | ||||
|           if (!seenFreeform) { | ||||
|             continue | ||||
|             continue; | ||||
|           } | ||||
|           const index = unseenFreeformValues.indexOf(seenFreeform) | ||||
|           const index = unseenFreeformValues.indexOf(seenFreeform); | ||||
|           if (index < 0) { | ||||
|             continue | ||||
|             continue; | ||||
|           } | ||||
|           unseenFreeformValues.splice(index, 1) | ||||
|           unseenFreeformValues.splice(index, 1); | ||||
|         } | ||||
|         // TODO this has _to much_ values | ||||
|         freeformInput.setData(unseenFreeformValues.join(";")) | ||||
|         checkedMappings.push(unseenFreeformValues.length > 0) | ||||
|         freeformInput.setData(unseenFreeformValues.join(";")); | ||||
|         checkedMappings.push(unseenFreeformValues.length > 0); | ||||
|       } | ||||
|     } | ||||
|     if (confg.freeform?.key) { | ||||
|       if (!confg.multiAnswer) { | ||||
|         // Somehow, setting multi-answer freeform values is broken if this is not set | ||||
|         freeformInput.setData(tgs[confg.freeform.key]) | ||||
|         freeformInput.setData(tgs[confg.freeform.key]); | ||||
|       } | ||||
|     } else { | ||||
|       freeformInput.setData(undefined); | ||||
|  | @ -102,9 +102,9 @@ | |||
|   $: { | ||||
|     // Even though 'config' is not declared as a store, Svelte uses it as one to update the component | ||||
|     // We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes | ||||
|     initialize($tags, config) | ||||
|     initialize($tags, config); | ||||
|   } | ||||
|   export let selectedTags: TagsFilter = undefined | ||||
|   export let selectedTags: TagsFilter = undefined; | ||||
| 
 | ||||
|   let mappings: Mapping[] = config?.mappings; | ||||
|   let searchTerm: UIEventSource<string> = new UIEventSource(""); | ||||
|  | @ -166,39 +166,41 @@ | |||
|       .catch(console.error); | ||||
|   } | ||||
| 
 | ||||
|   let featureSwitchIsTesting = state.featureSwitchIsTesting ?? new ImmutableStore(false); | ||||
|   let featureSwitchIsDebugging = state.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); | ||||
|   let showTags = state.userRelatedState?.showTags ?? new ImmutableStore(undefined); | ||||
|   let numberOfCs = state.osmConnection.userDetails.data.csCount; | ||||
|   onDestroy( | ||||
|     state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||
|       numberOfCs = ud.csCount; | ||||
|     }) | ||||
|   ); | ||||
|   let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false); | ||||
|   let featureSwitchIsDebugging = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); | ||||
|   let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined); | ||||
|   let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0; | ||||
|   if (state) { | ||||
|     onDestroy( | ||||
|       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||
|         numberOfCs = ud.csCount; | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#if config.question !== undefined} | ||||
|   <div class="interactive border-interactive flex flex-col p-1 px-2 relative overflow-y-auto" style="max-height: 85vh"> | ||||
|     <div class="sticky top-0" style="z-index: 11"> | ||||
|        | ||||
|     <div class="flex justify-between sticky top-0 interactive"> | ||||
| 
 | ||||
|       <div class="flex justify-between sticky top-0 interactive"> | ||||
|       <span class="font-bold"> | ||||
|         <SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement} /> | ||||
|       </span> | ||||
|       <slot name="upper-right" /> | ||||
|     </div> | ||||
| 
 | ||||
|     {#if config.questionhint} | ||||
|       <div> | ||||
|         <SpecialTranslation | ||||
|           t={config.questionhint} | ||||
|           {tags} | ||||
|           {state} | ||||
|           {layer} | ||||
|           feature={selectedElement} | ||||
|         /> | ||||
|         <slot name="upper-right" /> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|       {#if config.questionhint} | ||||
|         <div> | ||||
|           <SpecialTranslation | ||||
|             t={config.questionhint} | ||||
|             {tags} | ||||
|             {state} | ||||
|             {layer} | ||||
|             feature={selectedElement} | ||||
|           /> | ||||
|         </div> | ||||
|       {/if} | ||||
|     </div> | ||||
| 
 | ||||
|     {#if config.mappings?.length >= 8} | ||||
|  | @ -307,7 +309,7 @@ | |||
| 
 | ||||
|     <LoginToggle {state}> | ||||
|       <Loading slot="loading" /> | ||||
|       <SubtleButton slot="not-logged-in" on:click={() => state.osmConnection.AttemptLogin()}> | ||||
|       <SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}> | ||||
|         <img slot="image" src="./assets/svg/login.svg" class="h-8 w-8" /> | ||||
|         <Tr t={Translations.t.general.loginToStart} slot="message" /> | ||||
|       </SubtleButton> | ||||
|  | @ -316,7 +318,8 @@ | |||
|           <Tr t={$feedback} /> | ||||
|         </div> | ||||
|       {/if} | ||||
|       <div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap sticky bottom-0 interactive" style="z-index: 11"> | ||||
|       <div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap sticky bottom-0 interactive" | ||||
|            style="z-index: 11"> | ||||
|         <!-- TagRenderingQuestion-buttons --> | ||||
|         <slot name="cancel" /> | ||||
|         <slot name="save-button" {selectedTags}> | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ | |||
|       opinion: opinion.data, | ||||
|       metadata: { nickname, is_affiliated: isAffiliated.data }, | ||||
|     } | ||||
|     if (state.featureSwitchIsTesting.data) { | ||||
|     if (state.featureSwitchIsTesting?.data ?? true) { | ||||
|       console.log("Testing - not actually saving review", review) | ||||
|       await Utils.waitFor(1000) | ||||
|     } else { | ||||
|  |  | |||
|  | @ -742,7 +742,7 @@ export default class SpecialVisualizations { | |||
|                     const reviews = FeatureReviews.construct( | ||||
|                         feature, | ||||
|                         tags, | ||||
|                         state.userRelatedState.mangroveIdentity, | ||||
|                         state.userRelatedState?.mangroveIdentity, | ||||
|                         { | ||||
|                             nameKey: nameKey, | ||||
|                             fallbackName, | ||||
|  | @ -774,7 +774,7 @@ export default class SpecialVisualizations { | |||
|                     const reviews = FeatureReviews.construct( | ||||
|                         feature, | ||||
|                         tags, | ||||
|                         state.userRelatedState.mangroveIdentity, | ||||
|                         state.userRelatedState?.mangroveIdentity, | ||||
|                         { | ||||
|                             nameKey: nameKey, | ||||
|                             fallbackName, | ||||
|  | @ -984,7 +984,7 @@ export default class SpecialVisualizations { | |||
|                             if (state.layout === undefined) { | ||||
|                                 return "<feature title>" | ||||
|                             } | ||||
|                             const layer = state.layout.getMatchingLayer(tags) | ||||
|                             const layer = state.layout?.getMatchingLayer(tags) | ||||
|                             const title = layer?.title?.GetRenderValue(tags) | ||||
|                             if (title === undefined) { | ||||
|                                 return undefined | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| <script lang="ts"> | ||||
| 
 | ||||
|   import type { HighlightedTagRendering } from "./EditLayerState"; | ||||
|   import EditLayerState, { LayerStateSender } from "./EditLayerState"; | ||||
|   import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"; | ||||
|   import Region from "./Region.svelte"; | ||||
|   import TabbedGroup from "../Base/TabbedGroup.svelte"; | ||||
|   import { Store } from "../../Logic/UIEventSource"; | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import { Utils } from "../../Utils"; | ||||
|   import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||
|  | @ -12,6 +12,11 @@ | |||
|   import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; | ||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   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"; | ||||
| 
 | ||||
|   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||
| 
 | ||||
|  | @ -50,13 +55,13 @@ | |||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   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 | ||||
|   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"]; | ||||
|  | @ -70,6 +75,7 @@ | |||
|     return missing; | ||||
|   }); | ||||
| 
 | ||||
|   let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem; | ||||
| </script> | ||||
| 
 | ||||
| {#if $currentlyMissing.length > 0} | ||||
|  | @ -80,83 +86,95 @@ | |||
|                       path={[required]} /> | ||||
|   {/each} | ||||
| {:else} | ||||
|   <div class="w-full flex justify-between my-2"> | ||||
|     <slot /> | ||||
|     <h3>Editing layer {$title}</h3> | ||||
|     {#if $hasErrors > 0} | ||||
|       <div class="alert">{$hasErrors} errors detected</div> | ||||
|     {:else} | ||||
|       <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> | ||||
|         Try it out | ||||
|         <ChevronRightIcon class="h-6 w-6 shrink-0" /> | ||||
|       </a> | ||||
|     {/if} | ||||
|   </div> | ||||
|   <div class="m4"> | ||||
|     <TabbedGroup> | ||||
|       <div slot="title0" class="flex">General properties | ||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> | ||||
|       </div> | ||||
|       <div class="flex flex-col" slot="content0"> | ||||
|         <Region {state} configs={perRegion["Basic"]} /> | ||||
|   <div class="h-screen flex flex-col"> | ||||
| 
 | ||||
|       </div> | ||||
| 
 | ||||
| 
 | ||||
|       <div slot="title1" class="flex">Information panel (questions and answers) | ||||
|         <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"> | ||||
|         <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="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="title5">Configuration file</div> | ||||
|       <div slot="content5"> | ||||
|         <div> | ||||
|           Below, you'll find the raw configuration file in `.json`-format. | ||||
|           This is mosSendertly for debugging purposes | ||||
|     <div class="w-full flex justify-between my-2"> | ||||
|       <slot /> | ||||
|       <h3>Editing layer {$title}</h3> | ||||
|       {#if $hasErrors > 0} | ||||
|         <div class="alert">{$hasErrors} errors detected</div> | ||||
|       {:else} | ||||
|         <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> | ||||
|           Try it out | ||||
|           <ChevronRightIcon class="h-6 w-6 shrink-0" /> | ||||
|         </a> | ||||
|       {/if} | ||||
|     </div> | ||||
|     <div class="m4 h-full overflow-y-auto"> | ||||
|       <TabbedGroup> | ||||
|         <div slot="title0" class="flex">General properties | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> | ||||
|         </div> | ||||
|         <div class="literal-code"> | ||||
|           {JSON.stringify($configuration, null, "  ")} | ||||
|         </div> | ||||
|         {#each $messages as message} | ||||
|           <li> | ||||
|             {message.level} | ||||
|             <span class="literal-code">{message.context.path.join(".")}</span> | ||||
|             {message.message} | ||||
|             <span class="literal-code"> | ||||
|           {message.context.operation.join(".")} | ||||
|           </span> | ||||
|           </li> | ||||
|         {/each} | ||||
|       </div> | ||||
|     </TabbedGroup> | ||||
|         <div class="flex flex-col" slot="content0"> | ||||
|           <Region {state} configs={perRegion["Basic"]} /> | ||||
| 
 | ||||
|         </div> | ||||
| 
 | ||||
| 
 | ||||
|         <div slot="title1" class="flex">Information panel (questions and answers) | ||||
|           <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /> | ||||
|         </div> | ||||
|         <div slot="content1"> | ||||
|           <QuestionPreview path={["title"]} {state} schema={perRegion["title"][0]}></QuestionPreview> | ||||
|           <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> | ||||
|           <Region configs={perRegion["editing"]} {state} title="Other editing elements" /> | ||||
|         </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="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="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 | ||||
|           </div> | ||||
|           <div class="literal-code"> | ||||
|             <FromHtml src={JSON.stringify($configuration, null, "  ").replaceAll("\n","</br>")} /> | ||||
|           </div> | ||||
|           {#each $messages as message} | ||||
|             <li> | ||||
|               {message.level} | ||||
|               <span class="literal-code">{message.context.path.join(".")}</span> | ||||
|               {message.message} | ||||
|               <span class="literal-code"> | ||||
|                 {message.context.operation.join(".")} | ||||
|               </span> | ||||
|             </li> | ||||
|           {/each} | ||||
| 
 | ||||
|           The testobject (which is used to render the questions in the 'information panel' item has the following tags: | ||||
|            | ||||
|           <AllTagsPanel tags={state.testTags}></AllTagsPanel> | ||||
|         </div> | ||||
|       </TabbedGroup> | ||||
|     </div> | ||||
|   </div> | ||||
|   {#if $highlightedItem !== undefined} | ||||
|     <FloatOver on:close={() => highlightedItem.setData(undefined)}> | ||||
|       <TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} /> | ||||
|     </FloatOver> | ||||
|   {/if} | ||||
| 
 | ||||
| {/if} | ||||
|  |  | |||
|  | @ -15,6 +15,9 @@ import { TagUtils } from "../../Logic/Tags/TagUtils" | |||
| import StudioServer from "./StudioServer" | ||||
| import { Utils } from "../../Utils" | ||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| import { Feature, Point } from "geojson" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| 
 | ||||
| /** | ||||
|  * Sends changes back to the server | ||||
|  | @ -36,11 +39,30 @@ export class LayerStateSender { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| export interface HighlightedTagRendering { | ||||
|     path: ReadonlyArray<string | number> | ||||
|     schema: ConfigMeta | ||||
| } | ||||
| 
 | ||||
| export default class EditLayerState { | ||||
|     public readonly schema: ConfigMeta[] | ||||
| 
 | ||||
|     public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource<boolean> } | ||||
|     public readonly featureSwitches: { | ||||
|         featureSwitchIsDebugging: UIEventSource<boolean> | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used to preview and interact with the questions | ||||
|      */ | ||||
|     public readonly testTags = new UIEventSource<OsmTags>({ id: "node/-12345" }) | ||||
|     public readonly exampleFeature: Feature<Point> = { | ||||
|         type: "Feature", | ||||
|         properties: this.testTags.data, | ||||
|         geometry: { | ||||
|             type: "Point", | ||||
|             coordinates: [3.21, 51.2], | ||||
|         }, | ||||
|     } | ||||
|     public readonly configuration: UIEventSource<Partial<LayerConfigJson>> = new UIEventSource< | ||||
|         Partial<LayerConfigJson> | ||||
|     >({}) | ||||
|  | @ -48,6 +70,19 @@ export default class EditLayerState { | |||
|     public readonly server: StudioServer | ||||
|     // Needed for the special visualisations
 | ||||
|     public readonly osmConnection: OsmConnection | ||||
|     public readonly imageUploadManager = { | ||||
|         getCountsFor() { | ||||
|             return 0 | ||||
|         }, | ||||
|     } | ||||
|     public readonly layout: { getMatchingLayer: (key: any) => LayerConfig } | ||||
| 
 | ||||
|     /** | ||||
|      * The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out | ||||
|      */ | ||||
|     public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource( | ||||
|         undefined | ||||
|     ) | ||||
|     private readonly _stores = new Map<string, UIEventSource<any>>() | ||||
| 
 | ||||
|     constructor(schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection) { | ||||
|  | @ -71,6 +106,8 @@ export default class EditLayerState { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.highlightedItem.addCallback((h) => console.log("Highlighted is now", h)) | ||||
| 
 | ||||
|         const prepare = new Pipe( | ||||
|             new PrepareLayer(state), | ||||
|             new ValidateLayer("dynamic", false, undefined, true) | ||||
|  | @ -101,6 +138,16 @@ export default class EditLayerState { | |||
|             prepare.convert(<LayerConfigJson>config, context) | ||||
|             return context.messages | ||||
|         }) | ||||
| 
 | ||||
|         this.layout = { | ||||
|             getMatchingLayer: (_) => { | ||||
|                 try { | ||||
|                     return new LayerConfig(<LayerConfigJson>this.configuration.data, "dynamic") | ||||
|                 } catch (e) { | ||||
|                     return undefined | ||||
|                 } | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined { | ||||
|  | @ -180,7 +227,6 @@ 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 || | ||||
|  | @ -202,12 +248,10 @@ export default class EditLayerState { | |||
|         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 if (entry[lastBreadcrumb] !== v) { | ||||
|             console.log("Assigning and pinging at", path) | ||||
|             entry[lastBreadcrumb] = v | ||||
|             this.configuration.ping() | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										91
									
								
								src/UI/Studio/QuestionPreview.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/UI/Studio/QuestionPreview.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,91 @@ | |||
| <script lang="ts"> | ||||
|   import type { ConfigMeta } from "./configMeta"; | ||||
|   import EditLayerState from "./EditLayerState"; | ||||
|   import * as questions from "../../assets/generated/layers/questions.json"; | ||||
|   import { ImmutableStore, Store } from "../../Logic/UIEventSource"; | ||||
|   import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"; | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import * as nmd from "nano-markdown"; | ||||
|   import type { | ||||
|     QuestionableTagRenderingConfigJson | ||||
|   } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"; | ||||
|   import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let path: ReadonlyArray<string | number>; | ||||
|   export let schema: ConfigMeta; | ||||
|   let value = state.getStoreFor(path); | ||||
| 
 | ||||
|   let perId: Record<string, TagRenderingConfigJson[]> = {}; | ||||
|   for (const tagRendering of questions.tagRenderings) { | ||||
|     if (tagRendering.labels) { | ||||
|       for (const label of tagRendering.labels) { | ||||
|         perId[label] = (perId[label] ?? []).concat(tagRendering); | ||||
|       } | ||||
|     } | ||||
|     perId[tagRendering.id] = [tagRendering]; | ||||
|   } | ||||
| 
 | ||||
|   let configJson: Store<QuestionableTagRenderingConfigJson[]> = value.map(x => { | ||||
|     if (typeof x === "string") { | ||||
|       return perId[x]; | ||||
|     } else { | ||||
|       return [x]; | ||||
|     } | ||||
|   }); | ||||
|   let configs: Store<TagRenderingConfig[]> = configJson.mapD(configs => configs.map(config => new TagRenderingConfig(config))); | ||||
|   let id: Store<string> = value.mapD(c => { | ||||
|     if (c.id) { | ||||
|       return c.id; | ||||
|     } | ||||
|     if (typeof c === "string") { | ||||
|       return c; | ||||
|     } | ||||
|     return undefined; | ||||
|   }); | ||||
| 
 | ||||
|   let tags = state.testTags; | ||||
| 
 | ||||
|   let messages = state.messagesFor(path); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex"> | ||||
| 
 | ||||
|   <div class="flex flex-col interactive border-interactive m-4 w-full"> | ||||
| 
 | ||||
|     {#if $id} | ||||
|       TagRendering {$id} | ||||
|     {/if} | ||||
|     <button on:click={() => state.highlightedItem.setData({path, schema})}> | ||||
|       {#if schema.hints.question} | ||||
|         {schema.hints.question} | ||||
|       {/if} | ||||
|     </button> | ||||
|     {#if schema.description} | ||||
|       <FromHtml src={nmd(schema.description)} /> | ||||
|     {/if} | ||||
|     {#each $messages as message} | ||||
|       <div class="alert"> | ||||
|         {message.message} | ||||
|       </div> | ||||
|     {/each} | ||||
| 
 | ||||
|     <slot class="self-end my-4"></slot> | ||||
| 
 | ||||
| 
 | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="flex flex-col w-full m-4"> | ||||
|     {#each $configs as config} | ||||
|       <TagRenderingEditable | ||||
|         selectedElement={state.exampleFeature} | ||||
|         config={config} editingEnabled={new ImmutableStore(true)} showQuestionIfUnknown={true} | ||||
|         {state} | ||||
|         {tags}></TagRenderingEditable> | ||||
|     {/each} | ||||
|   </div> | ||||
| 
 | ||||
| 
 | ||||
| </div> | ||||
|  | @ -5,12 +5,13 @@ | |||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||
|   import SchemaBasedField from "./SchemaBasedField.svelte"; | ||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini"; | ||||
|   import TagRenderingInput from "./TagRenderingInput.svelte"; | ||||
|   import QuestionPreview from "./QuestionPreview.svelte"; | ||||
|   import { Utils } from "../../Utils"; | ||||
| 
 | ||||
|   export let state: EditLayerState; | ||||
|   export let schema: ConfigMeta; | ||||
| 
 | ||||
|   | ||||
| 
 | ||||
|   let title = schema.path.at(-1); | ||||
|   let singular = title; | ||||
|   if (title?.endsWith("s")) { | ||||
|  | @ -23,7 +24,11 @@ | |||
|   export let path: (string | number)[] = []; | ||||
|   const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"; | ||||
| 
 | ||||
|    | ||||
|   if (isTagRenderingBlock) { | ||||
|     schema = { ...schema }; | ||||
|     schema.description = undefined; | ||||
|   } | ||||
| 
 | ||||
|   const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path) | ||||
|     .filter(part => part.path.length - 1 === schema.path.length); | ||||
|   /** | ||||
|  | @ -64,15 +69,43 @@ | |||
|   } | ||||
| 
 | ||||
|   function del(value) { | ||||
|     const index = values.data.indexOf(value) | ||||
|     console.log("Deleting",value, index) | ||||
|     const index = values.data.indexOf(value); | ||||
|     console.log("Deleting", value, index); | ||||
|     values.data.splice(index, 1); | ||||
|     const store = <UIEventSource<[]>>state.getStoreFor(path); | ||||
|     store.data.splice(index, 1) | ||||
|     values.ping(); | ||||
|     store.ping() | ||||
| 
 | ||||
|     const store = <UIEventSource<[]>>state.getStoreFor(path); | ||||
|     store.data.splice(index, 1); | ||||
|     store.setData(Utils.NoNull(store.data)); | ||||
|     state.configuration.ping(); | ||||
|   } | ||||
| 
 | ||||
|   function swap(indexA, indexB) { | ||||
|     const valueA = values.data[indexA]; | ||||
|     const valueB = values.data[indexB]; | ||||
| 
 | ||||
|     values.data[indexA] = valueB; | ||||
|     values.data[indexB] = valueA; | ||||
|     values.ping(); | ||||
| 
 | ||||
|     const store = <UIEventSource<[]>>state.getStoreFor(path); | ||||
|     const svalueA = store.data[indexA]; | ||||
|     const svalueB = store.data[indexB]; | ||||
|     store.data[indexA] = svalueB; | ||||
|     store.data[indexB] = svalueA; | ||||
|     store.ping(); | ||||
|     state.configuration.ping(); | ||||
|   } | ||||
| 
 | ||||
|   function moveTo(currentIndex, targetIndex) { | ||||
|     const direction = currentIndex > targetIndex ? -1 : +1; | ||||
|     do { | ||||
|       swap(currentIndex, currentIndex + direction); | ||||
|       currentIndex = currentIndex + direction; | ||||
|     } while (currentIndex !== targetIndex); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| <div class="pl-2"> | ||||
|   <h3>{schema.path.at(-1)}</h3> | ||||
|  | @ -97,7 +130,7 @@ | |||
|       </div> | ||||
|     {/each} | ||||
|   {:else} | ||||
|     {#each $values as value (value)} | ||||
|     {#each $values as value, i (value)} | ||||
| 
 | ||||
|       {#if !isTagRenderingBlock} | ||||
|         <div class="flex justify-between items-center"> | ||||
|  | @ -110,12 +143,31 @@ | |||
|       {/if} | ||||
|       <div class="border border-black"> | ||||
|         {#if isTagRenderingBlock} | ||||
|           <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)}}> | ||||
|           <QuestionPreview {state} path={[...path, value]} {schema}> | ||||
|             <button on:click={() => {del(i)}}> | ||||
|               <TrashIcon class="w-4 h-4" /> | ||||
|               Delete this question | ||||
|             </button> | ||||
|           </TagRenderingInput> | ||||
| 
 | ||||
|             {#if i > 0} | ||||
|               <button on:click={() => {moveTo(i, 0)}}> | ||||
|                 Move to front | ||||
|               </button> | ||||
| 
 | ||||
|               <button on:click={() => {swap(i, i-1)}}> | ||||
|                 Move up | ||||
|               </button> | ||||
|             {/if} | ||||
|             {#if i + 1 < $values.length} | ||||
|               <button on:click={() => {swap(i, i+1)}}> | ||||
|                 Move down | ||||
|               </button> | ||||
|               <button on:click={() => {moveTo(i, $values.length-1)}}> | ||||
|                 Move to back | ||||
|               </button> | ||||
|             {/if} | ||||
| 
 | ||||
|           </QuestionPreview> | ||||
|         {:else} | ||||
|           {#each subparts as subpart} | ||||
|             <SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart} /> | ||||
|  |  | |||
|  | @ -32,9 +32,30 @@ let allowQuestions: Store<boolean> = (state.configuration.mapD(config => config. | |||
| 
 | ||||
| 
 | ||||
| let mappingsBuiltin: MappingConfigJson[] = []; | ||||
| let perLabel: Record<string, MappingConfigJson> = {} | ||||
| for (const tr of questions.tagRenderings) { | ||||
|   let description = tr["description"] ?? tr["question"] ?? "No description available"; | ||||
|   description = description["en"] ?? description; | ||||
|   if(tr["labels"]){ | ||||
|     const labels: string[] = tr["labels"] | ||||
|     for (const label of labels) { | ||||
|       let labelMapping: MappingConfigJson = perLabel[label]  | ||||
|        | ||||
|       if(!labelMapping){ | ||||
|         labelMapping = { | ||||
|           if: "value="+label, | ||||
|           then: { | ||||
|             en: "Builtin collection <b>"+label+"</b>:" | ||||
|           } | ||||
|         } | ||||
|         perLabel[label] = labelMapping | ||||
|         mappingsBuiltin.push(labelMapping) | ||||
|       } | ||||
|       labelMapping.then.en = labelMapping.then.en + "<div>"+description+"</div>" | ||||
|     } | ||||
|   } | ||||
|    | ||||
|    | ||||
|   mappingsBuiltin.push({ | ||||
|     if: "value=" + tr["id"], | ||||
|     then: { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue