From 32e0c18b0906dc7189cba75f0fb65451bbbeb77a Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 27 Jul 2022 23:59:04 +0200 Subject: [PATCH] Add 'send_email'-special element, use this in bike_repair_station --- Docs/SpecialRenderings.md | 2 +- Logic/State/MapState.ts | 4 -- Models/ThemeConfig/Conversion/PrepareLayer.ts | 28 +++++--- UI/SpecialVisualizations.ts | 64 +++++++++++++++---- UI/SubstitutedTranslation.ts | 29 ++++++++- .../bike_repair_station.json | 36 +++++++++-- 6 files changed, 127 insertions(+), 36 deletions(-) diff --git a/Docs/SpecialRenderings.md b/Docs/SpecialRenderings.md index da2ff0dbb..e95612122 100644 --- a/Docs/SpecialRenderings.md +++ b/Docs/SpecialRenderings.md @@ -15,7 +15,7 @@ General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_nam -Instead of using `{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}, one can also write +Instead of using `{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}` , one can also write `{"render":{"special":{"type":"some_special_visualisation","argname":"some_arg","message":{"en":"some other really long message","nl":"een boodschap in een andere taal"},"other_arg_name":"more args"}}}` diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index da9d68ae4..9af4edf6b 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -387,10 +387,6 @@ export default class MapState extends UserRelatedState { isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown") } - isDisplayed.addCallbackAndRun(_ => { - console.log("IsDisplayed?",layer.id, isDisplayed.data, layer.shownByDefault) - }) - const flayer: FilteredLayer = { isDisplayed, layerDef: layer, diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index 529b8397e..e6c05b558 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -38,7 +38,7 @@ class ExpandTagRendering extends Conversionlayer.tagRenderings.filter(tr => tr["id"] !== undefined) @@ -133,11 +133,11 @@ class ExpandTagRendering extends Conversion s) - if(state.sharedLayers.size === 0){ - warnings.push(ctx + ": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", ")) - }else{ - errors.push(ctx + ": While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", ")) - } + if (state.sharedLayers.size === 0) { + warnings.push(ctx + ": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", ")) + } else { + errors.push(ctx + ": While reusing tagrendering: " + name + ": layer " + layerName + " not found. Maybe you meant on of " + candidates.slice(0, 3).join(", ")) + } continue } candidates = Utils.NoNull(layer.tagRenderings.map(tr => tr["id"])).map(id => layerName + "." + id) @@ -458,14 +458,24 @@ export class RewriteSpecial extends DesugaringStep { for (const argName of argNamesList) { const v = special[argName] ?? "" if (Translations.isProbablyATranslation(v)) { - args.push(new Translation(v).textFor(ln)) + const txt = new Translation(v).textFor(ln) + .replace(/,/g, "&COMMA") + .replace(/\{/g, "&LBRACE") + .replace(/}/g, "&RBRACE") + ; + args.push(txt) + } else if (typeof v === "string") { + const txt = v.replace(/,/g, "&COMMA") + .replace(/\{/g, "&LBRACE") + .replace(/}/g, "&RBRACE") + args.push(txt) } else { args.push(v) } } const beforeText = before?.textFor(ln) ?? "" const afterText = after?.textFor(ln) ?? "" - result[ln] = `${beforeText}{${type}(${args.join(",")})}${afterText}` + result[ln] = `${beforeText}{${type}(${args.map(a => a).join(",")})}${afterText}` } return result } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index a2c929107..05cd97454 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -59,7 +59,8 @@ import {CheckBox} from "./Input/Checkboxes"; import Slider from "./Input/Slider"; import List from "./Base/List"; import StatisticsPanel from "./BigComponents/StatisticsPanel"; -import { OsmFeature } from "../Models/OsmFeature"; +import {OsmFeature} from "../Models/OsmFeature"; +import Link from "./Base/Link"; export interface SpecialVisualization { funcName: string, @@ -292,7 +293,7 @@ export default class SpecialVisualizations { if (typeof viz === "string") { viz = SpecialVisualizations.specialVisualizations.find(sv => sv.funcName === viz) } - if(viz === undefined){ + if (viz === undefined) { return undefined; } return new Combine( @@ -328,7 +329,7 @@ export default class SpecialVisualizations { "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args", new Title("Using expanded syntax", 4), - `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}, one can also write`, + `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`, new FixedUiElement(JSON.stringify({ render: { special: { @@ -341,7 +342,7 @@ export default class SpecialVisualizations { "other_arg_name": "more args" } } - })).SetClass("code") + }, null, " ")).SetClass("code") ]).SetClass("flex flex-col"), ...helpTexts ] @@ -1106,12 +1107,12 @@ export default class SpecialVisualizations { args: [], constr(state, tagSource, argument, guistate) { let parentId = tagSource.data.mr_challengeId; - let challenge = Stores.FromPromise(Utils.downloadJsonCached(`https://maproulette.org/api/v2/challenge/${parentId}`,24*60*60*1000)); + let challenge = Stores.FromPromise(Utils.downloadJsonCached(`https://maproulette.org/api/v2/challenge/${parentId}`, 24 * 60 * 60 * 1000)); - let details = new VariableUiElement( challenge.map(challenge => { + let details = new VariableUiElement(challenge.map(challenge => { let listItems: BaseUIElement[] = []; let title: BaseUIElement; - + if (challenge?.name) { title = new Title(challenge.name); } @@ -1124,13 +1125,13 @@ export default class SpecialVisualizations { listItems.push(new FixedUiElement(challenge.instruction)); } - if(listItems.length === 0) { + if (listItems.length === 0) { return undefined; } else { return [title, new List(listItems)]; } })) - return details; + return details; }, docs: "Show details of a MapRoulette task" }, @@ -1138,14 +1139,15 @@ export default class SpecialVisualizations { funcName: "statistics", docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", args: [], - constr : (state, tagsSource, args, guiState) => { + constr: (state, tagsSource, args, guiState) => { const elementsInview = new UIEventSource<{ distance: number, center: [number, number], element: OsmFeature, layer: LayerConfig }[]>([]); + function update() { - const mapCenter = <[number,number]> [state.locationControl.data.lon, state.locationControl.data.lon] + const mapCenter = <[number, number]>[state.locationControl.data.lon, state.locationControl.data.lon] const bbox = state.currentBounds.data const elements = state.featurePipeline.getAllVisibleElementsWithmeta(bbox).map(el => { const distance = GeoOperations.distanceBetween(el.center, mapCenter) - return {...el, distance } + return {...el, distance} }) elements.sort((e0, e1) => e0.distance - e1.distance) elementsInview.setData(elements) @@ -1162,6 +1164,44 @@ export default class SpecialVisualizations { }) return new StatisticsPanel(elementsInview, state) } + }, + { + funcName: "send_email", + docs: "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email", + args: [ + { + name: "to", + doc: "Who to send the email to?", + required: true + }, + { + name: "subject", + doc: "The subject of the email", + required: true + }, + { + name: "body", + doc: "The text in the email", + required: true + }, + + { + name: "button_text", + doc: "The text shown on the button in the UI", + required: true + } + ], + constr(state, tags, args) { + return new VariableUiElement(tags.map(tags => { + + const [to, subject, body, button_text] = args.map(str => Utils.SubstituteKeys(str, tags)) + const url = "mailto:" + to + "?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body) + return new SubtleButton(Svg.envelope_svg(), button_text, { + url + }) + + })) + } } ] diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index a2ec63764..514d946d7 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -55,6 +55,11 @@ export class SubstitutedTranslation extends VariableUiElement { return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); } const viz = proto.special; + if(viz === undefined){ + console.error("SPECIALRENDERING UNDEFINED for", tagsSource.data?.id, "THIS IS REALLY WEIRD") + return undefined + + } try { return viz.func.constr(state, tagsSource, proto.special.args, DefaultGuiState.state)?.SetStyle(proto.special.style); } catch (e) { @@ -73,6 +78,17 @@ export class SubstitutedTranslation extends VariableUiElement { this.SetClass("w-full") } + /** + * + * // Return empty list on empty input + * SubstitutedTranslation.ExtractSpecialComponents("") // => "" + * + * // Advanced cases with commas, braces and newlines should be handled without problem + * const templates = SubstitutedTranslation.ExtractSpecialComponents("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}") + * const templ = templates[0] + * templ.special.func.funcName // => "send_email" + * templ.special.args[0] = "{email}" + */ public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): { fixed?: string, special?: { @@ -81,11 +97,15 @@ export class SubstitutedTranslation extends VariableUiElement { style: string } }[] { + + if(template === ""){ + return [] + } for (const knownSpecial of extraMappings.concat(SpecialVisualizations.specialVisualizations)) { // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' - const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`); + const matched = template.match(new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s")); if (matched != null) { // We found a special component that should be brought to live @@ -97,7 +117,10 @@ export class SubstitutedTranslation extends VariableUiElement { if (argument.length > 0) { const realArgs = argument.split(",").map(str => str.trim() .replace(/&LPARENS/g, '(') - .replace(/&RPARENS/g, ')')); + .replace(/&RPARENS/g, ')') + .replace(/&LBRACE/g, '{') + .replace(/&RBRACE/g, '}') + .replace(/&COMMA/g, ',')); for (let i = 0; i < realArgs.length; i++) { if (args.length <= i) { args.push(realArgs[i]); @@ -124,7 +147,7 @@ export class SubstitutedTranslation extends VariableUiElement { // Hmm, we might have found an invalid rendering name console.warn("Found a suspicious special rendering value in: ", template, " did you mean one of: ", SpecialVisualizations.specialVisualizations.map(sp => sp.funcName + "()").join(", ")) } - + // IF we end up here, no changes have to be made - except to remove any resting {} return [{fixed: template}]; } diff --git a/assets/layers/bike_repair_station/bike_repair_station.json b/assets/layers/bike_repair_station/bike_repair_station.json index 722959ca8..a7589975d 100644 --- a/assets/layers/bike_repair_station/bike_repair_station.json +++ b/assets/layers/bike_repair_station/bike_repair_station.json @@ -557,14 +557,36 @@ ] }, "render": { - "en": "Report this bicycle pump as broken", - "nl": "Rapporteer deze fietspomp als kapot", - "de": "Melde diese Fahrradpumpe als kaputt", - "da": "Anmeld denne cykelpumpe som værende i stykker", - "es": "Reportar esta bomba para bicicletas como rota", - "fr": "Signaler cette pompe à vélo cassée" + "special": { + "type": "send_email", + "to": "{email}", + "subject": { + "en": "Broken bicycle pump", + "nl": "Kapotte fietspomp", + "de": "Fahrradpumpe kaputt", + "es": "Bomba para bicicletas rota", + "fr": "Pompe à vélo cassée", + "da": "Cykelpumpe i stykker" + }, + "body": { + "en": "Hello,\n\nWith this email, I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat={_lat}&lon={_lon}&z=18#{id} is broken.\n\n Kind regards", + "nl": "Geachte\n\nGraag had ik u gemeld dat een fietspomp defect is. De fietspomp bevindt zich hier: https://mapcomplete.osm.be/cyclofix?lat={_lat}&lon={_lon}&z=18#{id}.\n\nMet vriendelijke groeten.", + "de": "Hallo,\n\nMit dieser E-Mail möchte ich Ihnen mitteilen, dass die Fahrradpumpe, die sich unter https://mapcomplete.osm.be/cyclofix?lat={_lat}&lon={_lon}&z=18#{id} befindet, kaputt ist.", + "da": "Hej,\n\nMed denne e-mail vil jeg gerne oplyse, at cykelpumpen, der befinder sig på https://mapcomplete.osm.be/cyclofix?lat={_lat}&lon={_lon}&z=18#{id} er i stykker.\n\n Med venlig hilse", + "es": "Hola,\n\nCon este correo, me gustaría informar de que esta bomba para bicicletas situada en https://mapcomplete.osm.be/cyclofix?lat={_lat}&lon={_lon}&z=18#{id} está rota.\n\nUn saludo", + "fr": "Bonjour,\n\nCe mail pour vous informer que la pompe à vélo située à https://mapcomplete.osm.be/cyclofix?lat={_lat}&lon={_lon}&z=18#{id} est cassée.\n\nBien à vous." + }, + "button_text": { + "en": "Report this bicycle pump as broken", + "nl": "Rapporteer deze fietspomp als kapot", + "fr": "Signaler cette pompe à vélo cassée", + "de": "Melde diese Fahrradpumpe als kaputt", + "da": "Anmeld denne cykelpumpe som værende i stykker", + "es": "Reportar esta bomba para bicicletas como rota" + } + } }, - "id": "Email maintainer" + "id": "send_email_about_broken_pump" }, { "question": {