diff --git a/assets/layers/questions/questions.json b/assets/layers/questions/questions.json index d31abf84cb..fd43930ad9 100644 --- a/assets/layers/questions/questions.json +++ b/assets/layers/questions/questions.json @@ -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?" diff --git a/src/UI/Base/FloatOver.svelte b/src/UI/Base/FloatOver.svelte index b86baf78d3..eb94123453 100644 --- a/src/UI/Base/FloatOver.svelte +++ b/src/UI/Base/FloatOver.svelte @@ -10,7 +10,7 @@
{dispatch("close")}} >
{}}> diff --git a/src/UI/Base/LoginToggle.svelte b/src/UI/Base/LoginToggle.svelte index 24d6b0e4a8..e64ba57b8c 100644 --- a/src/UI/Base/LoginToggle.svelte +++ b/src/UI/Base/LoginToggle.svelte @@ -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> = { 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("online") {#if $badge} diff --git a/src/UI/Image/UploadImage.svelte b/src/UI/Image/UploadImage.svelte index cac0f38ad9..929bc71ebf 100644 --- a/src/UI/Image/UploadImage.svelte +++ b/src/UI/Image/UploadImage.svelte @@ -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) } diff --git a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte index 094cbf4df7..8387eecb40 100644 --- a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -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(tags?.[config.freeform?.key]) - let selectedMapping: number = undefined - let checkedMappings: boolean[] + let freeformInput = new UIEventSource(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 = 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; + }) + ); + } {#if config.question !== undefined}
- -
+ +
- -
- - {#if config.questionhint} -
- +
- {/if} + + {#if config.questionhint} +
+ +
+ {/if}
{#if config.mappings?.length >= 8} @@ -307,7 +309,7 @@ - state.osmConnection.AttemptLogin()}> + state?.osmConnection?.AttemptLogin()}> @@ -316,7 +318,8 @@
{/if} -
+
diff --git a/src/UI/Reviews/ReviewForm.svelte b/src/UI/Reviews/ReviewForm.svelte index f048dbc74c..ecb5afa0ac 100644 --- a/src/UI/Reviews/ReviewForm.svelte +++ b/src/UI/Reviews/ReviewForm.svelte @@ -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 { diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 98df80f706..9176859adc 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -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 "" } - const layer = state.layout.getMatchingLayer(tags) + const layer = state.layout?.getMatchingLayer(tags) const title = layer?.title?.GetRenderValue(tags) if (title === undefined) { return undefined diff --git a/src/UI/Studio/EditLayer.svelte b/src/UI/Studio/EditLayer.svelte index 5283ed9dd7..93160b730c 100644 --- a/src/UI/Studio/EditLayer.svelte +++ b/src/UI/Studio/EditLayer.svelte @@ -1,10 +1,10 @@ {#if $currentlyMissing.length > 0} @@ -80,83 +86,95 @@ path={[required]} /> {/each} {:else} -
- -

Editing layer {$title}

- {#if $hasErrors > 0} -
{$hasErrors} errors detected
- {:else} - - Try it out - - - {/if} -
-
- -
General properties - -
-
- +
-
- - -
Information panel (questions and answers) - -
-
- - - -
- -
- - Creating a new point -
- -
- -
- -
Rendering on the map - -
-
- - -
- -
Advanced functionality - -
-
- - -
-
Configuration file
-
-
- Below, you'll find the raw configuration file in `.json`-format. - This is mosSendertly for debugging purposes +
+ +

Editing layer {$title}

+ {#if $hasErrors > 0} +
{$hasErrors} errors detected
+ {:else} + + Try it out + + + {/if} +
+
+ +
General properties +
-
- {JSON.stringify($configuration, null, " ")} -
- {#each $messages as message} -
  • - {message.level} - {message.context.path.join(".")} - {message.message} - - {message.context.operation.join(".")} - -
  • - {/each} -
    - +
    + +
    + + +
    Information panel (questions and answers) + +
    +
    + + + +
    + +
    + + Creating a new point +
    + +
    + +
    + +
    Rendering on the map + +
    +
    + + +
    + +
    Advanced functionality + +
    +
    + + +
    +
    Configuration file
    +
    +
    + Below, you'll find the raw configuration file in `.json`-format. + This is mostly for debugging purposes +
    +
    + ")} /> +
    + {#each $messages as message} +
  • + {message.level} + {message.context.path.join(".")} + {message.message} + + {message.context.operation.join(".")} + +
  • + {/each} + + The testobject (which is used to render the questions in the 'information panel' item has the following tags: + + +
    + +
    + {#if $highlightedItem !== undefined} + highlightedItem.setData(undefined)}> + + + {/if} + {/if} diff --git a/src/UI/Studio/EditLayerState.ts b/src/UI/Studio/EditLayerState.ts index 93709e0044..14df103b64 100644 --- a/src/UI/Studio/EditLayerState.ts +++ b/src/UI/Studio/EditLayerState.ts @@ -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 + schema: ConfigMeta +} + export default class EditLayerState { public readonly schema: ConfigMeta[] - public readonly featureSwitches: { featureSwitchIsDebugging: UIEventSource } + public readonly featureSwitches: { + featureSwitchIsDebugging: UIEventSource + } + /** + * Used to preview and interact with the questions + */ + public readonly testTags = new UIEventSource({ id: "node/-12345" }) + public readonly exampleFeature: Feature = { + type: "Feature", + properties: this.testTags.data, + geometry: { + type: "Point", + coordinates: [3.21, 51.2], + }, + } public readonly configuration: UIEventSource> = new UIEventSource< Partial >({}) @@ -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 = new UIEventSource( + undefined + ) private readonly _stores = new Map>() 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(config, context) return context.messages }) + + this.layout = { + getMatchingLayer: (_) => { + try { + return new LayerConfig(this.configuration.data, "dynamic") + } catch (e) { + return undefined + } + }, + } } public getCurrentValueFor(path: ReadonlyArray): any | undefined { @@ -180,7 +227,6 @@ export default class EditLayerState { public setValueAt(path: ReadonlyArray, 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() } diff --git a/src/UI/Studio/QuestionPreview.svelte b/src/UI/Studio/QuestionPreview.svelte new file mode 100644 index 0000000000..71d405604e --- /dev/null +++ b/src/UI/Studio/QuestionPreview.svelte @@ -0,0 +1,91 @@ + + +
    + +
    + + {#if $id} + TagRendering {$id} + {/if} + + {#if schema.description} + + {/if} + {#each $messages as message} +
    + {message.message} +
    + {/each} + + + + +
    + +
    + {#each $configs as config} + + {/each} +
    + + +
    diff --git a/src/UI/Studio/SchemaBasedArray.svelte b/src/UI/Studio/SchemaBasedArray.svelte index e53ea6425d..3841b2ea85 100644 --- a/src/UI/Studio/SchemaBasedArray.svelte +++ b/src/UI/Studio/SchemaBasedArray.svelte @@ -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 = >state.getStoreFor(path); - store.data.splice(index, 1) values.ping(); - store.ping() + + const store = >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 = >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); + } + +

    {schema.path.at(-1)}

    @@ -97,7 +130,7 @@
    {/each} {:else} - {#each $values as value (value)} + {#each $values as value, i (value)} {#if !isTagRenderingBlock}
    @@ -110,12 +143,31 @@ {/if}
    {#if isTagRenderingBlock} - - - + + {#if i > 0} + + + + {/if} + {#if i + 1 < $values.length} + + + {/if} + + {:else} {#each subparts as subpart} diff --git a/src/UI/Studio/TagRenderingInput.svelte b/src/UI/Studio/TagRenderingInput.svelte index 3a5dabc72b..70aeeabe6c 100644 --- a/src/UI/Studio/TagRenderingInput.svelte +++ b/src/UI/Studio/TagRenderingInput.svelte @@ -32,9 +32,30 @@ let allowQuestions: Store = (state.configuration.mapD(config => config. let mappingsBuiltin: MappingConfigJson[] = []; +let perLabel: Record = {} 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 "+label+":" + } + } + perLabel[label] = labelMapping + mappingsBuiltin.push(labelMapping) + } + labelMapping.then.en = labelMapping.then.en + "
    "+description+"
    " + } + } + + mappingsBuiltin.push({ if: "value=" + tr["id"], then: {