Studio: add previews of the questions, edit them in floatover

This commit is contained in:
Pieter Vander Vennet 2023-10-25 00:03:51 +02:00
parent 8bc555fbe0
commit ac6e38a256
12 changed files with 391 additions and 159 deletions

View file

@ -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?"

View file

@ -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={() => {}}>

View file

@ -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}

View file

@ -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)
}

View file

@ -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}>

View file

@ -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 {

View file

@ -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

View file

@ -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}

View file

@ -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()
}

View 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>

View file

@ -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} />

View file

@ -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: {