Studio: improve error handling, fix renumbering

This commit is contained in:
Pieter Vander Vennet 2024-01-19 17:31:35 +01:00
parent 7afe58e6a5
commit 079a3f8694
10 changed files with 187 additions and 62 deletions

View file

@ -180,7 +180,7 @@
<div slot="title4" class="flex">
Advanced functionality
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced", "expert")} {state} />
<ErrorIndicatorForRegion firstPaths={firstPathsFor("advanced")} {state} />
</div>
<div slot="content4">
<Region configs={perRegion["advanced"]} {state} />

View file

@ -22,6 +22,7 @@ import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson
import { PrepareTheme } from "../../Models/ThemeConfig/Conversion/PrepareTheme"
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
export interface HighlightedTagRendering {
path: ReadonlyArray<string | number>
@ -66,7 +67,6 @@ export abstract class EditJsonState<T> {
this.messages = this.setupErrorsForLayers()
const layerId = this.getId()
this.highlightedItem.addCallbackD((hl) => console.log("Highlighted item is", hl))
this.configuration
.mapD((config) => {
if (!this.sendingUpdates) {
@ -110,6 +110,7 @@ export abstract class EditJsonState<T> {
public async delete() {
await this.server.delete(this.getId().data, this.category)
}
public getStoreFor<T>(path: ReadonlyArray<string | number>): UIEventSource<T | undefined> {
const key = path.join(".")
@ -172,7 +173,6 @@ export abstract class EditJsonState<T> {
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
let entry = this.configuration.data
console.trace("Setting value at", path,"to",v)
const isUndefined =
v === undefined ||
v === null ||
@ -249,6 +249,62 @@ export abstract class EditJsonState<T> {
}
}
class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
private readonly _step: Conversion<LayerConfigJson, T>
private readonly _state: DesugaringContext
private readonly _getTagRenderings: (t: T) => TagRenderingConfigJson[]
constructor(
state: DesugaringContext,
step: Conversion<LayerConfigJson, T>,
getTagRenderings: (t: T) => TagRenderingConfigJson[]
) {
super(
"When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this",
[],
"ContextRewritingStep"
)
this._state = state
this._step = step
this._getTagRenderings = getTagRenderings
}
convert(json: LayerConfigJson, context: ConversionContext): T {
const converted = this._step.convert(json, context)
const originalIds = json.tagRenderings?.map(
(tr) => (<QuestionableTagRenderingConfigJson>tr)["id"]
)
if (!originalIds) {
return converted
}
let newTagRenderings: TagRenderingConfigJson[]
if (converted === undefined) {
const prepared = new PrepareLayer(this._state)
newTagRenderings = <TagRenderingConfigJson[]>(
prepared.convert(json, context).tagRenderings
)
} else {
newTagRenderings = this._getTagRenderings(converted)
}
context.rewriteMessages((path) => {
if (path[0] !== "tagRenderings") {
return undefined
}
const newPath = [...path]
const idToSearch = newTagRenderings[newPath[1]].id
const oldIndex = originalIds.indexOf(idToSearch)
if (oldIndex < 0) {
console.warn("Original ID was not found: ", idToSearch)
return undefined // We don't modify the message
}
newPath[1] = oldIndex
return newPath
})
return converted
}
}
export default class EditLayerState extends EditJsonState<LayerConfigJson> {
// Needed for the special visualisations
public readonly osmConnection: OsmConnection
@ -334,9 +390,10 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
}
protected buildValidation(state: DesugaringContext) {
return new Pipe(
new PrepareLayer(state),
new ValidateLayer("dynamic", false, undefined, true)
return new ContextRewritingStep(
state,
new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)),
(t) => <TagRenderingConfigJson[]>t.raw.tagRenderings
)
}

View file

@ -2,7 +2,7 @@
import EditLayerState from "./EditLayerState"
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"
export let firstPaths: Set<string>
export let firstPaths: Set<string | number>
export let state: EditLayerState
let messagesCount = state.messages.map(
(msgs) =>

View file

@ -11,9 +11,11 @@
import { Utils } from "../../Utils"
import ToSvelte from "../Base/ToSvelte.svelte"
import { VariableUiElement } from "../Base/VariableUIElement"
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
export let state: EditLayerState
export let path: (string | number)[]
let messages = state.messagesFor(path)
let tag: UIEventSource<TagConfigJson> = state.getStoreFor([...path, "if"])
let parsedTag = tag.map((t) => (t ? TagUtils.Tag(t) : undefined))
let exampleTags = parsedTag.map((pt) => {
@ -27,7 +29,6 @@
}
return o
})
let uploadableOnly: boolean = true
let thenText: UIEventSource<Record<string, string>> = state.getStoreFor([...path, "then"])
let thenTextEn = thenText.mapD((translation) =>
@ -71,5 +72,11 @@
<i>No then is set</i>
{/if}
<FromHtml src={$parsedTag?.asHumanString(false, false, $exampleTags)} />
{#if $messages.length > 0}
<div class="alert m-2 flex">
<ExclamationTriangle class="w-6 h-6"/>
{$messages.length} errors
</div>
{/if}
</div>
{/if}

View file

@ -2,6 +2,7 @@ import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { Store } from "../../Logic/UIEventSource"
import { LayoutConfigJson } from "../../Models/ThemeConfig/Json/LayoutConfigJson"
export default class StudioServer {
private readonly url: string
@ -47,11 +48,13 @@ export default class StudioServer {
return layerOverview
}
async fetch(layerId: string, category: "layers", uid?: number): Promise<LayerConfigJson>
async fetch(layerId: string, category: "themes", uid?: number): Promise<LayoutConfigJson>
async fetch(
layerId: string,
category: "layers" | "themes",
uid?: number
): Promise<LayerConfigJson> {
): Promise<LayerConfigJson | LayoutConfigJson> {
try {
return await Utils.downloadJson(this.urlFor(layerId, category, uid))
} catch (e) {

View file

@ -24,13 +24,13 @@
import { onMount } from "svelte"
export let state: EditLayerState
export let schema: ConfigMeta
export let path: (string | number)[]
export let path: ReadonlyArray<string | number>
let messages = state.messagesFor(path)
let expertMode = state.expertMode
const store = state.getStoreFor(path)
let value = store.data
let hasSeenIntro = UIEventSource.asBoolean(
LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false")
LocalStorageSource.Get("studio-seen-tagrendering-tutorial", "false"),
)
onMount(() => {
if (!hasSeenIntro.data) {
@ -43,7 +43,7 @@
* Should only be enabled for 'tagrenderings' in the theme, if the source is OSM
*/
let allowQuestions: Store<boolean> = state.configuration.mapD(
(config) => path.at(0) === "tagRenderings" && config.source?.geoJson === undefined
(config) => path.at(0) === "tagRenderings" && config.source?.["geoJson"] === undefined,
)
let mappingsBuiltin: MappingConfigJson[] = []
@ -119,7 +119,7 @@
const freeformSchemaAll = <ConfigMeta[]>(
questionableTagRenderingSchemaRaw.filter(
(schema) => schema.path.length == 2 && schema.path[0] === "freeform" && $allowQuestions
(schema) => schema.path.length == 2 && schema.path[0] === "freeform" && $allowQuestions,
)
)
let freeformSchema = $expertMode
@ -128,7 +128,7 @@
const missing: string[] = questionableTagRenderingSchemaRaw
.filter(
(schema) =>
schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0])
schema.path.length >= 1 && !items.has(schema.path[0]) && !ignored.has(schema.path[0]),
)
.map((schema) => schema.path.join("."))
console.log({ state })
@ -164,7 +164,7 @@
{/if}
{#each $mappings ?? [] as mapping, i (mapping)}
<div class="interactive flex w-full">
<MappingInput {state} path={path.concat(["mappings", i])}>
<MappingInput {state} path={[...path, "mappings", i]}>
<button
slot="delete"
class="no-image-background rounded-full"
@ -178,6 +178,7 @@
</button>
</MappingInput>
</div>
{/each}
<button