From ecd8f5e1da5b929ddeba0a3ed2651f8b4dbc0cc1 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 17 Mar 2025 01:17:02 +0100 Subject: [PATCH] Feature(grb): add popup feature to validate e.g. a user profile --- assets/themes/grb/grb.json | 44 ++++++++ langs/themes/en.json | 24 +++++ langs/themes/nl.json | 24 +++++ .../ThemeConfig/Conversion/PrepareTheme.ts | 101 +++++++++--------- .../ThemeConfig/Json/ThemeConfigJson.ts | 22 ++++ src/Models/ThemeConfig/ThemeConfig.ts | 31 +++++- src/Models/ThemeConfig/WithContextLoader.ts | 2 +- src/UI/Base/LoginButton.svelte | 14 ++- src/UI/Base/Popup.svelte | 4 +- .../SettingsVisualisations.ts | 21 +++- src/UI/ThemeViewGUI.svelte | 24 ++++- 11 files changed, 247 insertions(+), 64 deletions(-) diff --git a/assets/themes/grb/grb.json b/assets/themes/grb/grb.json index 58409d303..5508a22ed 100644 --- a/assets/themes/grb/grb.json +++ b/assets/themes/grb/grb.json @@ -19,6 +19,50 @@ "shortDescription": { "nl": "Grb import helper tool" }, + "popup": [ + { + "id": "wikilink-needed", + "condition": "_description!~.*https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import.*", + "dismissable": false, + "title": { + "render": { + "en": "Profile mention obligated", + "nl": "Link op profiel verplicht" + } + }, + "body": [ + { + "render": { + "special": { + "type": "link", + "href": "https://www.openstreetmap.org/profile/edit", + "text": { + "en": "Edit your user profile", + "nl": "Pas je profiel aan" + } + }, + "after": { + "en": "to include the link https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import", + "nl": " en voeg deze link toe: https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import" + } + } + }, + { + "id": "reload_profile", + "render": { + "special": { + "type": "login_button", + "force": "yes", + "message": { + "en": "Reload your profile", + "nl": "Herlaad je profiel" + } + } + } + } + ] + } + ], "icon": "./assets/themes/grb/logo.svg", "startZoom": 9, "startLat": 51.0249, diff --git a/langs/themes/en.json b/langs/themes/en.json index 8a91f3192..8744e0448 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -623,6 +623,30 @@ } } } + }, + "popup": { + "0": { + "body": { + "0": { + "render": { + "special": { + "after": "to include the link https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import", + "text": "Edit your user profile" + } + } + }, + "1": { + "render": { + "special": { + "msg": "Reload your profile" + } + } + } + }, + "title": { + "render": "Profile mention obligated" + } + } } }, "guideposts": { diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 49c9e8b22..56088c853 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -669,6 +669,30 @@ } } }, + "popup": { + "0": { + "body": { + "0": { + "render": { + "special": { + "after": " en voeg deze link toe: https://wiki.openstreetmap.org/wiki/WikiProject_Belgium/Building_and_address_import", + "text": "Pas je profiel aan" + } + } + }, + "1": { + "render": { + "special": { + "msg": "Herlaad je profiel" + } + } + } + }, + "title": { + "render": "Link op profiel verplicht" + } + } + }, "shortDescription": "Grb import helper tool", "title": "GRB import helper" }, diff --git a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts index 4ad260ce1..3e28b85df 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -1,16 +1,6 @@ -import { - Concat, - Conversion, - DesugaringContext, - DesugaringStep, - Each, - Fuse, - On, - Pass, - SetDefault, -} from "./Conversion" +import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion" import { ThemeConfigJson } from "../Json/ThemeConfigJson" -import { PrepareLayer } from "./PrepareLayer" +import { PrepareLayer, RewriteSpecial } from "./PrepareLayer" import { LayerConfigJson } from "../Json/LayerConfigJson" import { Utils } from "../../../Utils" import Constants from "../../Constants" @@ -40,7 +30,7 @@ class SubstituteLayer extends Conversion [ lname, - Utils.levenshteinDistance(name, lname), + Utils.levenshteinDistance(name, lname) ]) withDistance.sort((a, b) => a[1] - b[1]) const ids = withDistance.map((n) => n[0]) @@ -130,9 +120,9 @@ class SubstituteLayer extends Conversion 0) { context.err( "This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + - unused.join(", ") + - "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore" + unused.join(", ") + + "\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore" ) } found.tagRenderings = filtered @@ -205,10 +195,10 @@ export class AddDefaultLayers extends DesugaringStep { if (alreadyLoaded.has(v.id)) { context.warn( "Layout " + - context + - " already has a layer with name " + - v.id + - "; skipping inclusion of this builtin layer" + context + + " already has a layer with name " + + v.id + + "; skipping inclusion of this builtin layer" ) continue } @@ -352,10 +342,10 @@ class AddDependencyLayersToTheme extends DesugaringStep { .enters("layer dependency") .err( "Layer " + - dependency.neededLayer + - " is loaded because " + - dependency.reason + - "; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`" + dependency.neededLayer + + " is loaded because " + + dependency.reason + + "; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`" ) } } @@ -380,12 +370,12 @@ class AddDependencyLayersToTheme extends DesugaringStep { if (dep === undefined) { const message = [ "Loading a dependency failed: layer " + - unmetDependency.neededLayer + - " is not found, neither as layer of " + - themeId + - " nor as builtin layer.", + unmetDependency.neededLayer + + " is not found, neither as layer of " + + themeId + + " nor as builtin layer.", reason, - "Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","), + "Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(",") ] throw message.join("\n\t") } @@ -395,7 +385,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { dep.description = reason dependenciesToAdd.unshift({ config: dep, - reason, + reason }) loadedLayerIds.add(dep.id) unmetDependencies = unmetDependencies.filter( @@ -440,7 +430,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { return { ...theme, - layers: layers, + layers: layers } } } @@ -510,10 +500,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep context.warn( "The theme " + - json.id + - " has an inline layer: " + - layer["id"] + - ". This is discouraged." + json.id + + " has an inline layer: " + + layer["id"] + + ". This is discouraged." ) } return json @@ -555,12 +545,12 @@ class PostvalidateTheme extends DesugaringStep { if (minZoomAll < layer.minzoom) { context.err( "There are multiple layers based on " + - basedOn + - ". The layer with id " + - layer.id + - " has a minzoom of " + - layer.minzoom + - ", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer." + basedOn + + ". The layer with id " + + layer.id + + " has a minzoom of " + + layer.minzoom + + ", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer." ) } } @@ -586,11 +576,11 @@ class PostvalidateTheme extends DesugaringStep { .enters("layers", config.id, "filter", "sameAs") .err( "The layer " + - config.id + - " follows the filter state of layer " + - sameAs + - ", but no layer with this name was found.\n\tDid you perhaps mean one of: " + - closeLayers.slice(0, 3).join(", ") + config.id + + " follows the filter state of layer " + + sameAs + + ", but no layer with this name was found.\n\tDid you perhaps mean one of: " + + closeLayers.slice(0, 3).join(", ") ) } } @@ -618,6 +608,13 @@ export class PrepareTheme extends Fuse { new SetDefault("socialImage", "assets/SocialImage.png", true), // We expand all tagrenderings first... new On("layers", new Each(new PrepareLayer(state))), + new On("popup", new Each( + new Fuse("Prepare popups", + new On("body", new Each(new RewriteSpecial())), + new On("title", new RewriteSpecial()) + ) + )), + // Then we apply the override all. We must first expand everything in case that we override something in an expanded tag // Note that it'll cheat with tagRenderings+ new ApplyOverrideAll(), diff --git a/src/Models/ThemeConfig/Json/ThemeConfigJson.ts b/src/Models/ThemeConfig/Json/ThemeConfigJson.ts index cbdeef07b..654a6b746 100644 --- a/src/Models/ThemeConfig/Json/ThemeConfigJson.ts +++ b/src/Models/ThemeConfig/Json/ThemeConfigJson.ts @@ -3,6 +3,8 @@ import ExtraLinkConfigJson from "./ExtraLinkConfigJson" import { RasterLayerProperties } from "../../RasterLayerProperties" import { Translatable } from "./Translatable" +import { TagConfigJson } from "./TagConfigJson" +import { TagRenderingConfigJson } from "./TagRenderingConfigJson" /** * Defines the entire theme. @@ -468,4 +470,24 @@ export interface ThemeConfigJson { * group: hidden */ _usedImages?: string[] + + /** + * If set, an _additional_ popup will be shown under the theme introduction page. + * + * The embedded tagRenderingConfigs will be run against the settings-state of the contributor. + * If multiple popups are set, the first popup of the list will be rendered on top (and thus be seen first). + */ + popup?: { + /** + * ifset: the user can dismiss this message + */ + dismissible?: boolean + condition?: TagConfigJson + title: TagRenderingConfigJson, + body: TagRenderingConfigJson[], + /** + * id of the popup, mostly to keep the translations in check + */ + id: string, + }[] } diff --git a/src/Models/ThemeConfig/ThemeConfig.ts b/src/Models/ThemeConfig/ThemeConfig.ts index de1851fc6..ad677a380 100644 --- a/src/Models/ThemeConfig/ThemeConfig.ts +++ b/src/Models/ThemeConfig/ThemeConfig.ts @@ -9,6 +9,10 @@ import LanguageUtils from "../../Utils/LanguageUtils" import { RasterLayerProperties } from "../RasterLayerProperties" import { Translatable } from "./Json/Translatable" +import { TagsFilter } from "../../Logic/Tags/TagsFilter" +import TagRenderingConfig from "./TagRenderingConfig" +import { TagUtils } from "../../Logic/Tags/TagUtils" +import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" /** * Minimal information about a theme @@ -93,6 +97,14 @@ export default class ThemeConfig implements ThemeInformation { public readonly source: ThemeConfigJson public readonly enableCache: boolean + public readonly popups: Readonly<{ + id: string, + dismissible?: boolean, + condition: TagsFilter, + title: TagRenderingConfig, + body: TagRenderingConfig[] + }>[] + constructor( json: ThemeConfigJson, official = true, @@ -193,11 +205,26 @@ export default class ThemeConfig implements ThemeInformation { icon: "./assets/svg/pop-out.svg", href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}", newTab: true, - requirements: ["iframe", "no-welcome-message"], + requirements: ["iframe", "no-welcome-message"] }, context + ".extraLink" ) + this.popups = (json.popup ?? []).map((p, i) => { + const ctx = context + ".popup." + i + if (!p.id) { + throw (ctx + ": an id is required") + } + const body: TagRenderingConfigJson[] = Array.isArray(p.body) ? p.body : [p.body] + return { + id: p.id, + dismissible: p.dismissible ?? false, + condition: TagUtils.Tag(p.condition), + title: new TagRenderingConfig(p.title, ctx + ".title"), + body: body.map((body, i) => new TagRenderingConfig(body, ctx + ".body." + i)) + } + }) + this.hideFromOverview = json.hideFromOverview ?? false this.lockLocation = <[[number, number], [number, number]]>json.lockLocation ?? undefined this.enableUserBadge = json.enableUserBadge ?? true @@ -351,7 +378,7 @@ export default class ThemeConfig implements ThemeInformation { // The 'favourite'-layer contains pretty much all images as it bundles all layers, so we exclude it const jsonNoFavourites = { ...json, - layers: json.layers.filter((l) => l["id"] !== "favourite"), + layers: json.layers.filter((l) => l["id"] !== "favourite") } const usedImages = jsonNoFavourites._usedImages usedImages.sort() diff --git a/src/Models/ThemeConfig/WithContextLoader.ts b/src/Models/ThemeConfig/WithContextLoader.ts index 0fee9d6db..46d16e281 100644 --- a/src/Models/ThemeConfig/WithContextLoader.ts +++ b/src/Models/ThemeConfig/WithContextLoader.ts @@ -12,7 +12,7 @@ export default class WithContextLoader { this._context = context } - /** Given a key, gets the corresponding property from the json (or the default if not found + /** Given a key, gets the corresponding property from the json (or the default if not found) * * The found value is interpreted as a tagrendering and fetched/parsed * */ diff --git a/src/UI/Base/LoginButton.svelte b/src/UI/Base/LoginButton.svelte index 21d332726..011732003 100644 --- a/src/UI/Base/LoginButton.svelte +++ b/src/UI/Base/LoginButton.svelte @@ -6,18 +6,26 @@ export let osmConnection: OsmConnection export let clss: string | undefined = undefined - + /** + * Show the button, even though we are logged in + */ + export let forceShow: boolean = false + export let msg: String = undefined if (osmConnection === undefined) { console.error("No osmConnection passed into loginButton") } let isLoggedIn = osmConnection.isLoggedIn -{#if !$isLoggedIn} +{#if !$isLoggedIn || forceShow} {/if} diff --git a/src/UI/Base/Popup.svelte b/src/UI/Base/Popup.svelte index 0670c5e7b..96df2ac07 100644 --- a/src/UI/Base/Popup.svelte +++ b/src/UI/Base/Popup.svelte @@ -42,8 +42,8 @@ shown.set(false)} - outsideclose + on:close={() =>shown.set(false)} + outsideclose={dismissable} size="xl" {dismissable} {defaultClass} diff --git a/src/UI/SpecialVisualisations/SettingsVisualisations.ts b/src/UI/SpecialVisualisations/SettingsVisualisations.ts index 8d1242039..b7ff0890c 100644 --- a/src/UI/SpecialVisualisations/SettingsVisualisations.ts +++ b/src/UI/SpecialVisualisations/SettingsVisualisations.ts @@ -111,12 +111,27 @@ export class SettingsVisualisations { }, { funcName: "login_button", - args: [], + args: [{ + name: "force", + doc: "Always show this button, even if logged in" + }, { + name: "message", + doc: "Message to display on the button" + }], docs: "Show a login button", needsUrls: [], group: "settings", - constr(state: SpecialVisualizationState): SvelteUIElement { - return new SvelteUIElement(LoginButton, { osmConnection: state.osmConnection }) + constr(state: SpecialVisualizationState, _, args): SvelteUIElement { + const force = args[0].toLowerCase() + let msg = args[1] + if (msg === "") { + msg = undefined + } + return new SvelteUIElement(LoginButton, { + osmConnection: state.osmConnection, + msg, + forceShow: force === "yes" || force === "true" + }) }, }, diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index a7d54765f..bff3dac80 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -49,6 +49,8 @@ import Loading from "./Base/Loading.svelte" import { WithSearchState } from "../Models/ThemeViewState/WithSearchState" import TitleHandler from "../Logic/Actors/TitleHandler" + import Popup from "./Base/Popup.svelte" + import TagRenderingAnswer from "./Popup/TagRendering/TagRenderingAnswer.svelte" export let state: WithSearchState new TitleHandler(state.selectedElement, state) @@ -76,6 +78,7 @@ let mapproperties: MapProperties = state.mapProperties let searchOpened = state.searchState.showSearchDrawer + let metatags = state.userRelatedState.preferencesAsTags Orientation.singleton.startMeasurements() let slideDuration = 150 // ms @@ -148,7 +151,7 @@ const bottomRight = mlmap.unproject([rect.right, rect.bottom]) const bbox = new BBox([ [topLeft.lng, topLeft.lat], - [bottomRight.lng, bottomRight.lat], + [bottomRight.lng, bottomRight.lat] ]) state.visualFeedbackViewportBounds.setData(bbox) } @@ -500,5 +503,24 @@ {/if} {/if} + {#each theme.popups as popup} + {#if popup.condition.matchesProperties($metatags)} + + +
+ {#each popup.body as body} + + {/each} + {popup.id} +
+
+ {/if} + {/each}