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…
Reference in a new issue