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", |       "id": "sugar_free", | ||||||
|  |       "labels": ["diets"], | ||||||
|       "question": { |       "question": { | ||||||
|         "en": "Does this shop have a sugar free offering?", |         "en": "Does this shop have a sugar free offering?", | ||||||
|         "de": "Verkauft das Geschäft zuckerfreie Produkte?" |         "de": "Verkauft das Geschäft zuckerfreie Produkte?" | ||||||
|  | @ -2441,6 +2442,7 @@ | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "id": "lactose_free", |       "id": "lactose_free", | ||||||
|  |       "labels": ["diets"], | ||||||
|       "question": { |       "question": { | ||||||
|         "en": "Does {title()} have a lactose-free offering?", |         "en": "Does {title()} have a lactose-free offering?", | ||||||
|         "de": "Verkauft {title()} laktosefreie Produkte?" |         "de": "Verkauft {title()} laktosefreie Produkte?" | ||||||
|  | @ -2478,6 +2480,7 @@ | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "id": "gluten_free", |       "id": "gluten_free", | ||||||
|  |       "labels": ["diets"], | ||||||
|       "question": { |       "question": { | ||||||
|         "en": "Does this shop have a gluten free offering?", |         "en": "Does this shop have a gluten free offering?", | ||||||
|         "de": "Verkauft das Geschäft glutenfreie Produkte?" |         "de": "Verkauft das Geschäft glutenfreie Produkte?" | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ | ||||||
| 
 | 
 | ||||||
| <div | <div | ||||||
|   class="absolute top-0 right-0 h-screen w-screen p-4 md:p-6" |   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")}} |   on:click={() => {dispatch("close")}} | ||||||
| > | > | ||||||
|   <div class="content normal-background" on:click|stopPropagation={() => {}}> |   <div class="content normal-background" on:click|stopPropagation={() => {}}> | ||||||
|  |  | ||||||
|  | @ -15,8 +15,8 @@ | ||||||
|    * If set, 'loading' will act as if we are already logged in. |    * If set, 'loading' will act as if we are already logged in. | ||||||
|    */ |    */ | ||||||
|   export let ignoreLoading: boolean = false |   export let ignoreLoading: boolean = false | ||||||
|   let loadingStatus = state.osmConnection.loadingStatus |   let loadingStatus = state?.osmConnection?.loadingStatus ?? new ImmutableStore("logged-in") | ||||||
|   let badge = state.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true) |   let badge = state?.featureSwitches?.featureSwitchUserbadge ?? new ImmutableStore(true) | ||||||
|   const t = Translations.t.general |   const t = Translations.t.general | ||||||
|   const offlineModes: Partial<Record<OsmServiceState, Translation>> = { |   const offlineModes: Partial<Record<OsmServiceState, Translation>> = { | ||||||
|     offline: t.loginFailedOfflineMode, |     offline: t.loginFailedOfflineMode, | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
|     unknown: t.loginFailedUnreachableMode, |     unknown: t.loginFailedUnreachableMode, | ||||||
|     readonly: t.loginFailedReadonlyMode, |     readonly: t.loginFailedReadonlyMode, | ||||||
|   } |   } | ||||||
|   const apiState = state.osmConnection.apiIsOnline |   const apiState = state?.osmConnection?.apiIsOnline ?? new ImmutableStore<OsmServiceState>("online") | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $badge} | {#if $badge} | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
|    */ |    */ | ||||||
| 
 | 
 | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import { Store } from "../../Logic/UIEventSource" |   import { ImmutableStore, Store } from "../../Logic/UIEventSource"; | ||||||
|   import type { OsmTags } from "../../Models/OsmFeature" |   import type { OsmTags } from "../../Models/OsmFeature" | ||||||
|   import LoginToggle from "../Base/LoginToggle.svelte" |   import LoginToggle from "../Base/LoginToggle.svelte" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|  | @ -28,14 +28,14 @@ | ||||||
|   export let labelText: string = undefined |   export let labelText: string = undefined | ||||||
|   const t = Translations.t.image |   const t = Translations.t.image | ||||||
| 
 | 
 | ||||||
|   let licenseStore = state.userRelatedState.imageLicense |   let licenseStore = state?.userRelatedState?.imageLicense ?? new ImmutableStore("CC0") | ||||||
| 
 | 
 | ||||||
|   function handleFiles(files: FileList) { |   function handleFiles(files: FileList) { | ||||||
|     for (let i = 0; i < files.length; i++) { |     for (let i = 0; i < files.length; i++) { | ||||||
|       const file = files.item(i) |       const file = files.item(i) | ||||||
|       console.log("Got file", file.name) |       console.log("Got file", file.name) | ||||||
|       try { |       try { | ||||||
|         state.imageUploadManager.uploadImageAndApply(file, tags) |         state.imageUploadManager?.uploadImageAndApply(file, tags) | ||||||
|       } catch (e) { |       } catch (e) { | ||||||
|         alert(e) |         alert(e) | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | @ -35,9 +35,9 @@ | ||||||
|   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); |   let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)); | ||||||
| 
 | 
 | ||||||
|   // Will be bound if a freeform is available |   // Will be bound if a freeform is available | ||||||
|   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) |   let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]); | ||||||
|   let selectedMapping: number = undefined |   let selectedMapping: number = undefined; | ||||||
|   let checkedMappings: boolean[] |   let checkedMappings: boolean[]; | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Prepares and fills the checkedMappings |    * Prepares and fills the checkedMappings | ||||||
|  | @ -58,40 +58,40 @@ | ||||||
|       (checkedMappings === undefined || |       (checkedMappings === undefined || | ||||||
|         checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) |         checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0)) | ||||||
|     ) { |     ) { | ||||||
|       const seenFreeforms = [] |       const seenFreeforms = []; | ||||||
|       TagUtils.FlattenMultiAnswer() |       TagUtils.FlattenMultiAnswer(); | ||||||
|       checkedMappings = [ |       checkedMappings = [ | ||||||
|         ...confg.mappings.map((mapping) => { |         ...confg.mappings.map((mapping) => { | ||||||
|           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs) |           const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs); | ||||||
|           if (matches && confg.freeform) { |           if (matches && confg.freeform) { | ||||||
|             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()) |             const newProps = TagUtils.changeAsProperties(mapping.if.asChange()); | ||||||
|             seenFreeforms.push(newProps[confg.freeform.key]) |             seenFreeforms.push(newProps[confg.freeform.key]); | ||||||
|           } |           } | ||||||
|           return matches |           return matches; | ||||||
|         }), |         }) | ||||||
|       ] |       ]; | ||||||
| 
 | 
 | ||||||
|       if (tgs !== undefined && confg.freeform) { |       if (tgs !== undefined && confg.freeform) { | ||||||
|         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [] |         const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []; | ||||||
|         for (const seenFreeform of seenFreeforms) { |         for (const seenFreeform of seenFreeforms) { | ||||||
|           if (!seenFreeform) { |           if (!seenFreeform) { | ||||||
|             continue |             continue; | ||||||
|           } |           } | ||||||
|           const index = unseenFreeformValues.indexOf(seenFreeform) |           const index = unseenFreeformValues.indexOf(seenFreeform); | ||||||
|           if (index < 0) { |           if (index < 0) { | ||||||
|             continue |             continue; | ||||||
|           } |           } | ||||||
|           unseenFreeformValues.splice(index, 1) |           unseenFreeformValues.splice(index, 1); | ||||||
|         } |         } | ||||||
|         // TODO this has _to much_ values |         // TODO this has _to much_ values | ||||||
|         freeformInput.setData(unseenFreeformValues.join(";")) |         freeformInput.setData(unseenFreeformValues.join(";")); | ||||||
|         checkedMappings.push(unseenFreeformValues.length > 0) |         checkedMappings.push(unseenFreeformValues.length > 0); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (confg.freeform?.key) { |     if (confg.freeform?.key) { | ||||||
|       if (!confg.multiAnswer) { |       if (!confg.multiAnswer) { | ||||||
|         // Somehow, setting multi-answer freeform values is broken if this is not set |         // 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 { |     } else { | ||||||
|       freeformInput.setData(undefined); |       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 |     // 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 |     // 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 mappings: Mapping[] = config?.mappings; | ||||||
|   let searchTerm: UIEventSource<string> = new UIEventSource(""); |   let searchTerm: UIEventSource<string> = new UIEventSource(""); | ||||||
|  | @ -166,39 +166,41 @@ | ||||||
|       .catch(console.error); |       .catch(console.error); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let featureSwitchIsTesting = state.featureSwitchIsTesting ?? new ImmutableStore(false); |   let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false); | ||||||
|   let featureSwitchIsDebugging = state.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); |   let featureSwitchIsDebugging = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false); | ||||||
|   let showTags = state.userRelatedState?.showTags ?? new ImmutableStore(undefined); |   let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined); | ||||||
|   let numberOfCs = state.osmConnection.userDetails.data.csCount; |   let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0; | ||||||
|   onDestroy( |   if (state) { | ||||||
|     state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { |     onDestroy( | ||||||
|       numberOfCs = ud.csCount; |       state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { | ||||||
|     }) |         numberOfCs = ud.csCount; | ||||||
|   ); |       }) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if config.question !== undefined} | {#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="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="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"> |       <span class="font-bold"> | ||||||
|         <SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement} /> |         <SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement} /> | ||||||
|       </span> |       </span> | ||||||
|       <slot name="upper-right" /> |         <slot name="upper-right" /> | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     {#if config.questionhint} |  | ||||||
|       <div> |  | ||||||
|         <SpecialTranslation |  | ||||||
|           t={config.questionhint} |  | ||||||
|           {tags} |  | ||||||
|           {state} |  | ||||||
|           {layer} |  | ||||||
|           feature={selectedElement} |  | ||||||
|         /> |  | ||||||
|       </div> |       </div> | ||||||
|     {/if} | 
 | ||||||
|  |       {#if config.questionhint} | ||||||
|  |         <div> | ||||||
|  |           <SpecialTranslation | ||||||
|  |             t={config.questionhint} | ||||||
|  |             {tags} | ||||||
|  |             {state} | ||||||
|  |             {layer} | ||||||
|  |             feature={selectedElement} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       {/if} | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     {#if config.mappings?.length >= 8} |     {#if config.mappings?.length >= 8} | ||||||
|  | @ -307,7 +309,7 @@ | ||||||
| 
 | 
 | ||||||
|     <LoginToggle {state}> |     <LoginToggle {state}> | ||||||
|       <Loading slot="loading" /> |       <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" /> |         <img slot="image" src="./assets/svg/login.svg" class="h-8 w-8" /> | ||||||
|         <Tr t={Translations.t.general.loginToStart} slot="message" /> |         <Tr t={Translations.t.general.loginToStart} slot="message" /> | ||||||
|       </SubtleButton> |       </SubtleButton> | ||||||
|  | @ -316,7 +318,8 @@ | ||||||
|           <Tr t={$feedback} /> |           <Tr t={$feedback} /> | ||||||
|         </div> |         </div> | ||||||
|       {/if} |       {/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 --> |         <!-- TagRenderingQuestion-buttons --> | ||||||
|         <slot name="cancel" /> |         <slot name="cancel" /> | ||||||
|         <slot name="save-button" {selectedTags}> |         <slot name="save-button" {selectedTags}> | ||||||
|  |  | ||||||
|  | @ -46,7 +46,7 @@ | ||||||
|       opinion: opinion.data, |       opinion: opinion.data, | ||||||
|       metadata: { nickname, is_affiliated: isAffiliated.data }, |       metadata: { nickname, is_affiliated: isAffiliated.data }, | ||||||
|     } |     } | ||||||
|     if (state.featureSwitchIsTesting.data) { |     if (state.featureSwitchIsTesting?.data ?? true) { | ||||||
|       console.log("Testing - not actually saving review", review) |       console.log("Testing - not actually saving review", review) | ||||||
|       await Utils.waitFor(1000) |       await Utils.waitFor(1000) | ||||||
|     } else { |     } else { | ||||||
|  |  | ||||||
|  | @ -742,7 +742,7 @@ export default class SpecialVisualizations { | ||||||
|                     const reviews = FeatureReviews.construct( |                     const reviews = FeatureReviews.construct( | ||||||
|                         feature, |                         feature, | ||||||
|                         tags, |                         tags, | ||||||
|                         state.userRelatedState.mangroveIdentity, |                         state.userRelatedState?.mangroveIdentity, | ||||||
|                         { |                         { | ||||||
|                             nameKey: nameKey, |                             nameKey: nameKey, | ||||||
|                             fallbackName, |                             fallbackName, | ||||||
|  | @ -774,7 +774,7 @@ export default class SpecialVisualizations { | ||||||
|                     const reviews = FeatureReviews.construct( |                     const reviews = FeatureReviews.construct( | ||||||
|                         feature, |                         feature, | ||||||
|                         tags, |                         tags, | ||||||
|                         state.userRelatedState.mangroveIdentity, |                         state.userRelatedState?.mangroveIdentity, | ||||||
|                         { |                         { | ||||||
|                             nameKey: nameKey, |                             nameKey: nameKey, | ||||||
|                             fallbackName, |                             fallbackName, | ||||||
|  | @ -984,7 +984,7 @@ export default class SpecialVisualizations { | ||||||
|                             if (state.layout === undefined) { |                             if (state.layout === undefined) { | ||||||
|                                 return "<feature title>" |                                 return "<feature title>" | ||||||
|                             } |                             } | ||||||
|                             const layer = state.layout.getMatchingLayer(tags) |                             const layer = state.layout?.getMatchingLayer(tags) | ||||||
|                             const title = layer?.title?.GetRenderValue(tags) |                             const title = layer?.title?.GetRenderValue(tags) | ||||||
|                             if (title === undefined) { |                             if (title === undefined) { | ||||||
|                                 return undefined |                                 return undefined | ||||||
|  |  | ||||||
|  | @ -1,10 +1,10 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 
 |   import type { HighlightedTagRendering } from "./EditLayerState"; | ||||||
|   import EditLayerState, { LayerStateSender } from "./EditLayerState"; |   import EditLayerState, { LayerStateSender } from "./EditLayerState"; | ||||||
|   import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"; |   import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"; | ||||||
|   import Region from "./Region.svelte"; |   import Region from "./Region.svelte"; | ||||||
|   import TabbedGroup from "../Base/TabbedGroup.svelte"; |   import TabbedGroup from "../Base/TabbedGroup.svelte"; | ||||||
|   import { Store } from "../../Logic/UIEventSource"; |   import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||||
|   import type { ConfigMeta } from "./configMeta"; |   import type { ConfigMeta } from "./configMeta"; | ||||||
|   import { Utils } from "../../Utils"; |   import { Utils } from "../../Utils"; | ||||||
|   import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; |   import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"; | ||||||
|  | @ -12,6 +12,11 @@ | ||||||
|   import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; |   import ErrorIndicatorForRegion from "./ErrorIndicatorForRegion.svelte"; | ||||||
|   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; |   import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||||
|   import SchemaBasedInput from "./SchemaBasedInput.svelte"; |   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; |   const layerSchema: ConfigMeta[] = <any>layerSchemaRaw; | ||||||
| 
 | 
 | ||||||
|  | @ -50,13 +55,13 @@ | ||||||
| 
 | 
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function configForRequiredField(id: string): ConfigMeta{ |   function configForRequiredField(id: string): ConfigMeta { | ||||||
|     let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id) |     let config = layerSchema.find(config => config.path.length === 1 && config.path[0] === id); | ||||||
|     config = Utils.Clone(config) |     config = Utils.Clone(config); | ||||||
|     config.required = true |     config.required = true; | ||||||
|     console.log(">>>", config) |     console.log(">>>", config); | ||||||
|     config.hints.ifunset = undefined |     config.hints.ifunset = undefined; | ||||||
|     return  config |     return config; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   let requiredFields = ["id", "name", "description"]; |   let requiredFields = ["id", "name", "description"]; | ||||||
|  | @ -70,6 +75,7 @@ | ||||||
|     return missing; |     return missing; | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   let highlightedItem: UIEventSource<HighlightedTagRendering> = state.highlightedItem; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $currentlyMissing.length > 0} | {#if $currentlyMissing.length > 0} | ||||||
|  | @ -80,83 +86,95 @@ | ||||||
|                       path={[required]} /> |                       path={[required]} /> | ||||||
|   {/each} |   {/each} | ||||||
| {:else} | {:else} | ||||||
|   <div class="w-full flex justify-between my-2"> |   <div class="h-screen flex flex-col"> | ||||||
|     <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> |     <div class="w-full flex justify-between my-2"> | ||||||
| 
 |       <slot /> | ||||||
| 
 |       <h3>Editing layer {$title}</h3> | ||||||
|       <div slot="title1" class="flex">Information panel (questions and answers) |       {#if $hasErrors > 0} | ||||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("title","tagrenderings","editing")} {state} /> |         <div class="alert">{$hasErrors} errors detected</div> | ||||||
|       </div> |       {:else} | ||||||
|       <div slot="content1"> |         <a class="primary button" href={baseUrl+state.server.layerUrl(title.data)} target="_blank" rel="noopener"> | ||||||
|         <Region configs={perRegion["title"]} {state} title="Popup title" /> |           Try it out | ||||||
|         <Region configs={perRegion["tagrenderings"]} {state} title="Popup contents" /> |           <ChevronRightIcon class="h-6 w-6 shrink-0" /> | ||||||
|         <Region configs={perRegion["editing"]} {state} title="Other editing elements" /> |         </a> | ||||||
|       </div> |       {/if} | ||||||
| 
 |     </div> | ||||||
|       <div slot="title2"> |     <div class="m4 h-full overflow-y-auto"> | ||||||
|         <ErrorIndicatorForRegion firstPaths={firstPathsFor("presets")} {state} /> |       <TabbedGroup> | ||||||
|         Creating a new point |         <div slot="title0" class="flex">General properties | ||||||
|       </div> |           <ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} /> | ||||||
| 
 |  | ||||||
|       <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> |         </div> | ||||||
|         <div class="literal-code"> |         <div class="flex flex-col" slot="content0"> | ||||||
|           {JSON.stringify($configuration, null, "  ")} |           <Region {state} configs={perRegion["Basic"]} /> | ||||||
|         </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> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         <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> |   </div> | ||||||
|  |   {#if $highlightedItem !== undefined} | ||||||
|  |     <FloatOver on:close={() => highlightedItem.setData(undefined)}> | ||||||
|  |       <TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} /> | ||||||
|  |     </FloatOver> | ||||||
|  |   {/if} | ||||||
|  | 
 | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | @ -15,6 +15,9 @@ import { TagUtils } from "../../Logic/Tags/TagUtils" | ||||||
| import StudioServer from "./StudioServer" | import StudioServer from "./StudioServer" | ||||||
| import { Utils } from "../../Utils" | import { Utils } from "../../Utils" | ||||||
| import { OsmConnection } from "../../Logic/Osm/OsmConnection" | 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 |  * 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 { | export default class EditLayerState { | ||||||
|     public readonly schema: ConfigMeta[] |     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< |     public readonly configuration: UIEventSource<Partial<LayerConfigJson>> = new UIEventSource< | ||||||
|         Partial<LayerConfigJson> |         Partial<LayerConfigJson> | ||||||
|     >({}) |     >({}) | ||||||
|  | @ -48,6 +70,19 @@ export default class EditLayerState { | ||||||
|     public readonly server: StudioServer |     public readonly server: StudioServer | ||||||
|     // Needed for the special visualisations
 |     // Needed for the special visualisations
 | ||||||
|     public readonly osmConnection: OsmConnection |     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>>() |     private readonly _stores = new Map<string, UIEventSource<any>>() | ||||||
| 
 | 
 | ||||||
|     constructor(schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection) { |     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( |         const prepare = new Pipe( | ||||||
|             new PrepareLayer(state), |             new PrepareLayer(state), | ||||||
|             new ValidateLayer("dynamic", false, undefined, true) |             new ValidateLayer("dynamic", false, undefined, true) | ||||||
|  | @ -101,6 +138,16 @@ export default class EditLayerState { | ||||||
|             prepare.convert(<LayerConfigJson>config, context) |             prepare.convert(<LayerConfigJson>config, context) | ||||||
|             return context.messages |             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 { |     public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined { | ||||||
|  | @ -180,7 +227,6 @@ export default class EditLayerState { | ||||||
| 
 | 
 | ||||||
|     public setValueAt(path: ReadonlyArray<string | number>, v: any) { |     public setValueAt(path: ReadonlyArray<string | number>, v: any) { | ||||||
|         let entry = this.configuration.data |         let entry = this.configuration.data | ||||||
|         console.log("Setting value at", path, v) |  | ||||||
|         const isUndefined = |         const isUndefined = | ||||||
|             v === undefined || |             v === undefined || | ||||||
|             v === null || |             v === null || | ||||||
|  | @ -202,12 +248,10 @@ export default class EditLayerState { | ||||||
|         const lastBreadcrumb = path.at(-1) |         const lastBreadcrumb = path.at(-1) | ||||||
|         if (isUndefined) { |         if (isUndefined) { | ||||||
|             if (entry && entry[lastBreadcrumb]) { |             if (entry && entry[lastBreadcrumb]) { | ||||||
|                 console.log("Deleting", lastBreadcrumb, "of", path.join(".")) |  | ||||||
|                 delete entry[lastBreadcrumb] |                 delete entry[lastBreadcrumb] | ||||||
|                 this.configuration.ping() |                 this.configuration.ping() | ||||||
|             } |             } | ||||||
|         } else if (entry[lastBreadcrumb] !== v) { |         } else if (entry[lastBreadcrumb] !== v) { | ||||||
|             console.log("Assigning and pinging at", path) |  | ||||||
|             entry[lastBreadcrumb] = v |             entry[lastBreadcrumb] = v | ||||||
|             this.configuration.ping() |             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 SchemaBasedInput from "./SchemaBasedInput.svelte"; | ||||||
|   import SchemaBasedField from "./SchemaBasedField.svelte"; |   import SchemaBasedField from "./SchemaBasedField.svelte"; | ||||||
|   import { TrashIcon } from "@babeard/svelte-heroicons/mini"; |   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 state: EditLayerState; | ||||||
|   export let schema: ConfigMeta; |   export let schema: ConfigMeta; | ||||||
| 
 | 
 | ||||||
|   | 
 | ||||||
|   let title = schema.path.at(-1); |   let title = schema.path.at(-1); | ||||||
|   let singular = title; |   let singular = title; | ||||||
|   if (title?.endsWith("s")) { |   if (title?.endsWith("s")) { | ||||||
|  | @ -23,7 +24,11 @@ | ||||||
|   export let path: (string | number)[] = []; |   export let path: (string | number)[] = []; | ||||||
|   const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"; |   const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"; | ||||||
| 
 | 
 | ||||||
|    |   if (isTagRenderingBlock) { | ||||||
|  |     schema = { ...schema }; | ||||||
|  |     schema.description = undefined; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path) |   const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path) | ||||||
|     .filter(part => part.path.length - 1 === schema.path.length); |     .filter(part => part.path.length - 1 === schema.path.length); | ||||||
|   /** |   /** | ||||||
|  | @ -64,15 +69,43 @@ | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function del(value) { |   function del(value) { | ||||||
|     const index = values.data.indexOf(value) |     const index = values.data.indexOf(value); | ||||||
|     console.log("Deleting",value, index) |     console.log("Deleting", value, index); | ||||||
|     values.data.splice(index, 1); |     values.data.splice(index, 1); | ||||||
|     const store = <UIEventSource<[]>>state.getStoreFor(path); |  | ||||||
|     store.data.splice(index, 1) |  | ||||||
|     values.ping(); |     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> | </script> | ||||||
| <div class="pl-2"> | <div class="pl-2"> | ||||||
|   <h3>{schema.path.at(-1)}</h3> |   <h3>{schema.path.at(-1)}</h3> | ||||||
|  | @ -97,7 +130,7 @@ | ||||||
|       </div> |       </div> | ||||||
|     {/each} |     {/each} | ||||||
|   {:else} |   {:else} | ||||||
|     {#each $values as value (value)} |     {#each $values as value, i (value)} | ||||||
| 
 | 
 | ||||||
|       {#if !isTagRenderingBlock} |       {#if !isTagRenderingBlock} | ||||||
|         <div class="flex justify-between items-center"> |         <div class="flex justify-between items-center"> | ||||||
|  | @ -110,12 +143,31 @@ | ||||||
|       {/if} |       {/if} | ||||||
|       <div class="border border-black"> |       <div class="border border-black"> | ||||||
|         {#if isTagRenderingBlock} |         {#if isTagRenderingBlock} | ||||||
|           <TagRenderingInput path={[...path, (value)]} {state} {schema} > |           <QuestionPreview {state} path={[...path, value]} {schema}> | ||||||
|             <button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit" |             <button on:click={() => {del(i)}}> | ||||||
|                     on:click={() => {del(value)}}> |  | ||||||
|               <TrashIcon class="w-4 h-4" /> |               <TrashIcon class="w-4 h-4" /> | ||||||
|  |               Delete this question | ||||||
|             </button> |             </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} |         {:else} | ||||||
|           {#each subparts as subpart} |           {#each subparts as subpart} | ||||||
|             <SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={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 mappingsBuiltin: MappingConfigJson[] = []; | ||||||
|  | let perLabel: Record<string, MappingConfigJson> = {} | ||||||
| for (const tr of questions.tagRenderings) { | for (const tr of questions.tagRenderings) { | ||||||
|   let description = tr["description"] ?? tr["question"] ?? "No description available"; |   let description = tr["description"] ?? tr["question"] ?? "No description available"; | ||||||
|   description = description["en"] ?? description; |   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({ |   mappingsBuiltin.push({ | ||||||
|     if: "value=" + tr["id"], |     if: "value=" + tr["id"], | ||||||
|     then: { |     then: { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue