Studio: WIP
This commit is contained in:
parent
6ff2c629f0
commit
cb538c2303
33 changed files with 608 additions and 2071 deletions
|
@ -393,6 +393,7 @@ export interface LayerConfigJson {
|
|||
* This is mainly create questions for a 'left' and a 'right' side of the road.
|
||||
* These will be grouped and questions will be asked together
|
||||
*
|
||||
* type: tagrendering[]
|
||||
* group: tagrenderings
|
||||
*
|
||||
*/
|
||||
|
|
|
@ -10,7 +10,7 @@ export interface TagRenderingConfigJson {
|
|||
* question: What text should be rendered?
|
||||
*
|
||||
* This piece of text will be shown in the infobox.
|
||||
* Note that "{key}"-parts are substituted by the corresponding values of the element.
|
||||
* Note that "&LBRACEkey&RBRACE"-parts are substituted by the corresponding values of the element.
|
||||
*
|
||||
* This text will be shown if:
|
||||
* - there is no mapping which matches (or there are no matches)
|
||||
|
|
|
@ -233,6 +233,10 @@ export default class TagRenderingConfig {
|
|||
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (txt.indexOf("{translated(" + this.freeform.key + ")") >= 0) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
this.freeform.type === "opening_hours" &&
|
||||
txt.indexOf("{opening_hours_table(") >= 0
|
||||
|
|
|
@ -61,6 +61,8 @@ export default class InputHelpers {
|
|||
translation: (value) => new SvelteUIElement(TranslationInput, { value }),
|
||||
} as const
|
||||
|
||||
public static hideInputField : string[] = ["translation"]
|
||||
|
||||
/**
|
||||
* Constructs a mapProperties-object for the given properties.
|
||||
* Assumes that the first helper-args contains the desired zoom-level
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import InputHelper from "../../InputElement/InputHelper.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import { Unit } from "../../../Models/Unit"
|
||||
import InputHelpers from "../../InputElement/InputHelpers";
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
export let config: TagRenderingConfig
|
||||
|
@ -52,7 +53,7 @@
|
|||
{value}
|
||||
/>
|
||||
</Inline>
|
||||
{:else}
|
||||
{:else if InputHelpers.hideInputField.indexOf(config.freeform.type) < 0}
|
||||
<ValidatedInput
|
||||
{feedback}
|
||||
{getCountry}
|
||||
|
|
|
@ -1344,6 +1344,36 @@ export default class SpecialVisualizations {
|
|||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "translated",
|
||||
docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",
|
||||
args: [
|
||||
{
|
||||
name: "key",
|
||||
doc: "The attribute to interpret as json",
|
||||
defaultValue: "value",
|
||||
},
|
||||
],
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new VariableUiElement(
|
||||
tagSource.map((tags) => {
|
||||
const v = tags[argument[0] ?? "value"]
|
||||
try {
|
||||
const tr = JSON.parse(v)
|
||||
return new Translation(tr).SetClass("font-bold")
|
||||
} catch (e) {
|
||||
return v
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import layerSchemaRaw from "../../../assets/layerconfigmeta.json"
|
||||
import layerSchemaRaw from "../../assets/layerconfigmeta.json"
|
||||
import Region from "./Region.svelte";
|
||||
import TabbedGroup from "../Base/TabbedGroup.svelte";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
@ -16,7 +16,7 @@
|
|||
/**
|
||||
* Blacklist for the general area tab
|
||||
*/
|
||||
const regionBlacklist = ["hidden",undefined,"infobox", "tagrenderings","maprendering", "editing"]
|
||||
const regionBlacklist = ["hidden",undefined,"infobox", "tagrenderings","maprendering", "editing", "title"]
|
||||
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group))
|
||||
|
||||
const perRegion: Record<string, ConfigMeta[]> = {}
|
||||
|
@ -40,20 +40,26 @@
|
|||
<TabbedGroup tab={new UIEventSource(1)}>
|
||||
<div slot="title0">General properties</div>
|
||||
<div class="flex flex-col" slot="content0">
|
||||
<!--
|
||||
{#each baselayerRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region}/>
|
||||
{/each}
|
||||
|
||||
{#each leftoverRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region}/>
|
||||
{/each}
|
||||
{/each}-->
|
||||
</div>
|
||||
|
||||
<div slot="title1">Information panel (questions and answers)</div>
|
||||
<div slot="content1">
|
||||
<Region {state} configs={perRegion["title"]} title="Title"/>
|
||||
<Region {state} configs={perRegion["tagrenderings"]} title="Infobox"/>
|
||||
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>
|
||||
<Region {state} configs={perRegion["tagrenderings"]} title="Popup contents">
|
||||
<div slot="description">
|
||||
The bulk of the popup content
|
||||
</div>
|
||||
</Region>
|
||||
<!--
|
||||
<Region {state} configs={perRegion["title"]} title="Popup title"/>
|
||||
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>-->
|
||||
</div>
|
||||
|
||||
<div slot="title2">Rendering on the map</div>
|
||||
|
|
|
@ -22,7 +22,7 @@ export default class EditLayerState {
|
|||
featureSwitchIsDebugging: new UIEventSource<boolean>(true),
|
||||
}
|
||||
this.configuration.addCallback((config) => {
|
||||
console.log("Current config is", Utils.Clone(config))
|
||||
// console.log("Current config is", Utils.Clone(config))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ export let title: string
|
|||
{#if title}
|
||||
<h3>{title}</h3>
|
||||
<div class="pl-2 border border-black flex flex-col gap-y-1">
|
||||
|
||||
<slot name="description"/>
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config}/>
|
||||
{/each}
|
||||
|
|
|
@ -1,94 +1,109 @@
|
|||
<script lang="ts">
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import {TrashIcon} from "@babeard/svelte-heroicons/mini";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
|
||||
import TagRenderingInput from "./TagRenderingInput.svelte";
|
||||
|
||||
export let state: EditLayerState
|
||||
export let schema: ConfigMeta
|
||||
export let state: EditLayerState;
|
||||
export let schema: ConfigMeta;
|
||||
|
||||
let title = schema.path.at(-1)
|
||||
let singular = title
|
||||
if (title.endsWith("s")) {
|
||||
singular = title.slice(0, title.length - 1)
|
||||
let title = schema.path.at(-1);
|
||||
let singular = title;
|
||||
if (title.endsWith("s")) {
|
||||
singular = title.slice(0, title.length - 1);
|
||||
}
|
||||
let article = "a";
|
||||
if (singular.match(/^[aeoui]/)) {
|
||||
article = "an";
|
||||
}
|
||||
export let path: (string | number)[] = [];
|
||||
|
||||
const subparts = state.getSchemaStartingWith(schema.path);
|
||||
|
||||
/**
|
||||
* Store the _indices_
|
||||
*/
|
||||
export let values: UIEventSource<number[]> = new UIEventSource<number[]>([]);
|
||||
|
||||
const currentValue = <[]>state.getCurrentValueFor(path);
|
||||
if (currentValue) {
|
||||
if (!Array.isArray(currentValue)) {
|
||||
console.error("SchemaBaseArray for path", path, "expected an array as initial value, but got a", typeof currentValue, currentValue);
|
||||
} else {
|
||||
values.setData(currentValue.map((_, i) => i));
|
||||
}
|
||||
let article = "a"
|
||||
if (singular.match(/^[aeoui]/)) {
|
||||
article = "an"
|
||||
}
|
||||
let createdItems = values.data.length;
|
||||
|
||||
|
||||
function createItem() {
|
||||
values.data.push(createdItems);
|
||||
createdItems++;
|
||||
values.ping();
|
||||
}
|
||||
|
||||
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path, i];
|
||||
const toAdd = [...subpartPath];
|
||||
for (const part of path) {
|
||||
if (toAdd[0] === part) {
|
||||
toAdd.splice(0, 1);
|
||||
}
|
||||
}
|
||||
export let path: (string | number)[] = []
|
||||
newPath.push(...toAdd);
|
||||
return newPath;
|
||||
}
|
||||
|
||||
const subparts = state.getSchemaStartingWith(schema.path)
|
||||
|
||||
let createdItems = 0
|
||||
/**
|
||||
* Keeps track of the items.
|
||||
* We keep a single string (stringified 'createdItems') to make sure the order is correct
|
||||
*/
|
||||
export let values: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||
|
||||
const currentValue = <[]>state.getCurrentValueFor(path)
|
||||
if (currentValue) {
|
||||
if (!Array.isArray(currentValue)) {
|
||||
console.error("SchemaBaseArray for path", path, "expected an array as initial value, but got a", typeof currentValue, currentValue)
|
||||
} else {
|
||||
values.setData(currentValue.map((_, i) => i))
|
||||
}
|
||||
}
|
||||
|
||||
function createItem() {
|
||||
values.data.push(createdItems)
|
||||
createdItems++
|
||||
values.ping()
|
||||
}
|
||||
|
||||
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
|
||||
const newPath = [...path, i]
|
||||
const toAdd = [...subpartPath]
|
||||
for (const part of path) {
|
||||
if (toAdd[0] === part) {
|
||||
toAdd.splice(0, 1)
|
||||
}
|
||||
}
|
||||
newPath.push(...toAdd)
|
||||
return newPath
|
||||
}
|
||||
function del(value) {
|
||||
values.data.splice(values.data.indexOf(value));
|
||||
values.ping();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="pl-2">
|
||||
<h3>{schema.path.at(-1)}</h3>
|
||||
<h3>{schema.path.at(-1)}</h3>
|
||||
|
||||
{#if subparts.length > 0}
|
||||
{#if subparts.length > 0}
|
||||
<span class="subtle">
|
||||
{schema.description}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if $values.length === 0}
|
||||
No values are defined
|
||||
{:else if subparts.length === 0}
|
||||
<!-- We need an array of values, so we use the typehint of the _parent_ element as field -->
|
||||
{#each $values as value (value)}
|
||||
<SchemaBasedField {state} {schema} path={[...path, value]}/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each $values as value (value)}
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="m-0">{singular} {value}</h3>
|
||||
<button class="border-black border rounded-full p-1 w-fit h-fit" on:click={() => {values.data.splice(values.data.indexOf(value)); values.ping()}}>
|
||||
<TrashIcon class="w-4 h-4"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="border border-black">
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart}/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<button on:click={createItem}>Add {article} {singular}</button>
|
||||
{#if $values.length === 0}
|
||||
No values are defined
|
||||
{:else if subparts.length === 0}
|
||||
<!-- We need an array of values, so we use the typehint of the _parent_ element as field -->
|
||||
{#each $values as value (value)}
|
||||
<div class="flex w-full">
|
||||
<SchemaBasedField {state} {schema} path={[...path, value]} />
|
||||
<button class="border-black border rounded-full p-1 w-fit h-fit"
|
||||
on:click={() => {del(value)}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each $values as value (value)}
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="m-0">{singular} {value}</h3>
|
||||
<button class="border-black border rounded-full p-1 w-fit h-fit"
|
||||
on:click={() => {del(value)}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="border border-black">
|
||||
{#if path.length === 1 && path[0] === "tagRenderings"}
|
||||
<TagRenderingInput path={path.concat(value)} {state} {schema}/>
|
||||
{:else}
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<button on:click={createItem}>Add {article} {singular}</button>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
id: path.join("_"),
|
||||
render: schema.type === "boolean" ? undefined : ((schema.hints.inline ?? schema.path.at(-1) )+ ": <b>{value}</b>"),
|
||||
render: schema.type === "boolean" ? undefined : ((schema.hints.inline ?? schema.path.at(-1) )+ ": <b>{translated(value)}</b>"),
|
||||
question: schema.hints.question,
|
||||
questionHint: nmd(schema.description),
|
||||
freeform: schema.type === "boolean" ? undefined : {
|
||||
|
@ -76,12 +76,10 @@
|
|||
err = path.join(".") + " " + e
|
||||
}
|
||||
let startValue = state.getCurrentValueFor(path)
|
||||
console.log("StartValue for", path.join("."), " is", startValue)
|
||||
if (typeof startValue !== "string") {
|
||||
startValue = JSON.stringify(startValue)
|
||||
}
|
||||
const tags = new UIEventSource<Record<string, string>>({value: startValue ?? ""})
|
||||
tags.addCallbackAndRunD(tgs => console.log(">>> tgs for",path.join("."),"are",tgs ))
|
||||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
if (schema.type === "boolan") {
|
||||
|
@ -90,7 +88,6 @@
|
|||
if (schema.type === "number") {
|
||||
return Number(v)
|
||||
}
|
||||
console.log(schema, v)
|
||||
if(isTranslation) {
|
||||
if(v === ""){
|
||||
return {}
|
||||
|
@ -104,7 +101,7 @@
|
|||
{#if err !== undefined}
|
||||
<span class="alert">{err}</span>
|
||||
{:else}
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -11,10 +11,14 @@
|
|||
export let state: EditLayerState
|
||||
export let path: (string | number)[] = []
|
||||
|
||||
|
||||
console.log("Constructing", path,"with schema", schema)
|
||||
|
||||
</script>
|
||||
|
||||
{#if schema.type === "array"}
|
||||
{#if schema.hints.typehint === "tagrendering[]"}
|
||||
<!-- We cheat a bit here by matching this 'magical' type... -->
|
||||
<SchemaBasedArray {path} {state} {schema}/>
|
||||
{:else if schema.type === "array"}
|
||||
<SchemaBasedArray {path} {state} {schema}/>
|
||||
{:else if schema.hints.typehint === "tag"}
|
||||
<RegisteredTagInput {state} {path} {schema}/>
|
||||
|
|
61
src/UI/Studio/TagRenderingInput.svelte
Normal file
61
src/UI/Studio/TagRenderingInput.svelte
Normal file
|
@ -0,0 +1,61 @@
|
|||
<script lang="ts">/**
|
||||
* Little helper class to deal with choosing a builtin tagRendering or defining one yourself.
|
||||
* Breaks the ideology that everything should be schema based
|
||||
*/
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import type {
|
||||
MappingConfigJson,
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import * as questions from "../../assets/generated/layers/questions.json";
|
||||
|
||||
export let state: EditLayerState;
|
||||
export let schema: ConfigMeta;
|
||||
export let path: (string | number)[];
|
||||
|
||||
let value = state.getCurrentValueFor(path);
|
||||
|
||||
let mappings: MappingConfigJson[] = [];
|
||||
for (const tr of questions.tagRenderings) {
|
||||
let description = tr["description"] ?? tr["question"] ?? "No description available";
|
||||
description = description["en"] ?? description;
|
||||
mappings.push({
|
||||
if: "value=" + tr["id"],
|
||||
then: {
|
||||
"en": "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{
|
||||
question: "Which builtin element should be shown?",
|
||||
mappings
|
||||
});
|
||||
|
||||
|
||||
const configOverride = <QuestionableTagRenderingConfigJson>{
|
||||
render: "This is a builtin question which changes some properties. Editing those is not possible within MapComplete Studio"
|
||||
};
|
||||
|
||||
const tags = new UIEventSource({ value });
|
||||
|
||||
tags.addCallbackAndRunD(tgs => {
|
||||
state.setValueAt(path, tgs["value"]);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if typeof value === "string"}
|
||||
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
|
||||
{tags} />
|
||||
|
||||
{:else}
|
||||
<div>
|
||||
TR{JSON.stringify(state.getCurrentValueFor(path))}
|
||||
</div>
|
||||
{/if}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -591,7 +591,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "\nThis piece of text will be shown in the infobox.\nNote that \"{key}\"-parts are substituted by the corresponding values of the element.\n\nThis text will be shown if:\n- there is no mapping which matches (or there are no matches)\n- no question, no mappings and no 'freeform' is set\n\nNote that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />`"
|
||||
"description": "\nThis piece of text will be shown in the infobox.\nNote that \"&LBRACEkey&RBRACE\"-parts are substituted by the corresponding values of the element.\n\nThis text will be shown if:\n- there is no mapping which matches (or there are no matches)\n- no question, no mappings and no 'freeform' is set\n\nNote that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />`"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "\nThis piece of text will be shown in the infobox.\nNote that \"{key}\"-parts are substituted by the corresponding values of the element.\n\nThis text will be shown if:\n- there is no mapping which matches (or there are no matches)\n- no question, no mappings and no 'freeform' is set\n\nNote that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />`"
|
||||
"description": "\nThis piece of text will be shown in the infobox.\nNote that \"&LBRACEkey&RBRACE\"-parts are substituted by the corresponding values of the element.\n\nThis text will be shown if:\n- there is no mapping which matches (or there are no matches)\n- no question, no mappings and no 'freeform' is set\n\nNote that this is a HTML-interpreted value, so you can add links as e.g. '<a href='{website}'>{website}</a>' or include images such as `This is of type A <br><img src='typeA-icon.svg' />`"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue