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

@ -22,11 +22,14 @@
export let highlightedRendering: UIEventSource<string> = undefined;
export let showQuestionIfUnknown: boolean = false;
let editMode = false;
/**
* Indicates if this tagRendering currently shows the attribute or asks the question to _change_ the property
*/
export let editMode = !config.IsKnown(tags) || showQuestionIfUnknown;
if (tags) {
onDestroy(
tags.addCallbackAndRunD((tags) => {
editMode = showQuestionIfUnknown && !config.IsKnown(tags);
tags.addCallbackD((tags) => {
editMode = !config.IsKnown(tags)
})
);
}

View file

@ -132,6 +132,7 @@
function onSave() {
if (selectedTags === undefined) {
console.log("SelectedTags is undefined, ignoring 'onSave'-event")
return;
}
if (layer === undefined || layer?.source === null) {
@ -197,20 +198,20 @@
</span>
<slot name="upper-right" />
</div>
{#if config.questionhint}
<div>
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
</div>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full">
<img src="./assets/svg/search.svg" class="h-6 w-6" />

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

View file

@ -24,8 +24,9 @@
import { QuestionMarkCircleIcon } from "@babeard/svelte-heroicons/mini";
import type { ConfigMeta } from "./Studio/configMeta";
import EditTheme from "./Studio/EditTheme.svelte";
export let studioUrl = window.location.hostname === "127.0.0.1" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
import * as meta from "../../package.json"
export let studioUrl = window.location.hostname === "127.0.0.2" ? "http://127.0.0.1:1235" : "https://studio.mapcomplete.org";
let osmConnection = new OsmConnection(new OsmConnection({
oauth_token: QueryParameters.GetQueryParameter(
@ -61,18 +62,22 @@
let layerId = editLayerState.configuration.map(layerConfig => layerConfig.id);
let showIntro = UIEventSource.asBoolean(LocalStorageSource.Get("studio-show-intro", "true"));
const version = meta.version
async function editLayer(event: Event) {
const layerId: {owner: number, id: string} = event.detail;
state = "loading";
editLayerState.startSavingUpdates(false)
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner));
editLayerState.startSavingUpdates()
state = "editing_layer";
}
async function editTheme(event: Event) {
const id : {id: string, owner: number} = event.detail;
state = "loading";
editThemeState.startSavingUpdates(false)
editThemeState.configuration.setData(await studio.fetch(id.id, "themes", id.owner));
editThemeState.startSavingUpdates()
state = "editing_theme";
}
@ -153,6 +158,7 @@
Show the introduction again
</NextButton>
</div>
<span class="subtle">MapComplete version {version}</span>
</div>
{:else if state === "edit_layer"}
@ -160,14 +166,14 @@
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
<h2>Choose a layer to edit</h2>
<ChooseLayerToEdit layerIds={$selfLayers} on:layerSelected={editLayer}>
<ChooseLayerToEdit {osmConnection} layerIds={$selfLayers} on:layerSelected={editLayer}>
<h3 slot="title">Your layers</h3>
</ChooseLayerToEdit>
<h3>Layers by other contributors</h3>
<ChooseLayerToEdit layerIds={$otherLayers} on:layerSelected={editLayer} />
<ChooseLayerToEdit {osmConnection} layerIds={$otherLayers} on:layerSelected={editLayer} />
<h3>Official layers by MapComplete</h3>
<ChooseLayerToEdit layerIds={$officialLayers} on:layerSelected={editLayer} />
<ChooseLayerToEdit {osmConnection} layerIds={$officialLayers} on:layerSelected={editLayer} />
</div>
{:else if state === "edit_theme"}
@ -175,13 +181,13 @@
<BackButton clss="small p-1" imageClass="w-8 h-8" on:click={() => {state =undefined}}>MapComplete Studio
</BackButton>
<h2>Choose a theme to edit</h2>
<ChooseLayerToEdit layerIds={$selfThemes} on:layerSelected={editTheme}>
<ChooseLayerToEdit {osmConnection} layerIds={$selfThemes} on:layerSelected={editTheme}>
<h3 slot="title">Your themes</h3>
</ChooseLayerToEdit>
<h3>Themes by other contributors</h3>
<ChooseLayerToEdit layerIds={$otherThemes} on:layerSelected={editTheme} />
<ChooseLayerToEdit {osmConnection} layerIds={$otherThemes} on:layerSelected={editTheme} />
<h3>Official themes by MapComplete</h3>
<ChooseLayerToEdit layerIds={$officialThemes} on:layerSelected={editTheme} />
<ChooseLayerToEdit {osmConnection} layerIds={$officialThemes} on:layerSelected={editTheme} />
</div>
{:else if state === "loading"}

View file

@ -59,7 +59,6 @@ export class Translation extends BaseUIElement {
"Constructing a translation, but the object containing translations is empty " +
(context ?? "No context given")
)
throw `Constructing a translation, but the object containing translations is empty (${context})`
}
}