Studio: improvements after user test

This commit is contained in:
Pieter Vander Vennet 2023-11-02 04:35:32 +01:00
parent 449c1adb00
commit e79a0fc81d
59 changed files with 1312 additions and 2920 deletions

View file

@ -1,20 +1,10 @@
<script lang="ts">
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { createEventDispatcher } from "svelte";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
import { AllKnownLayouts, AllKnownLayoutsLazy } from "../../Customizations/AllKnownLayouts";
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import EditItemButton from "./EditItemButton.svelte";
export let layerIds: { id: string }[];
export let layerIds: { id: string, owner: number }[];
export let category: "layers" | "themes" = "layers";
const dispatch = createEventDispatcher<{ layerSelected: string }>();
function fetchIconDescription(layerId): any {
if(category === "themes"){
return AllKnownLayouts.allKnownLayouts.get(layerId).icon
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
export let osmConnection: OsmConnection;
</script>
@ -22,12 +12,7 @@
<slot name="title" />
<div class="flex flex-wrap">
{#each Array.from(layerIds) as layer}
<NextButton clss="small" on:click={() => dispatch("layerSelected", layer)}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layer.id)} />
</div>
{layer.id}
</NextButton>
<EditItemButton info={layer} {category} {osmConnection} on:layerSelected/>
{/each}
</div>
{/if}

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
import { createEventDispatcher } from "svelte";
export let info: { id: string, owner: number };
export let category: "layers" | "themes";
export let osmConnection: OsmConnection;
let displayName = UIEventSource.FromPromise(osmConnection.getInformationAboutUser(info.owner)).mapD(response => response.display_name);
let selfId = osmConnection.userDetails.mapD(ud => ud.uid)
function fetchIconDescription(layerId): any {
if (category === "themes") {
return AllKnownLayouts.allKnownLayouts.get(layerId).icon;
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
const dispatch = createEventDispatcher<{ layerSelected: string }>();
</script>
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(info.id)} />
</div>
<b class="px-1"> {info.id}</b>
{#if info.owner && info.owner !== $selfId}
(made by {$displayName ?? info.owner})
{/if}
</NextButton>

View file

@ -21,8 +21,8 @@
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
export let state: EditLayerState;
const messages = state.messages;
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
let messages = state.messages;
let hasErrors = messages.mapD((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
const configuration = state.configuration;
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
@ -33,7 +33,7 @@
}
const title: Store<string> = state.getStoreFor(["id"]);
let title: Store<string> = state.getStoreFor(["id"]);
const wl = window.location;
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";
@ -53,13 +53,15 @@
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"];
let currentlyMissing = state.configuration.map(config => {
if(!config){
return []
}
const missing = [];
for (const requiredField of requiredFields) {
if (!config[requiredField]) {
@ -160,7 +162,9 @@
</div>
{#if $highlightedItem !== undefined}
<FloatOver on:close={() => highlightedItem.setData(undefined)}>
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} />
<div class="mt-16">
<TagRenderingInput path={$highlightedItem.path} {state} schema={$highlightedItem.schema} />
</div>
</FloatOver>
{/if}

View file

@ -3,7 +3,6 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import {
Conversion,
ConversionContext,
ConversionMessage,
DesugaringContext,
Pipe,
@ -21,6 +20,7 @@ import { Feature, Point } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson"
import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext";
export interface HighlightedTagRendering {
path: ReadonlyArray<string | number>
@ -41,7 +41,9 @@ export abstract class EditJsonState<T> {
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
undefined
)
sendingUpdates = false
private readonly _stores = new Map<string, UIEventSource<any>>()
private boolean
constructor(schema: ConfigMeta[], server: StudioServer, category: "layers" | "themes") {
this.schema = schema
@ -52,7 +54,13 @@ export abstract class EditJsonState<T> {
const layerId = this.getId()
this.configuration
.mapD((config) => JSON.stringify(config, null, " "))
.mapD((config) => {
if (!this.sendingUpdates) {
console.log("Not sending updates yet! Trigger 'startSendingUpdates' first")
return undefined
}
return JSON.stringify(config, null, " ")
})
.stabilized(100)
.addCallbackD(async (config) => {
const id = layerId.data
@ -60,10 +68,17 @@ export abstract class EditJsonState<T> {
console.warn("No id found in layer, not updating")
return
}
await server.update(id, config, category)
await this.server.update(id, config, this.category)
})
}
public startSavingUpdates(enabled = true) {
this.sendingUpdates = enabled
if (enabled) {
this.configuration.ping()
}
}
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
// Walk the path down to see if we find something
let entry = this.configuration.data
@ -96,7 +111,7 @@ export abstract class EditJsonState<T> {
public register(
path: ReadonlyArray<string | number>,
value: Store<any>,
noInitialSync: boolean = false
noInitialSync: boolean = true
): () => void {
const unsync = value.addCallback((v) => {
this.setValueAt(path, v)
@ -260,6 +275,18 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
}
this.addMissingTagRenderingIds()
this.configuration.addCallbackAndRunD((layer) => {
if (layer.tagRenderings) {
// A bit of cleanup
const lBefore = layer.tagRenderings.length
const cleaned = Utils.NoNull(layer.tagRenderings)
if (cleaned.length != lBefore) {
layer.tagRenderings = cleaned
this.configuration.ping()
}
}
})
}
protected buildValidation(state: DesugaringContext) {
@ -300,6 +327,10 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
}
export class EditThemeState extends EditJsonState<LayoutConfigJson> {
constructor(schema: ConfigMeta[], server: StudioServer) {
super(schema, server, "themes")
}
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
return new Pipe(
new PrepareTheme(state),
@ -307,10 +338,6 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
)
}
constructor(schema: ConfigMeta[], server: StudioServer) {
super(schema, server, "themes")
}
protected getId(): Store<string> {
return this.configuration.mapD((config) => config.id)
}

View file

@ -10,8 +10,8 @@
export let state: EditThemeState;
let schema: ConfigMeta[] = state.schema.filter(schema => schema.path.length > 0);
let config = state.configuration;
const messages = state.messages;
const hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
let messages = state.messages;
let hasErrors = messages.map((m: ConversionMessage[]) => m.filter(m => m.level === "error").length);
let title = state.getStoreFor(["id"]);
const wl = window.location;
const baseUrl = wl.protocol + "//" + wl.host + "/theme.html?userlayout=";

View file

@ -5,12 +5,13 @@
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 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";
import { Utils } from "../../Utils";
export let state: EditLayerState;
export let path: ReadonlyArray<string | number>;
@ -34,9 +35,15 @@
return [x];
}
});
let configs: Store<TagRenderingConfig[]> = configJson.mapD(configs => configs.map(config => new TagRenderingConfig(config)));
let configs: Store<TagRenderingConfig[]> =configJson.mapD(configs => Utils.NoNull( configs.map(config => {
try{
return new TagRenderingConfig(config);
}catch (e) {
return undefined
}
})));
let id: Store<string> = value.mapD(c => {
if (c.id) {
if (c?.id) {
return c.id;
}
if (typeof c === "string") {
@ -49,6 +56,14 @@
let messages = state.messagesFor(path);
let description = schema.description
if(description){
try{
description = nmd(description)
}catch (e) {
console.error("Could not convert description to markdown", {description})
}
}
</script>
<div class="flex">
@ -63,8 +78,8 @@
{schema.hints.question}
{/if}
</button>
{#if schema.description}
<FromHtml src={nmd(schema.description)} />
{#if description}
<FromHtml src={description} />
{/if}
{#each $messages as message}
<div class="alert">

View file

@ -16,6 +16,7 @@
export let state: EditLayerState;
export let path: (string | number)[] = [];
export let schema: ConfigMeta;
export let startInEditModeIfUnset: boolean = false
let value = new UIEventSource<string | any>(undefined);
const isTranslation = schema.hints.typehint === "translation" || schema.hints.typehint === "rendered" || ConfigMetaUtils.isTranslation(schema);
@ -118,6 +119,7 @@
}
let startValue = state.getCurrentValueFor(path);
const tags = new UIEventSource<Record<string, string>>({ value: startValue });
let startInEditMode = !startValue && startInEditModeIfUnset
try {
onDestroy(state.register(path, tags.map(tgs => {
const v = tgs["value"];
@ -157,7 +159,7 @@
<span class="alert">{err}</span>
{:else}
<div class="w-full flex flex-col">
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
<TagRenderingEditable editMode={startInEditMode} {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags} />
{#if $messages.length > 0}
{#each $messages as msg}
<div class="alert">{msg.message}</div>

View file

@ -149,7 +149,7 @@
}
return tags["value"] === "true";
});
onDestroy(state.register(path, directValue, true));
onDestroy(state.register(path, directValue));
}
let subSchemas: ConfigMeta[] = [];

View file

@ -72,6 +72,7 @@ const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson
const tags = new UIEventSource({ value });
const store = state.getStoreFor(path);
tags.addCallbackAndRunD(tgs => {
store.setData(tgs["value"]);
@ -112,7 +113,7 @@ const missing: string[] = questionableTagRenderingSchemaRaw.filter(schema => sch
<slot name="upper-right" />
</div>
{#if $allowQuestions}
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
<SchemaBasedField startInEditModeIfUnset={true} {state} path={[...path,"question"]} schema={topLevelItems["question"]} />
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]} />
{/if}
{#each ($mappings ?? []) as mapping, i (mapping)}