From 5d1c93396d5a3935102182f060395e608bf17276 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 2 Aug 2024 19:06:14 +0200 Subject: [PATCH] Add various improvements and fixes to studio, should fix #2055 --- public/css/index-tailwind-output.css | 18 +- src/Logic/State/UserSettingsMetaTagging.ts | 48 +-- .../ThemeConfig/Conversion/Conversion.ts | 7 +- .../ThemeConfig/Conversion/PrepareTheme.ts | 2 +- .../ThemeConfig/Conversion/Validation.ts | 310 +++++++++--------- .../ThemeConfig/Json/LayoutConfigJson.ts | 2 + src/UI/Flowbite/AccordionSingle.svelte | 2 +- .../TagRendering/TagRenderingQuestion.svelte | 3 - .../CollapsedTagRenderingPreview.svelte | 207 ++++++++++++ src/UI/Studio/EditLayerState.ts | 49 +-- src/UI/Studio/QuestionPreview.svelte | 47 ++- src/UI/Studio/RawEditor.svelte | 46 ++- src/UI/Studio/Region.svelte | 7 +- src/UI/Studio/SchemaBasedArray.svelte | 171 +--------- src/UI/Studio/SchemaBasedField.svelte | 4 +- src/UI/Studio/SchemaBasedInput.svelte | 7 +- src/UI/Studio/SchemaBasedMultiType.svelte | 4 +- src/UI/StudioGUI.svelte | 4 +- src/index.css | 11 + 19 files changed, 531 insertions(+), 418 deletions(-) create mode 100644 src/UI/Studio/CollapsedTagRenderingPreview.svelte diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 5666d26c7..4ff2caa99 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -993,10 +993,6 @@ video { margin-right: 4rem; } -.mb-4 { - margin-bottom: 1rem; -} - .mt-4 { margin-top: 1rem; } @@ -1029,6 +1025,10 @@ video { margin-right: 0.25rem; } +.mb-4 { + margin-bottom: 1rem; +} + .ml-1 { margin-left: 0.25rem; } @@ -4686,6 +4686,16 @@ textarea { color: black; } +h2.group { + /* For flowbite accordions */ + margin: 0; +} + +.group button { + /* For flowbite accordions */ + border-radius: 0; +} + /************************* OTHER CATEGORIES ********************************/ /** diff --git a/src/Logic/State/UserSettingsMetaTagging.ts b/src/Logic/State/UserSettingsMetaTagging.ts index 6e568c5c3..33a5ae85b 100644 --- a/src/Logic/State/UserSettingsMetaTagging.ts +++ b/src/Logic/State/UserSettingsMetaTagging.ts @@ -1,42 +1,14 @@ import { Utils } from "../../Utils" /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ export class ThemeMetaTagging { - public static readonly themeName = "usersettings" + public static readonly themeName = "usersettings" - public metaTaggging_for_usersettings(feat: { properties: Record }) { - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => - feat.properties._description - .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) - ?.at(1) - ) - Utils.AddLazyProperty( - feat.properties, - "_d", - () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.href.match(/mastodon|en.osm.town/) !== null - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => - ((feat) => { - const e = document.createElement("div") - e.innerHTML = feat.properties._d - return Array.from(e.getElementsByTagName("a")).filter( - (a) => a.getAttribute("rel")?.indexOf("me") >= 0 - )[0]?.href - })(feat) - ) - Utils.AddLazyProperty( - feat.properties, - "_mastodon_candidate", - () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a - ) - feat.properties["__current_backgroun"] = "initial_value" - } -} + public metaTaggging_for_usersettings(feat: {properties: Record}) { + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) ) + Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) ) + Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a ) + feat.properties['__current_backgroun'] = 'initial_value' + } +} \ No newline at end of file diff --git a/src/Models/ThemeConfig/Conversion/Conversion.ts b/src/Models/ThemeConfig/Conversion/Conversion.ts index 1c8dc641c..9285659cd 100644 --- a/src/Models/ThemeConfig/Conversion/Conversion.ts +++ b/src/Models/ThemeConfig/Conversion/Conversion.ts @@ -73,15 +73,20 @@ export abstract class DesugaringStep extends Conversion {} export class Pipe extends Conversion { private readonly _step0: Conversion private readonly _step1: Conversion + private readonly _failfast: boolean - constructor(step0: Conversion, step1: Conversion) { + constructor(step0: Conversion, step1: Conversion, failfast = false) { super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`) this._step0 = step0 this._step1 = step1 + this._failfast = failfast } convert(json: TIn, context: ConversionContext): TOut { const r0 = this._step0.convert(json, context.inOperation(this._step0.name)) + if(context.hasErrors() && this._failfast){ + return undefined + } return this._step1.convert(r0, context.inOperation(this._step1.name)) } } diff --git a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts index dec7a7109..12c5a60a0 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -21,6 +21,7 @@ import DependencyCalculator from "../DependencyCalculator" import { AddContextToTranslations } from "./AddContextToTranslations" import ValidationUtils from "./ValidationUtils" import { ConversionContext } from "./ConversionContext" +import { PrevalidateTheme } from "./Validation" class SubstituteLayer extends Conversion { private readonly _state: DesugaringContext @@ -664,7 +665,6 @@ export class PrepareTheme extends Fuse { ) { super( "Fully prepares and expands a theme", - new AddContextToTranslationsInLayout(), new PreparePersonalTheme(state), new WarnForUnsubstitutedLayersInTheme(), diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 6769b1bf1..f0f860c51 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -36,7 +36,7 @@ class ValidateLanguageCompleteness extends DesugaringStep { super( "Checks that the given object is fully translated in the specified languages", [], - "ValidateLanguageCompleteness" + "ValidateLanguageCompleteness", ) this._languages = languages ?? ["en"] } @@ -50,18 +50,18 @@ class ValidateLanguageCompleteness extends DesugaringStep { .filter( (t) => t.tr.translations[neededLanguage] === undefined && - t.tr.translations["*"] === undefined + t.tr.translations["*"] === undefined, ) .forEach((missing) => { context .enter(missing.context.split(".")) .err( `The theme ${obj.id} should be translation-complete for ` + - neededLanguage + - ", but it lacks a translation for " + - missing.context + - ".\n\tThe known translation is " + - missing.tr.textFor("en") + neededLanguage + + ", but it lacks a translation for " + + missing.context + + ".\n\tThe known translation is " + + missing.tr.textFor("en"), ) }) } @@ -78,7 +78,7 @@ export class DoesImageExist extends DesugaringStep { constructor( knownImagePaths: Set, checkExistsSync: (path: string) => boolean = undefined, - ignore?: Set + ignore?: Set, ) { super("Checks if an image exists", [], "DoesImageExist") this._ignore = ignore @@ -114,15 +114,15 @@ export class DoesImageExist extends DesugaringStep { if (!this._knownImagePaths.has(image)) { if (this.doesPathExist === undefined) { context.err( - `Image with path ${image} not found or not attributed; it is used in ${context}` + `Image with path ${image} not found or not attributed; it is used in ${context}`, ) } else if (!this.doesPathExist(image)) { context.err( - `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.` + `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`, ) } else { context.err( - `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info` + `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`, ) } } @@ -146,7 +146,7 @@ export class ValidateTheme extends DesugaringStep { doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, - sharedTagRenderings?: Set + sharedTagRenderings?: Set, ) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme") this._validateImage = doesImageExist @@ -165,15 +165,15 @@ export class ValidateTheme extends DesugaringStep { if (json["units"] !== undefined) { context.err( "The theme " + - json.id + - " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " + json.id + + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ", ) } if (json["roamingRenderings"] !== undefined) { context.err( "Theme " + - json.id + - " contains an old 'roamingRenderings'. Use an 'overrideAll' instead" + json.id + + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead", ) } } @@ -191,10 +191,10 @@ export class ValidateTheme extends DesugaringStep { for (const remoteImage of remoteImages) { context.err( "Found a remote image: " + - remoteImage.path + - " in theme " + - json.id + - ", please download it." + remoteImage.path + + " in theme " + + json.id + + ", please download it.", ) } for (const image of images) { @@ -210,17 +210,17 @@ export class ValidateTheme extends DesugaringStep { const filename = this._path.substring( this._path.lastIndexOf("/") + 1, - this._path.length - 5 + this._path.length - 5, ) if (theme.id !== filename) { context.err( "Theme ids should be the same as the name.json, but we got id: " + - theme.id + - " and filename " + - filename + - " (" + - this._path + - ")" + theme.id + + " and filename " + + filename + + " (" + + this._path + + ")", ) } this._validateImage.convert(theme.icon, context.enter("icon")) @@ -228,13 +228,13 @@ export class ValidateTheme extends DesugaringStep { const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"])) if (dups.length > 0) { context.err( - `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` + `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`, ) } if (json["mustHaveLanguage"] !== undefined) { new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert( theme, - context + context, ) } if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) { @@ -242,7 +242,7 @@ export class ValidateTheme extends DesugaringStep { const targetLanguage = theme.title.SupportedLanguages()[0] if (targetLanguage !== "en") { context.err( - `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key` + `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`, ) } @@ -286,7 +286,7 @@ export class ValidateTheme extends DesugaringStep { .err( `This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby .slice(0, 5) - .join(", ")}` + .join(", ")}`, ) } } @@ -309,7 +309,7 @@ export class ValidateThemeAndLayers extends Fuse { doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, - sharedTagRenderings?: Set + sharedTagRenderings?: Set, ) { super( "Validates a theme and the contained layers", @@ -319,10 +319,10 @@ export class ValidateThemeAndLayers extends Fuse { new Each( new Bypass( (layer) => Constants.added_by_default.indexOf(layer.id) < 0, - new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true) - ) - ) - ) + new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true), + ), + ), + ), ) } } @@ -332,7 +332,7 @@ class OverrideShadowingCheck extends DesugaringStep { super( "Checks that an 'overrideAll' does not override a single override", [], - "OverrideShadowingCheck" + "OverrideShadowingCheck", ) } @@ -378,6 +378,9 @@ class MiscThemeChecks extends DesugaringStep { if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) { context.err("The theme " + json.id + " has no 'layers' defined") } + if (!Array.isArray(json.layers)) { + context.enter("layers").err("The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?") + } if (json.socialImage === "") { context.warn("Social image for theme " + json.id + " is the emtpy string") } @@ -406,7 +409,7 @@ class MiscThemeChecks extends DesugaringStep { context .enter("overideAll") .err( - "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them." + "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.", ) } return json @@ -418,7 +421,7 @@ export class PrevalidateTheme extends Fuse { super( "Various consistency checks on the raw JSON", new MiscThemeChecks(), - new OverrideShadowingCheck() + new OverrideShadowingCheck(), ) } } @@ -428,7 +431,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep ["_abc"] */ private static extractCalculatedTagNames( - layerConfig?: LayerConfigJson | { calculatedTags: string[] } + layerConfig?: LayerConfigJson | { calculatedTags: string[] }, ) { return ( layerConfig?.calculatedTags?.map((ct) => { @@ -728,16 +731,16 @@ export class DetectShadowedMappings extends DesugaringStep\` instead. The images found are ${images.join( - ", " - )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged` + ", ", + )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`, ) } else { ctx.info( `Ignored image ${images.join( - ", " - )} in 'then'-clause of a mapping as this check has been disabled` + ", ", + )} in 'then'-clause of a mapping as this check has been disabled`, ) for (const image of images) { @@ -832,7 +835,7 @@ class ValidatePossibleLinks extends DesugaringStep does have `rel='noopener'` set", [], - "ValidatePossibleLinks" + "ValidatePossibleLinks", ) } @@ -862,21 +865,21 @@ class ValidatePossibleLinks extends DesugaringStep, - context: ConversionContext + context: ConversionContext, ): string | Record { if (typeof json === "string") { if (this.isTabnabbingProne(json)) { context.err( "The string " + - json + - " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping" + json + + " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping", ) } } else { for (const k in json) { if (this.isTabnabbingProne(json[k])) { context.err( - `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping` + `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`, ) } } @@ -894,7 +897,7 @@ class CheckTranslation extends DesugaringStep { super( "Checks that a translation is valid and internally consistent", ["*"], - "CheckTranslation" + "CheckTranslation", ) this._allowUndefined = allowUndefined } @@ -935,6 +938,7 @@ class CheckTranslation extends DesugaringStep { class MiscTagRenderingChecks extends DesugaringStep { private readonly _layerConfig: LayerConfigJson + constructor(layerConfig?: LayerConfigJson) { super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks") this._layerConfig = layerConfig @@ -942,17 +946,17 @@ class MiscTagRenderingChecks extends DesugaringStep { convert( json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson, - context: ConversionContext + context: ConversionContext, ): TagRenderingConfigJson { if (json["special"] !== undefined) { context.err( - 'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`' + "Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`", ) } if (Object.keys(json).length === 1 && typeof json["render"] === "string") { context.warn( - `use the content directly instead of {render: ${JSON.stringify(json["render"])}}` + `use the content directly instead of {render: ${JSON.stringify(json["render"])}}`, ) } @@ -964,7 +968,7 @@ class MiscTagRenderingChecks extends DesugaringStep { const mapping: MappingConfigJson = json.mappings[i] CheckTranslation.noUndefined.convert( mapping.then, - context.enters("mappings", i, "then") + context.enters("mappings", i, "then"), ) if (!mapping.if) { console.log( @@ -973,7 +977,7 @@ class MiscTagRenderingChecks extends DesugaringStep { "if", mapping.if, context.path.join("."), - mapping.then + mapping.then, ) context.enters("mappings", i, "if").err("No `if` is defined") } @@ -983,7 +987,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enters("mappings", i, "addExtraTags", j) .err( - "Detected a 'null' or 'undefined' value. Either specify a tag or delete this item" + "Detected a 'null' or 'undefined' value. Either specify a tag or delete this item", ) } } @@ -994,18 +998,18 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enters("mappings", i, "then") .warn( - "A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' without the question, resulting in a weird phrasing in the information box" + "A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' without the question, resulting in a weird phrasing in the information box", ) } } } if (json["group"]) { - context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead') + context.err("Groups are deprecated, use `\"label\": [\"" + json["group"] + "\"]` instead") } if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) { context.err( - "A question is defined, but no mappings nor freeform (key) are. Add at least one of them" + "A question is defined, but no mappings nor freeform (key) are. Add at least one of them", ) } if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) { @@ -1015,7 +1019,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("questionHint") .err( - "A questionHint is defined, but no question is given. As such, the questionHint will never be shown" + "A questionHint is defined, but no question is given. As such, the questionHint will never be shown", ) } @@ -1023,7 +1027,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enters("icon", "size") .err( - "size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`" + "size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`", ) } @@ -1033,10 +1037,10 @@ class MiscTagRenderingChecks extends DesugaringStep { .enter("render") .err( "This tagRendering allows to set a value to key " + - json.freeform.key + - ", but does not define a `render`. Please, add a value here which contains `{" + - json.freeform.key + - "}`" + json.freeform.key + + ", but does not define a `render`. Please, add a value here which contains `{" + + json.freeform.key + + "}`", ) } else { const render = new Translation(json.render) @@ -1067,7 +1071,7 @@ class MiscTagRenderingChecks extends DesugaringStep { const keyFirstArg = ["canonical", "fediverse_link", "translated"] if ( keyFirstArg.some( - (funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0 + (funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0, ) ) { continue @@ -1091,7 +1095,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("render") .err( - `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?` + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`, ) continue } @@ -1103,7 +1107,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("render") .err( - `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}` + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`, ) continue } @@ -1111,7 +1115,7 @@ class MiscTagRenderingChecks extends DesugaringStep { context .enter("render") .err( - `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}` + `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}`, ) } } @@ -1126,22 +1130,22 @@ class MiscTagRenderingChecks extends DesugaringStep { .enters("freeform", "type") .err( "No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " + - tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; ") + tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; "), ) } } else if (json.freeform.type === "nsi") { context .enters("freeform", "type") .warn( - "No need to explicitly set type to 'NSI', autodetected based on freeform type" + "No need to explicitly set type to 'NSI', autodetected based on freeform type", ) } } if (json.render && json["question"] && json.freeform === undefined) { context.err( `Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation( - json["question"] - ).textFor("en")}` + json["question"], + ).textFor("en")}`, ) } @@ -1152,9 +1156,9 @@ class MiscTagRenderingChecks extends DesugaringStep { .enters("freeform", "type") .err( "Unknown type: " + - freeformType + - "; try one of " + - Validators.availableTypes.join(", ") + freeformType + + "; try one of " + + Validators.availableTypes.join(", "), ) } } @@ -1192,7 +1196,7 @@ export class ValidateTagRenderings extends Fuse { new On("question", new ValidatePossibleLinks()), new On("questionHint", new ValidatePossibleLinks()), new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))), - new MiscTagRenderingChecks(layerConfig) + new MiscTagRenderingChecks(layerConfig), ) } } @@ -1211,7 +1215,7 @@ export class PrevalidateLayer extends DesugaringStep { path: string, isBuiltin: boolean, doesImageExist: DoesImageExist, - studioValidations: boolean + studioValidations: boolean, ) { super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer") this._path = path @@ -1237,7 +1241,7 @@ export class PrevalidateLayer extends DesugaringStep { context .enter("source") .err( - "No source section is defined; please define one as data is not loaded otherwise" + "No source section is defined; please define one as data is not loaded otherwise", ) } else { if (json.source === "special" || json.source === "special:library") { @@ -1245,7 +1249,7 @@ export class PrevalidateLayer extends DesugaringStep { context .enters("source", "osmTags") .err( - "No osmTags defined in the source section - these should always be present, even for geojson layer" + "No osmTags defined in the source section - these should always be present, even for geojson layer", ) } else { const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags") @@ -1254,7 +1258,7 @@ export class PrevalidateLayer extends DesugaringStep { .enters("source", "osmTags") .err( "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + - osmTags.asHumanString(false, false, {}) + osmTags.asHumanString(false, false, {}), ) } } @@ -1280,10 +1284,10 @@ export class PrevalidateLayer extends DesugaringStep { .enter("syncSelection") .err( "Invalid sync-selection: must be one of " + - LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") + - " but got '" + - json.syncSelection + - "'" + LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") + + " but got '" + + json.syncSelection + + "'", ) } if (json["pointRenderings"]?.length > 0) { @@ -1302,7 +1306,7 @@ export class PrevalidateLayer extends DesugaringStep { } json.pointRendering?.forEach((pr, i) => - this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)) + this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)), ) if (json["mapRendering"]) { @@ -1319,8 +1323,8 @@ export class PrevalidateLayer extends DesugaringStep { if (!Constants.priviliged_layers.find((x) => x == json.id)) { context.err( "Layer " + - json.id + - " uses 'special' as source.osmTags. However, this layer is not a priviliged layer" + json.id + + " uses 'special' as source.osmTags. However, this layer is not a priviliged layer", ) } } @@ -1335,19 +1339,19 @@ export class PrevalidateLayer extends DesugaringStep { context .enter("title") .err( - "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error." + "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.", ) } if (json.title === null) { context.info( - "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." + "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.", ) } { // Check for multiple, identical builtin questions - usability for studio users const duplicates = Utils.Duplicates( - json.tagRenderings.filter((tr) => typeof tr === "string") + json.tagRenderings.filter((tr) => typeof tr === "string"), ) for (let i = 0; i < json.tagRenderings.length; i++) { const tagRendering = json.tagRenderings[i] @@ -1377,7 +1381,7 @@ export class PrevalidateLayer extends DesugaringStep { { // duplicate ids in tagrenderings check const duplicates = Utils.NoNull( - Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) + Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))), ) if (duplicates.length > 0) { // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list @@ -1385,11 +1389,11 @@ export class PrevalidateLayer extends DesugaringStep { .enter("tagRenderings") .err( "Some tagrenderings have a duplicate id: " + - duplicates.join(", ") + - "\n" + - JSON.stringify( - json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0) - ) + duplicates.join(", ") + + "\n" + + JSON.stringify( + json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0), + ), ) } } @@ -1422,8 +1426,8 @@ export class PrevalidateLayer extends DesugaringStep { if (json["overpassTags"] !== undefined) { context.err( "Layer " + - json.id + - 'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": }\' instead of "overpassTags": (note: this isn\'t your fault, the custom theme generator still spits out the old format)' + json.id + + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": }' instead of \"overpassTags\": (note: this isn't your fault, the custom theme generator still spits out the old format)", ) } const forbiddenTopLevel = [ @@ -1443,7 +1447,7 @@ export class PrevalidateLayer extends DesugaringStep { } if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) { context.err( - "Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'" + "Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'", ) } @@ -1460,9 +1464,9 @@ export class PrevalidateLayer extends DesugaringStep { if (this._path != undefined && this._path.indexOf(expected) < 0) { context.err( "Layer is in an incorrect place. The path is " + - this._path + - ", but expected " + - expected + this._path + + ", but expected " + + expected, ) } } @@ -1480,13 +1484,13 @@ export class PrevalidateLayer extends DesugaringStep { .enter(["tagRenderings", ...emptyIndexes]) .err( `Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join( - "," - )}])` + ",", + )}])`, ) } const duplicateIds = Utils.Duplicates( - (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions") + (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"), ) if (duplicateIds.length > 0 && !Utils.runningFromConsole) { context @@ -1510,7 +1514,7 @@ export class PrevalidateLayer extends DesugaringStep { if (json.tagRenderings !== undefined) { new On( "tagRenderings", - new Each(new ValidateTagRenderings(json, this._doesImageExist)) + new Each(new ValidateTagRenderings(json, this._doesImageExist)), ).convert(json, context) } @@ -1537,7 +1541,7 @@ export class PrevalidateLayer extends DesugaringStep { context .enters("pointRendering", i, "marker", indexM, "icon", "condition") .err( - "Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead." + "Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead.", ) } } @@ -1575,9 +1579,9 @@ export class PrevalidateLayer extends DesugaringStep { .enters("presets", i, "tags") .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: " + - tags.asHumanString(false, false, {}) + - "\n The required tags are: " + - baseTags.asHumanString(false, false, {}) + tags.asHumanString(false, false, {}) + + "\n The required tags are: " + + baseTags.asHumanString(false, false, {}), ) } } @@ -1594,7 +1598,7 @@ export class ValidateLayerConfig extends DesugaringStep { isBuiltin: boolean, doesImageExist: DoesImageExist, studioValidations: boolean = false, - skipDefaultLayers: boolean = false + skipDefaultLayers: boolean = false, ) { super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") this.validator = new ValidateLayer( @@ -1602,7 +1606,7 @@ export class ValidateLayerConfig extends DesugaringStep { isBuiltin, doesImageExist, studioValidations, - skipDefaultLayers + skipDefaultLayers, ) } @@ -1630,7 +1634,7 @@ class ValidatePointRendering extends DesugaringStep { context .enter("markers") .err( - `Detected a field 'markerS' in pointRendering. It is written as a singular case` + `Detected a field 'markerS' in pointRendering. It is written as a singular case`, ) } if (json.marker && !Array.isArray(json.marker)) { @@ -1640,7 +1644,7 @@ class ValidatePointRendering extends DesugaringStep { context .enter("location") .err( - "A pointRendering should have at least one 'location' to defined where it should be rendered. " + "A pointRendering should have at least one 'location' to defined where it should be rendered. ", ) } return json @@ -1659,26 +1663,26 @@ export class ValidateLayer extends Conversion< isBuiltin: boolean, doesImageExist: DoesImageExist, studioValidations: boolean = false, - skipDefaultLayers: boolean = false + skipDefaultLayers: boolean = false, ) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer") this._prevalidation = new PrevalidateLayer( path, isBuiltin, doesImageExist, - studioValidations + studioValidations, ) this._skipDefaultLayers = skipDefaultLayers } convert( json: LayerConfigJson, - context: ConversionContext + context: ConversionContext, ): { parsed: LayerConfig; raw: LayerConfigJson } { context = context.inOperation(this.name) if (typeof json === "string") { context.err( - `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed` + `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`, ) return undefined } @@ -1709,7 +1713,7 @@ export class ValidateLayer extends Conversion< context .enters("calculatedTags", i) .err( - `Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}` + `Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`, ) } } @@ -1757,7 +1761,7 @@ export class ValidateLayer extends Conversion< context .enters("allowMove", "enableAccuracy") .err( - "`enableAccuracy` is written with two C in the first occurrence and only one in the last" + "`enableAccuracy` is written with two C in the first occurrence and only one in the last", ) } @@ -1788,8 +1792,8 @@ export class ValidateFilter extends DesugaringStep { .enters("fields", i) .err( `Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from( - Validators.availableTypes - ).join(",")}` + Validators.availableTypes, + ).join(",")}`, ) } } @@ -1806,13 +1810,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{ super( "Tries to detect layers where a shared filter can be used (or where similar filters occur)", [], - "DetectDuplicateFilters" + "DetectDuplicateFilters", ) } convert( json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }, - context: ConversionContext + context: ConversionContext, ): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } { const { layers, themes } = json const perOsmTag = new Map< @@ -1876,7 +1880,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{ filter: FilterConfigJson }[] >, - layout?: LayoutConfigJson | undefined + layout?: LayoutConfigJson | undefined, ): void { if (layer.filter === undefined || layer.filter === null) { return @@ -1916,7 +1920,7 @@ export class DetectDuplicatePresets extends DesugaringStep { super( "Detects mappings which have identical (english) names or identical mappings.", ["presets"], - "DetectDuplicatePresets" + "DetectDuplicatePresets", ) } @@ -1927,13 +1931,13 @@ export class DetectDuplicatePresets extends DesugaringStep { if (new Set(enNames).size != enNames.length) { const dups = Utils.Duplicates(enNames) const layersWithDup = json.layers.filter((l) => - l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) + l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0), ) const layerIds = layersWithDup.map((l) => l.id) context.err( `This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join( - ", " - )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets` + ", ", + )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`, ) } @@ -1948,17 +1952,17 @@ export class DetectDuplicatePresets extends DesugaringStep { Utils.SameObject(presetATags, presetBTags) && Utils.sameList( presetA.preciseInput.snapToLayers, - presetB.preciseInput.snapToLayers + presetB.preciseInput.snapToLayers, ) ) { context.err( `This theme has multiple presets with the same tags: ${presetATags.asHumanString( false, false, - {} + {}, )}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[ j - ].title.textFor("en")}'` + ].title.textFor("en")}'`, ) } } @@ -1983,13 +1987,13 @@ export class ValidateThemeEnsemble extends Conversion< super( "Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes", [], - "ValidateThemeEnsemble" + "ValidateThemeEnsemble", ) } convert( json: LayoutConfig[], - context: ConversionContext + context: ConversionContext, ): Map< string, { @@ -2040,11 +2044,11 @@ export class ValidateThemeEnsemble extends Conversion< context.err( [ "The layer with id '" + - id + - "' is found in multiple themes with different tag definitions:", + id + + "' is found in multiple themes with different tag definitions:", "\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}), "\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}), - ].join("\n") + ].join("\n"), ) } } diff --git a/src/Models/ThemeConfig/Json/LayoutConfigJson.ts b/src/Models/ThemeConfig/Json/LayoutConfigJson.ts index 107103ad4..28c9596c1 100644 --- a/src/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -80,6 +80,7 @@ export interface LayoutConfigJson { * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64) * * Type: icon + * suggestions: return Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i})) * group: basic * */ @@ -156,6 +157,7 @@ export interface LayoutConfigJson { * type: layer[] * types: hidden | layer | hidden * group: layers + * title: value["builtin"] ?? value["id"] ?? value * suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: ""+key+" (builtin) - "+layers.get(key).description})) * * A theme must contain at least one layer. diff --git a/src/UI/Flowbite/AccordionSingle.svelte b/src/UI/Flowbite/AccordionSingle.svelte index afd6568ff..da4d90143 100644 --- a/src/UI/Flowbite/AccordionSingle.svelte +++ b/src/UI/Flowbite/AccordionSingle.svelte @@ -6,7 +6,7 @@ - +
diff --git a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte index e096759b8..86901cb84 100644 --- a/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/src/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -373,7 +373,6 @@ {feedback} {unit} {state} - {extraTags} feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} @@ -418,7 +417,6 @@ {feedback} {unit} {state} - {extraTags} feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} @@ -464,7 +462,6 @@ {feedback} {unit} {state} - {extraTags} feature={selectedElement} value={freeformInput} unvalidatedText={freeformInputUnvalidated} diff --git a/src/UI/Studio/CollapsedTagRenderingPreview.svelte b/src/UI/Studio/CollapsedTagRenderingPreview.svelte new file mode 100644 index 000000000..920bcb832 --- /dev/null +++ b/src/UI/Studio/CollapsedTagRenderingPreview.svelte @@ -0,0 +1,207 @@ + + + + +
+ {#if !isTagRenderingBlock} +
+
+ {#if schema.hints.icon} + + {/if} + {#if schema.hints.title} + +
+ {singular} + {i} +
+ {:else} + {singular} + {i} + {/if} +
+ +
+ {:else if typeof value === "string"} + Builtin: {value} + {:else if value["builtin"]} + reused tagrendering {JSON.stringify(value["builtin"])} + {:else} + + {/if} +
+
+ {#if isTagRenderingBlock} + + + + {#if i > 0} + + + + {/if} + {#if i + 1 < $currentValue.length} + + + {/if} + + {:else if schema.hints.types} + + {:else} + {#each subparts as subpart} + + {/each} + {/if} +
+
diff --git a/src/UI/Studio/EditLayerState.ts b/src/UI/Studio/EditLayerState.ts index 45ab9914e..95e3374db 100644 --- a/src/UI/Studio/EditLayerState.ts +++ b/src/UI/Studio/EditLayerState.ts @@ -8,7 +8,7 @@ import { Pipe, } from "../../Models/ThemeConfig/Conversion/Conversion" import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" -import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation" +import { PrevalidateTheme, ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation" import { AllSharedLayers } from "../../Customizations/AllSharedLayers" import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { TagUtils } from "../../Logic/Tags/TagUtils" @@ -33,6 +33,8 @@ export abstract class EditJsonState { public readonly schema: ConfigMeta[] public readonly category: "layers" | "themes" public readonly server: StudioServer + public readonly osmConnection: OsmConnection + public readonly showIntro: UIEventSource<"no" | "intro" | "tagrenderings"> = ( LocalStorageSource.Get("studio-show-intro", "intro") ) @@ -51,7 +53,7 @@ export abstract class EditJsonState { * The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out */ public readonly highlightedItem: UIEventSource = new UIEventSource( - undefined + undefined, ) private sendingUpdates = false private readonly _stores = new Map>() @@ -60,10 +62,12 @@ export abstract class EditJsonState { schema: ConfigMeta[], server: StudioServer, category: "layers" | "themes", + osmConnection: OsmConnection, options?: { expertMode?: UIEventSource - } + }, ) { + this.osmConnection = osmConnection this.schema = schema this.server = server this.category = category @@ -88,6 +92,10 @@ export abstract class EditJsonState { await this.server.update(id, config, this.category) }) this.messages = this.createMessagesStore() + this.register(["credits"], this.osmConnection.userDetails.mapD(u => u.name), false) + this.register(["credits:uid"], this.osmConnection.userDetails.mapD(u => u.uid), false) + + } public startSavingUpdates(enabled = true) { @@ -132,7 +140,7 @@ export abstract class EditJsonState { public register( path: ReadonlyArray, value: Store, - noInitialSync: boolean = true + noInitialSync: boolean = true, ): () => void { const unsync = value.addCallback((v) => { this.setValueAt(path, v) @@ -146,7 +154,7 @@ export abstract class EditJsonState { public getSchemaStartingWith(path: string[]) { return this.schema.filter( (sch) => - !path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)) + !path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)), ) } @@ -167,7 +175,7 @@ export abstract class EditJsonState { const schemas = this.schema.filter( (sch) => sch !== undefined && - !path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)) + !path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)), ) if (schemas.length == 0) { console.warn("No schemas found for path", path.join(".")) @@ -257,12 +265,12 @@ class ContextRewritingStep extends Conversion { constructor( state: DesugaringContext, step: Conversion, - getTagRenderings: (t: T) => TagRenderingConfigJson[] + 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" + "ContextRewritingStep", ) this._state = state this._step = step @@ -272,7 +280,7 @@ class ContextRewritingStep extends Conversion { convert(json: LayerConfigJson, context: ConversionContext): T { const converted = this._step.convert(json, context) const originalIds = json.tagRenderings?.map( - (tr) => (tr)["id"] + (tr) => (tr)["id"], ) if (!originalIds) { return converted @@ -307,7 +315,6 @@ class ContextRewritingStep extends Conversion { export default class EditLayerState extends EditJsonState { // Needed for the special visualisations - public readonly osmConnection: OsmConnection public readonly imageUploadManager = { getCountsFor() { return 0 @@ -335,10 +342,9 @@ export default class EditLayerState extends EditJsonState { schema: ConfigMeta[], server: StudioServer, osmConnection: OsmConnection, - options: { expertMode: UIEventSource } + options: { expertMode: UIEventSource }, ) { - super(schema, server, "layers", options) - this.osmConnection = osmConnection + super(schema, server, "layers", osmConnection, options) this.layout = { getMatchingLayer: () => { try { @@ -393,7 +399,7 @@ export default class EditLayerState extends EditJsonState { return new ContextRewritingStep( state, new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)), - (t) => t.raw.tagRenderings + (t) => t.raw.tagRenderings, ) } @@ -427,7 +433,7 @@ export default class EditLayerState extends EditJsonState { } protected async validate( - configuration: Partial + configuration: Partial, ): Promise { const layers = AllSharedLayers.getSharedLayersConfigs() @@ -456,16 +462,19 @@ export class EditThemeState extends EditJsonState { constructor( schema: ConfigMeta[], server: StudioServer, - options: { expertMode: UIEventSource } + osmConnection: OsmConnection, + options: { expertMode: UIEventSource }, ) { - super(schema, server, "themes", options) + super(schema, server, "themes", osmConnection, options) this.setupFixers() } protected buildValidation(state: DesugaringContext): Conversion { - return new Pipe( - new PrepareTheme(state), - new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())) + return new Pipe(new PrevalidateTheme(), + new Pipe( + new PrepareTheme(state), + new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())), + ), true, ) } diff --git a/src/UI/Studio/QuestionPreview.svelte b/src/UI/Studio/QuestionPreview.svelte index 5a68403eb..13271bc51 100644 --- a/src/UI/Studio/QuestionPreview.svelte +++ b/src/UI/Studio/QuestionPreview.svelte @@ -1,11 +1,13 @@ -
+{#if useFallback} +