Studio: UX improvements after usertest

This commit is contained in:
Pieter Vander Vennet 2023-10-22 01:30:05 +02:00
parent 44c1548e89
commit a9bfe4f37b
11 changed files with 173 additions and 122 deletions

View file

@ -18,5 +18,6 @@ The participant has extensive OpenStreetMap-knowledge but only used MapComplete
- [x] The 'try it out'-button should be a 'next'-button - [x] The 'try it out'-button should be a 'next'-button
- [x] Entering an incorrect ID and pressing enter still takes you to the layer editor with an incorrect ID - [x] Entering an incorrect ID and pressing enter still takes you to the layer editor with an incorrect ID
- [x] A name and description are obligatory to use the layer as single-layer-theme; but those error messages are unclear. - [x] A name and description are obligatory to use the layer as single-layer-theme; but those error messages are unclear.
- [ ] - [x] This user had an expression with two tags in an AND. There was some confusion if the taginfo-count gave the totals for the tags individually or for the entire expression.
- [ ] Fix: play with padding and wording
- [x] BUG: having a complex expression for tags (e.g. with `and: [key=value, key0=value0]`) fails as the JSON would be stringified

View file

@ -1109,7 +1109,7 @@ export class ValidateLayer extends Conversion<
const doMatch = baseTags.matchesProperties(properties) const doMatch = baseTags.matchesProperties(properties)
if (!doMatch) { if (!doMatch) {
context context
.enters("presets", i) .enters("presets", i, "tags")
.err( .err(
"This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + "This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
JSON.stringify(properties) + JSON.stringify(properties) +

View file

@ -5,7 +5,7 @@ import { UIEventSource } from "../../../Logic/UIEventSource";
import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson"; import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson";
import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte"; import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte";
export let value: UIEventSource<undefined | string>; export let value: UIEventSource<TagConfigJson>;
export let uploadableOnly: boolean; export let uploadableOnly: boolean;
export let overpassSupportNeeded: boolean; export let overpassSupportNeeded: boolean;
@ -14,18 +14,7 @@ export let overpassSupportNeeded: boolean;
*/ */
export let silent: boolean = false; export let silent: boolean = false;
let tag: UIEventSource<string | TagConfigJson> = value.sync(s => { let tag: UIEventSource<string | TagConfigJson> = value
try {
return JSON.parse(s);
} catch (e) {
return s;
}
}, [], t => {
if(typeof t === "string"){
return t
}
return JSON.stringify(t);
});
</script> </script>

View file

@ -20,7 +20,7 @@
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"; import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte";
export let type: ValidatorType; export let type: ValidatorType;
export let value: UIEventSource<string>; export let value: UIEventSource<string | object>;
export let feature: Feature; export let feature: Feature;
export let args: (string | number | boolean)[] = undefined; export let args: (string | number | boolean)[] = undefined;

View file

@ -74,7 +74,12 @@
} }
feedback?.setData(undefined) feedback?.setData(undefined)
value.setData(v + (selectedUnit.data ?? "")) if(selectedUnit.data){
value.setData(v + selectedUnit.data)
}else{
value.setData(v)
}
} }

View file

@ -238,7 +238,7 @@
bind:group={selectedMapping} bind:group={selectedMapping}
name={"mappings-radio-" + config.id} name={"mappings-radio-" + config.id}
value={i} value={i}
on:keypress={e => {console.log(e) ; if(e.key === "Enter") onSave()}} on:keypress={e => {if(e.key === "Enter") onSave()}}
/> />
</TagRenderingMappingInput> </TagRenderingMappingInput>
{/each} {/each}

View file

@ -75,6 +75,7 @@ import AllReviews from "./Reviews/AllReviews.svelte"
import StarsBarIcon from "./Reviews/StarsBarIcon.svelte" import StarsBarIcon from "./Reviews/StarsBarIcon.svelte"
import ReviewForm from "./Reviews/ReviewForm.svelte" import ReviewForm from "./Reviews/ReviewForm.svelte"
import Questionbox from "./Popup/TagRendering/Questionbox.svelte" import Questionbox from "./Popup/TagRendering/Questionbox.svelte"
import { TagUtils } from "../Logic/Tags/TagUtils"
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -1400,6 +1401,49 @@ export default class SpecialVisualizations {
return new FixedUiElement("{" + args[0] + "}") return new FixedUiElement("{" + args[0] + "}")
}, },
}, },
{
funcName: "tags",
docs: "Shows a (json of) tags in a human-readable way + links to the wiki",
needsUrls: [],
args: [
{
name: "key",
defaultValue: "value",
doc: "The key to look for the tags",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const key = argument[0] ?? "value"
return new VariableUiElement(
tagSource.map((tags) => {
let value = tags[key]
if (!value) {
return new FixedUiElement("No tags found").SetClass("font-bold")
}
if (typeof value === "string" && value.startsWith("{")) {
value = JSON.parse(value)
}
try {
const parsed = TagUtils.Tag(value)
return parsed.asHumanString(true, false, {})
} catch (e) {
return new FixedUiElement(
"Could not parse this tag: " +
JSON.stringify(value) +
" due to " +
e
).SetClass("alert")
}
})
)
},
},
] ]
specialVisualizations.push(new AutoApplyButton(specialVisualizations)) specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -21,7 +21,15 @@
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema); const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
let type = schema.hints.typehint ?? "string"; let type = schema.hints.typehint ?? "string";
let rendervalue = schema.type === "boolean" ? undefined : ((schema.hints.inline ?? schema.path.join(".")) + " <b>{translated(value)}</b>"); let rendervalue = ((schema.hints.inline ?? schema.path.join(".")) + " <b>{translated(value)}</b>");
if(schema.type === "boolean"){
rendervalue = undefined
}
if(schema.hints.typehint === "tag") {
rendervalue = "{tags()}"
}
let helperArgs = schema.hints.typehelper?.split(","); let helperArgs = schema.hints.typehelper?.split(",");
let inline = schema.hints.inline !== undefined; let inline = schema.hints.inline !== undefined;
if (isTranslation) { if (isTranslation) {
@ -165,7 +173,7 @@
{/each} {/each}
{/if} {/if}
{#if window.location.hostname === "127.0.0.1"} {#if window.location.hostname === "127.0.0.1"}
<span class="subtle">{schema.path.join(".")}</span> <span class="subtle">{schema.path.join(".")} {schema.hints.typehint}</span>
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -24,63 +24,63 @@ export let overpassSupportNeeded: boolean;
export let silent: boolean; export let silent: boolean;
function update(_) { function update(_) {
let config: TagConfigJson = <any>{}; let config: TagConfigJson = <any>{};
if (!mode) { if (!mode) {
return; return;
} }
const tags = []; const tags = [];
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data); const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data);
for (const src of subpartSources) { for (const src of subpartSources) {
const t = src.data; const t = src.data;
if (!t) { if (!t) {
// We indicate upstream that this value is invalid // We indicate upstream that this value is invalid
tag.setData(undefined); tag.setData(undefined);
return; return;
}
tags.push(t);
}
if (tags.length === 1) {
tag.setData(tags[0]);
} else {
config[mode] = tags;
tag.setData(config);
} }
tags.push(t);
}
if (tags.length === 1) {
tag.setData(tags[0]);
} else {
config[mode] = tags;
tag.setData(config);
}
} }
function addBasicTag(value?: string) { function addBasicTag(value?: string) {
const src = new UIEventSource(value); const src = new UIEventSource(value);
basicTags.data.push(src); basicTags.data.push(src);
basicTags.ping(); basicTags.ping();
src.addCallbackAndRunD(_ => update(_)); src.addCallbackAndRunD(_ => update(_));
} }
function removeTag(basicTag: UIEventSource<any>) { function removeTag(basicTag: UIEventSource<any>) {
const index = basicTags.data.indexOf(basicTag); const index = basicTags.data.indexOf(basicTag);
console.log("Removing", index, basicTag); console.log("Removing", index, basicTag);
if (index >= 0) { if (index >= 0) {
basicTag.setData(undefined); basicTag.setData(undefined);
basicTags.data.splice(index, 1); basicTags.data.splice(index, 1);
basicTags.ping(); basicTags.ping();
} }
} }
function removeExpression(expr: UIEventSource<any>) { function removeExpression(expr: UIEventSource<any>) {
const index = expressions.data.indexOf(expr); const index = expressions.data.indexOf(expr);
if (index >= 0) { if (index >= 0) {
expr.setData(undefined); expr.setData(undefined);
expressions.data.splice(index, 1); expressions.data.splice(index, 1);
expressions.ping(); expressions.ping();
} }
} }
function addExpression(expr?: TagConfigJson) { function addExpression(expr?: TagConfigJson) {
const src = new UIEventSource(expr); const src = new UIEventSource(expr);
expressions.data.push(src); expressions.data.push(src);
expressions.ping(); expressions.ping();
src.addCallbackAndRunD(_ => update(_)); src.addCallbackAndRunD(_ => update(_));
} }
@ -91,32 +91,36 @@ basicTags.addCallback(_ => update(_));
let initialTag: TagConfigJson = tag.data; let initialTag: TagConfigJson = tag.data;
function initWith(initialTag: TagConfigJson) { function initWith(initialTag: TagConfigJson) {
if (typeof initialTag === "string") { if (typeof initialTag === "string") {
addBasicTag(initialTag); if (initialTag.startsWith("{")) {
return; initialTag = JSON.parse(initialTag);
} else {
addBasicTag(initialTag);
return;
} }
mode = <"or" | "and">Object.keys(initialTag)[0]; }
const subExprs = (<TagConfigJson[]>initialTag[mode]); mode = <"or" | "and">Object.keys(initialTag)[0];
if (!subExprs || subExprs.length == 0) { const subExprs = (<TagConfigJson[]>initialTag[mode]);
return; if (!subExprs || subExprs.length == 0) {
} return;
if (subExprs.length == 1) { }
initWith(subExprs[0]); if (subExprs.length == 1) {
return; initWith(subExprs[0]);
} return;
for (const subExpr of subExprs) { }
if (typeof subExpr === "string") { for (const subExpr of subExprs) {
addBasicTag(subExpr); if (typeof subExpr === "string") {
} else { addBasicTag(subExpr);
addExpression(subExpr); } else {
} addExpression(subExpr);
} }
}
} }
if (!initialTag) { if (!initialTag) {
addBasicTag(); addBasicTag();
} else { } else {
initWith(initialTag); initWith(initialTag);
} }
@ -125,45 +129,45 @@ if (!initialTag) {
<div class="flex items-center"> <div class="flex items-center">
{#if !uploadableOnly} {#if !uploadableOnly}
<select bind:value={mode}> <select bind:value={mode}>
<option value="and">and</option> <option value="and">and</option>
<option value="or">or</option> <option value="or">or</option>
</select> </select>
{/if} {/if}
<div class="border-l-4 border-black flex flex-col ml-1 pl-1"> <div class="border-l-4 border-black flex flex-col ml-1 pl-1">
{#each $basicTags as basicTag (basicTag)} {#each $basicTags as basicTag (basicTag)}
<div class="flex"> <div class="flex">
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} /> <BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} />
{#if $basicTags.length + $expressions.length > 1} {#if $basicTags.length + $expressions.length > 1}
<button class="border border-black rounded-full w-fit h-fit p-0" <button class="border border-black rounded-full w-fit h-fit p-0"
on:click={() => removeTag(basicTag)}> on:click={() => removeTag(basicTag)}>
<TrashIcon class="w-4 h-4 p-1" /> <TrashIcon class="w-4 h-4 p-1" />
</button> </button>
{/if} {/if}
</div> </div>
{/each} {/each}
{#each $expressions as expression} {#each $expressions as expression}
<FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}> <FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}>
<button class="small" slot="delete" on:click={() => removeExpression(expression)}> <button class="small" slot="delete" on:click={() => removeExpression(expression)}>
<TrashIcon class="w-3 h-3 p-0" /> <TrashIcon class="w-3 h-3 p-0" />
Delete subexpression Delete subexpression
</button> </button>
</FullTagInput> </FullTagInput>
{/each} {/each}
<div class="flex"> <div class="flex">
<button class="w-fit small" on:click={() => addBasicTag()}> <button class="w-fit small" on:click={() => addBasicTag()}>
Add a tag Add a tag
</button> </button>
{#if !uploadableOnly} {#if !uploadableOnly}
<!-- Do not allow to add an expression, as everything is 'and' anyway --> <!-- Do not allow to add an expression, as everything is 'and' anyway -->
<button class="w-fit small" on:click={() => addExpression()}> <button class="w-fit small" on:click={() => addExpression()}>
Add an expression Add an expression
</button> </button>
{/if} {/if}
<slot name="delete" /> <slot name="delete" />
</div>
</div> </div>
</div>
</div> </div>

View file

@ -58,6 +58,6 @@ const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").co
{/if} {/if}
{:else if $tagInfoStats && (!silent || $total < 250) } {:else if $tagInfoStats && (!silent || $total < 250) }
<a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}> <a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}>
{$total} features on OSM have this tag {$total} features have <span class="literal-code">{$tag}</span>
</a> </a>
{/if} {/if}

View file

@ -130,5 +130,5 @@
{:else if $feedbackGlobal} {:else if $feedbackGlobal}
<Tr cls="alert" t={$feedbackGlobal}/> <Tr cls="alert" t={$feedbackGlobal}/>
{/if} {/if}
<TagInfoStats {silent} {tag}/> <TagInfoStats {silent} {tag}/>
</div> </div>