forked from MapComplete/MapComplete
Studio: usability tests
This commit is contained in:
parent
0f60977b6d
commit
2041a9245d
37 changed files with 524 additions and 446 deletions
|
|
@ -71,6 +71,11 @@ export default class EditLayerState {
|
|||
sharedLayers: layers,
|
||||
}
|
||||
}
|
||||
|
||||
const prepare = new Pipe(
|
||||
new PrepareLayer(state),
|
||||
new ValidateLayer("dynamic", false, undefined, true)
|
||||
)
|
||||
this.messages = this.configuration.mapD((config) => {
|
||||
const trs = Utils.NoNull(config.tagRenderings ?? [])
|
||||
for (let i = 0; i < trs.length; i++) {
|
||||
|
|
@ -93,11 +98,6 @@ export default class EditLayerState {
|
|||
}
|
||||
}
|
||||
|
||||
const prepare = new Pipe(
|
||||
new PrepareLayer(state),
|
||||
new ValidateLayer("dynamic", false, undefined)
|
||||
)
|
||||
|
||||
const context = ConversionContext.construct([], ["prepare"])
|
||||
prepare.convert(<LayerConfigJson>config, context)
|
||||
return context.messages
|
||||
|
|
|
|||
|
|
@ -1,156 +1,169 @@
|
|||
<script lang="ts">
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import nmd from "nano-markdown"
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import { onDestroy } from "svelte";
|
||||
import type { JsonSchemaType } from "./jsonSchema";
|
||||
import { ConfigMetaUtils } from "./configMeta.ts"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import nmd from "nano-markdown";
|
||||
import type {
|
||||
QuestionableTagRenderingConfigJson
|
||||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import { onDestroy } from "svelte";
|
||||
import type { JsonSchemaType } from "./jsonSchema";
|
||||
import { ConfigMetaUtils } from "./configMeta.ts";
|
||||
|
||||
export let state: EditLayerState
|
||||
export let path: (string | number)[] = []
|
||||
export let schema: ConfigMeta
|
||||
let value = new UIEventSource<string | any>(undefined)
|
||||
|
||||
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema)
|
||||
let type = schema.hints.typehint ?? "string"
|
||||
if(isTranslation){
|
||||
type = "translation"
|
||||
export let state: EditLayerState;
|
||||
export let path: (string | number)[] = [];
|
||||
export let schema: ConfigMeta;
|
||||
let value = new UIEventSource<string | any>(undefined);
|
||||
|
||||
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
|
||||
let type = schema.hints.typehint ?? "string";
|
||||
|
||||
let rendervalue = schema.type === "boolean" ? undefined : ((schema.hints.inline ?? schema.path.join(".")) + " <b>{translated(value)}</b>")
|
||||
let helperArgs = undefined
|
||||
let inline = schema.hints.inline !== undefined
|
||||
if (isTranslation) {
|
||||
type = "translation";
|
||||
if(schema.hints.inline){
|
||||
const inlineValue = schema.hints.inline
|
||||
rendervalue = inlineValue
|
||||
inline = false
|
||||
helperArgs = [inlineValue.substring(0, inlineValue.indexOf("{")), inlineValue.substring(inlineValue.indexOf("}") + 1)]
|
||||
}
|
||||
if(type.endsWith("[]")){
|
||||
type = type.substring(0, type.length - 2)
|
||||
}
|
||||
if (type.endsWith("[]")) {
|
||||
type = type.substring(0, type.length - 2);
|
||||
}
|
||||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
id: path.join("_"),
|
||||
render: rendervalue,
|
||||
question: schema.hints.question,
|
||||
questionHint: nmd(schema.description),
|
||||
freeform: schema.type === "boolean" ? undefined : {
|
||||
key: "value",
|
||||
type,
|
||||
inline,
|
||||
helperArgs
|
||||
}
|
||||
};
|
||||
|
||||
if (schema.hints.default) {
|
||||
configJson.mappings = [{
|
||||
if: "value=", // We leave this blank
|
||||
then: schema.path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? "")
|
||||
}];
|
||||
} else if (!schema.required) {
|
||||
configJson.mappings = [{
|
||||
if: "value=",
|
||||
then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? "")
|
||||
}];
|
||||
}
|
||||
|
||||
function mightBeBoolean(type: undefined | JsonSchemaType): boolean {
|
||||
if (type === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (type["type"]) {
|
||||
type = type["type"];
|
||||
}
|
||||
if (type === "boolean") {
|
||||
return true;
|
||||
}
|
||||
if (!Array.isArray(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
id: path.join("_"),
|
||||
render: schema.type === "boolean" ? undefined : ((schema.hints.inline ?? schema.path.at(-1) )+ ": translated value: <b>{translated(value)}</b>"),
|
||||
question: schema.hints.question,
|
||||
questionHint: nmd(schema.description),
|
||||
freeform: schema.type === "boolean" ? undefined : {
|
||||
key: "value",
|
||||
type,
|
||||
inline: schema.hints.inline !== undefined
|
||||
},
|
||||
}
|
||||
return type.some(t => mightBeBoolean(t));
|
||||
}
|
||||
|
||||
if (schema.hints.default) {
|
||||
configJson.mappings = [{
|
||||
if: "value=", // We leave this blank
|
||||
then: schema.path.at(-1) + " is not set. The default value <b>" + schema.hints.default + "</b> will be used. " + (schema.hints.ifunset ?? ""),
|
||||
}]
|
||||
} else if (!schema.required) {
|
||||
configJson.mappings = [{
|
||||
if: "value=",
|
||||
then: schema.path.at(-1) + " is not set. " + (schema.hints.ifunset ?? ""),
|
||||
}]
|
||||
}
|
||||
if (mightBeBoolean(schema.type)) {
|
||||
configJson.mappings = configJson.mappings ?? [];
|
||||
configJson.mappings.push(
|
||||
{
|
||||
if: "value=true",
|
||||
then: schema.hints?.iftrue ?? "Yes"
|
||||
},
|
||||
{
|
||||
if: "value=false",
|
||||
then: schema.hints?.iffalse ?? "No"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function mightBeBoolean(type: undefined | JsonSchemaType): boolean {
|
||||
if(type === undefined){
|
||||
return false
|
||||
}
|
||||
if(type["type"]){
|
||||
type = type["type"]
|
||||
}
|
||||
if(type === "boolean"){
|
||||
return true
|
||||
}
|
||||
if(!Array.isArray(type)){
|
||||
return false
|
||||
}
|
||||
|
||||
return type.some(t => mightBeBoolean(t) )
|
||||
if (schema.hints.suggestions) {
|
||||
if (!configJson.mappings) {
|
||||
configJson.mappings = [];
|
||||
}
|
||||
if (mightBeBoolean(schema.type)) {
|
||||
configJson.mappings = configJson.mappings ?? []
|
||||
configJson.mappings.push(
|
||||
{
|
||||
if: "value=true",
|
||||
then: "Yes: "+(schema.hints?.iftrue??"")
|
||||
},
|
||||
{
|
||||
if: "value=false",
|
||||
then: "No: "+(schema.hints?.iffalse??"")
|
||||
}
|
||||
)
|
||||
configJson.mappings.push(...schema.hints.suggestions);
|
||||
}
|
||||
let config: TagRenderingConfig;
|
||||
let err: string = undefined;
|
||||
let messages = state.messages.mapD(msgs => msgs.filter(msg => {
|
||||
const pth = msg.context.path;
|
||||
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
|
||||
if (pth[i] !== path[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.hints.suggestions) {
|
||||
if (!configJson.mappings) {
|
||||
configJson.mappings = []
|
||||
return true;
|
||||
}));
|
||||
try {
|
||||
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."));
|
||||
} catch (e) {
|
||||
console.error(e, config);
|
||||
err = path.join(".") + " " + e;
|
||||
}
|
||||
let startValue = state.getCurrentValueFor(path);
|
||||
const tags = new UIEventSource<Record<string, string>>({ value: startValue });
|
||||
try {
|
||||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
if (typeof v !== "string") {
|
||||
return v;
|
||||
}
|
||||
if (schema.type === "boolan") {
|
||||
return v === "true" || v === "yes" || v === "1";
|
||||
}
|
||||
if (mightBeBoolean(schema.type)) {
|
||||
if (v === "true" || v === "yes" || v === "1") {
|
||||
return true;
|
||||
}
|
||||
configJson.mappings.push(...schema.hints.suggestions)
|
||||
}
|
||||
let config: TagRenderingConfig
|
||||
let err: string = undefined
|
||||
let messages = state.messages.mapD(msgs => msgs.filter(msg => {
|
||||
const pth = msg.context.path
|
||||
for (let i = 0; i < Math.min(pth.length, path.length); i++) {
|
||||
if(pth[i] !== path[i]){
|
||||
return false
|
||||
}
|
||||
if (v === "false" || v === "no" || v === "0") {
|
||||
console.log("Setting false...");
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}))
|
||||
try {
|
||||
config = new TagRenderingConfig(configJson, "config based on " + schema.path.join("."))
|
||||
} catch (e) {
|
||||
console.error(e, config)
|
||||
err = path.join(".") + " " + e
|
||||
}
|
||||
let startValue = state.getCurrentValueFor(path)
|
||||
const tags = new UIEventSource<Record<string, string>>({value: startValue})
|
||||
try {
|
||||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
if(typeof v !== "string"){
|
||||
return v
|
||||
}
|
||||
if (schema.type === "boolan") {
|
||||
return v === "true" || v === "yes" || v === "1"
|
||||
}
|
||||
if(mightBeBoolean(schema.type)){
|
||||
if(v === "true" || v === "yes" || v === "1"){
|
||||
return true
|
||||
}
|
||||
if(v === "false" || v === "no" || v === "0"){
|
||||
console.log("Setting false...")
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (schema.type === "number") {
|
||||
if(v === ""){
|
||||
return undefined
|
||||
}
|
||||
return Number(v)
|
||||
}
|
||||
if (isTranslation && typeof v === "string") {
|
||||
if (v === "") {
|
||||
return {}
|
||||
}
|
||||
return JSON.parse(v)
|
||||
}
|
||||
return v
|
||||
}), isTranslation))
|
||||
}catch (e) {
|
||||
console.error("Could not register", path,"due to",e)
|
||||
}
|
||||
}
|
||||
if (schema.type === "number") {
|
||||
if (v === "") {
|
||||
return undefined;
|
||||
}
|
||||
return Number(v);
|
||||
}
|
||||
if (isTranslation && typeof v === "string") {
|
||||
if (v === "") {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(v);
|
||||
}
|
||||
return v;
|
||||
}), isTranslation));
|
||||
} catch (e) {
|
||||
console.error("Could not register", path, "due to", e);
|
||||
}
|
||||
</script>
|
||||
{#if err !== undefined}
|
||||
<span class="alert">{err}</span>
|
||||
<span class="alert">{err}</span>
|
||||
{:else}
|
||||
<div class="w-full flex flex-col">
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/>
|
||||
{#if $messages.length > 0}
|
||||
{#each $messages as msg}
|
||||
<div class="alert">{msg.message}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-full flex flex-col">
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
|
||||
{#if $messages.length > 0}
|
||||
{#each $messages as msg}
|
||||
<div class="alert">{msg.message}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<span class="subtle">{schema.path.join(".")}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import * as questions from "../../assets/generated/layers/questions.json";
|
||||
import MappingInput from "./MappingInput.svelte";
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline";
|
||||
|
|
@ -22,7 +22,14 @@ export let state: EditLayerState;
|
|||
export let schema: ConfigMeta;
|
||||
export let path: (string | number)[];
|
||||
|
||||
let value = state.getCurrentValueFor(path) ;
|
||||
let value = state.getCurrentValueFor(path);
|
||||
|
||||
/**
|
||||
* Allows the theme builder to create 'writable' themes.
|
||||
* Should only be enabled for 'tagrenderings' in the theme, if the source is OSM
|
||||
*/
|
||||
let allowQuestions: Store<boolean> = (state.configuration.mapD(config => config.source?.geoJson === undefined))
|
||||
|
||||
|
||||
let mappingsBuiltin: MappingConfigJson[] = [];
|
||||
for (const tr of questions.tagRenderings) {
|
||||
|
|
@ -44,13 +51,14 @@ const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson
|
|||
|
||||
|
||||
const tags = new UIEventSource({ value });
|
||||
const store = state.getStoreFor(path)
|
||||
const store = state.getStoreFor(path);
|
||||
tags.addCallbackAndRunD(tgs => {
|
||||
store.setData(tgs["value"])
|
||||
store.setData(tgs["value"]);
|
||||
});
|
||||
|
||||
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
|
||||
|
||||
$: console.log("Allow questions:", $allowQuestions)
|
||||
const topLevelItems: Record<string, ConfigMeta> = {};
|
||||
for (const item of questionableTagRenderingSchemaRaw) {
|
||||
if (item.path.length === 1) {
|
||||
|
|
@ -64,7 +72,12 @@ function initMappings() {
|
|||
}
|
||||
}
|
||||
|
||||
const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && schema.path[0] === "freeform");
|
||||
const items = new Set(["question", "questionHint", "multiAnswer", "freeform", "render", "condition", "metacondition", "mappings", "icon"]);
|
||||
const ignored = new Set(["labels", "description", "classes"]);
|
||||
|
||||
const freeformSchema = <ConfigMeta[]>questionableTagRenderingSchemaRaw
|
||||
.filter(schema => schema.path.length == 2 && schema.path[0] === "freeform" && ($allowQuestions || schema.path[1] === "key"));
|
||||
const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0])).map(schema => schema.path.join("."));
|
||||
</script>
|
||||
|
||||
{#if typeof value === "string"}
|
||||
|
|
@ -80,10 +93,12 @@ const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(
|
|||
<slot name="upper-right" />
|
||||
</div>
|
||||
|
||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]}></SchemaBasedField>
|
||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]}></SchemaBasedField>
|
||||
<SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]}></SchemaBasedField>
|
||||
|
||||
{#if $allowQuestions}
|
||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
|
||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
|
||||
{:else}
|
||||
|
||||
{/if}
|
||||
{#each ($mappings ?? []) as mapping, i (mapping)}
|
||||
<div class="flex interactive w-full">
|
||||
<MappingInput {mapping} {state} path={path.concat(["mappings", i])}>
|
||||
|
|
@ -98,17 +113,26 @@ const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(
|
|||
</div>
|
||||
{/each}
|
||||
|
||||
<button class="small primary"
|
||||
<button class="primary"
|
||||
on:click={() =>{ initMappings(); mappings.data.push({if: undefined, then: {}}); mappings.ping()} }>
|
||||
Add a mapping
|
||||
</button>
|
||||
|
||||
<SchemaBasedField {state} path={[...path,"multiAnswer"]} schema={topLevelItems["multiAnswer"]}></SchemaBasedField>
|
||||
|
||||
<div class="border border-gray-200 border-dashed">
|
||||
<h3>Text field and input element configuration</h3>
|
||||
<Region {state} {path} configs={freeformSchema}/>
|
||||
<SchemaBasedField {state} path={[...path,"multiAnswer"]} schema={topLevelItems["multiAnswer"]} />
|
||||
|
||||
<h3>Text field and input element configuration</h3>
|
||||
<div class="border-l pl-2 border-gray-800 border-dashed">
|
||||
<SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]} />
|
||||
<Region {state} {path} configs={freeformSchema} />
|
||||
<SchemaBasedField {state} path={[...path,"icon"]} schema={topLevelItems["icon"]} />
|
||||
|
||||
</div>
|
||||
|
||||
<SchemaBasedField {state} path={[...path,"condition"]} schema={topLevelItems["condition"]} />
|
||||
<SchemaBasedField {state} path={[...path,"metacondition"]} schema={topLevelItems["metacondition"]} />
|
||||
|
||||
{#each missing as field}
|
||||
<SchemaBasedField {state} path={[...path,field]} schema={topLevelItems[field]} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue