Studio: WIP

This commit is contained in:
Pieter Vander Vennet 2023-08-08 13:52:58 +02:00
parent 6ff2c629f0
commit cb538c2303
33 changed files with 608 additions and 2071 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

@ -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. '&lt;a href='{website}'>{website}&lt;/a>' or include images such as `This is of type A &lt;br>&lt;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. '&lt;a href='{website}'>{website}&lt;/a>' or include images such as `This is of type A &lt;br>&lt;img src='typeA-icon.svg' />`"
},
{
"path": [

View file

@ -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. '&lt;a href='{website}'>{website}&lt;/a>' or include images such as `This is of type A &lt;br>&lt;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. '&lt;a href='{website}'>{website}&lt;/a>' or include images such as `This is of type A &lt;br>&lt;img src='typeA-icon.svg' />`"
},
{
"path": [