TagRenderingConfig: fix 'leftovers' of multi-answer freeform, generateDocs now generates Markdown instead of a BaseUIElement, add 'postfixDistinguished'-option for 'charge'-key

This commit is contained in:
Pieter Vander Vennet 2024-06-06 03:16:36 +02:00
parent 53ef1b947d
commit 3a69157d10
4 changed files with 138 additions and 92 deletions

View file

@ -867,6 +867,7 @@
},
"website",
{
"id": "charge_cost_rewritten",
"rewrite": {
"sourceString": [
"{product_key}",
@ -902,34 +903,38 @@
]
]
},
"renderings": {
"question": {
"en": "How much does a {product_name} cost?",
"ca": "Quant costa {product_name}?",
"de": "Wie viel kostet {product_name}?",
"cs": "Kolik stojí {product_name}?",
"nl": "Hoeveel kost {product_name}?",
"pt_BR": "Quanto custa {product_name}?",
"es": "¿Cuánto cuesta {product_name}?",
"pt": "Quanto custa {product_name}?"
},
"render": {
"en": "{product_name} costs {charge:{product_key}}",
"ca": "{product_name} costa {charge:{product_key}}",
"de": "{product_name} kostet {charge:{product_key}}",
"cs": "{product_name} {charge:{product_key}}",
"nl": "{product_name} kost {charge:{product_key}}",
"pt_BR": "{product_name} custa {charge:{product_key}}",
"es": "{product_name} cuesta {charge:{product_key}}",
"pt": "{product_name} custa {charge:{product_key}}"
},
"freeform": {
"key": "charge:{product_key}",
"type": "currency"
},
"condition": "vending~.*{product_key}.*",
"id": "charge_{product_key}"
}
"renderings": [
{
"id": "charge_{product_key}",
"question": {
"en": "How much does a {product_name} cost?",
"ca": "Quant costa {product_name}?",
"de": "Wie viel kostet {product_name}?",
"cs": "Kolik stojí {product_name}?",
"nl": "Hoeveel kost {product_name}?",
"pt_BR": "Quanto custa {product_name}?",
"es": "¿Cuánto cuesta {product_name}?",
"pt": "Quanto custa {product_name}?"
},
"render": {
"en": "{product_name} costs {charge}",
"ca": "{product_name} costa {charge}",
"de": "{product_name} kostet {charge}",
"cs": "{product_name} {charge}",
"nl": "{product_name} kost {charge}",
"pt_BR": "{product_name} custa {charge}",
"es": "{product_name} cuesta {charge}",
"pt": "{product_name} custa {charge}"
},
"freeform": {
"key": "charge",
"type": "currency",
"inline": true,
"postfixDistinguished": "{product_key}"
},
"condition": "vending~.*{product_key}.*"
}
]
},
{
"id": "operational_status",

View file

@ -264,7 +264,22 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* ifunset: The question will be considered answered if any value is set for the key
* group: expert
*/
invalidValues?: TagConfigJson
invalidValues?: TagConfigJson,
/**
* question: If this key shared and distinguished by a postfix, what is the postfix?
* This option is used specifically for `charge`, where the cost is indicated with `/item`.
*
* For example, a vending machine might sell `bicycle_tube`.
* Setting this value to `bicycle_tube`, then answering this question will set `charge= €XX/bicycle_tube`.
* If charge did already contain another value, e.g. `charge= €YY/some_item; €ZZ/other_item`, then `€XX/bicycle_tube`will be added.
* Note: those values are sorted alphabetically
* Note: no need to add the `/`
*
* ifunset: Don't distinguish by postfix
* group: expert
*/
postfixDistinguished?: string
}
/**

View file

@ -5,13 +5,8 @@ import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { RegexTag } from "../../Logic/Tags/RegexTag"
@ -19,9 +14,7 @@ import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import NameSuggestionIndex from "../../Logic/Web/NameSuggestionIndex"
import { GeoOperations } from "../../Logic/GeoOperations"
import { Feature } from "geojson"
export interface Icon {
}
import MarkdownUtils from "../../Utils/MarkdownUtils"
export interface Mapping {
readonly if: UploadableTag
@ -72,6 +65,7 @@ export default class TagRenderingConfig {
readonly addExtraTags: UploadableTag[]
readonly inline: boolean
readonly default?: string
readonly postfixDistinguished?: string
}
public readonly multiAnswer: boolean
@ -201,7 +195,8 @@ export default class TagRenderingConfig {
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default
default: json.freeform.default,
postfixDistinguished: json.freeform.postfixDistinguished?.trim()
}
if (json.freeform["extraTags"] !== undefined) {
throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
@ -218,6 +213,14 @@ export default class TagRenderingConfig {
throw `If you use a freeform key 'questions', the ID must be 'questions' too to trigger the special behaviour. The current id is '${this.id}' (at ${context})`
}
}
if (this.freeform.postfixDistinguished) {
if (this.multiAnswer) {
throw "At " + context + ": a postfixDistinguished-value cannot be used with a multiAnswer"
}
if (this.freeform.postfixDistinguished.startsWith("/")) {
throw "At " + context + ": a postfixDistinguished-value should not start with `/`. This will be inserted automatically"
}
}
// freeform.type is validated in Validation.ts so that we don't need ValidatedTextFields here
if (this.freeform.addExtraTags) {
@ -466,9 +469,9 @@ export default class TagRenderingConfig {
// A flag to check that the freeform key isn't matched multiple times
// If it is undefined, it is "used" already, or at least we don't have to check for it anymore
const freeformKeyDefined = this.freeform?.key !== undefined
const usedFreeformValues = new Set<string>()
// We run over all the mappings first, to check if the mapping matches
const applicableMappings: {
if?: TagsFilter
then: TypedTranslation<Record<string, string>>
img?: string
}[] = Utils.NoNull(
@ -477,23 +480,23 @@ export default class TagRenderingConfig {
return mapping
}
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
if (freeformKeyDefined && mapping.if.isUsableAsAnswer()) {
// THe freeform key is defined: what value does it use though?
// We mark the value to see if we have any leftovers
const value = mapping.if
.asChange({})
.find((kv) => kv.k === this.freeform.key).v
usedFreeformValues.add(value)
}
return mapping
}
return undefined
})
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
const usedFreeformValues = new Set<string>(
applicableMappings
?.flatMap(m => m.if?.usedTags() ?? [])
?.filter(kv => kv.key === this.freeform.key)
?.map(kv => kv.value)
)
const freeformValues = tags[this.freeform.key].split(";")
const leftovers = freeformValues.filter((v) => !usedFreeformValues.has(v))
const leftovers = freeformValues.filter((v) => !usedFreeformValues.has(v.trim()))
for (const leftover of leftovers) {
applicableMappings.push({
then: new TypedTranslation<object>(
@ -539,6 +542,23 @@ export default class TagRenderingConfig {
}
if (this.freeform?.key === undefined || tags[this.freeform.key] !== undefined) {
const postfix = this.freeform?.postfixDistinguished
if (postfix !== undefined) {
const allFreeforms = tags[this.freeform.key].split(";").map(s => s.trim())
for (const allFreeform of allFreeforms) {
if (allFreeform.endsWith(postfix)) {
const [v] = allFreeform.split("/")
// We found the needed postfix
return {
then: this.render.PartialSubs({ [this.freeform.key]: v.trim() }),
icon: this.renderIcon,
iconClass: this.renderIconClass
}
}
}
// needed postfix not found
return undefined
}
return { then: this.render, icon: this.renderIcon, iconClass: this.renderIconClass }
}
@ -643,6 +663,11 @@ export default class TagRenderingConfig {
* const tags = config.constructChangeSpecification("Tu-Fr 05:30-09:30", undefined, undefined, { }}
* tags // =>new And([ new Tag("opening_hours", "Tu-Fr 05:30-09:30")])
*
* const config = new TagRenderingConfig({"id": "charge", render: "One tube costs {charge}", freeform: {key: "charge", postfixDistinguished: "bicycle_tube"]}, })
* const tags = config.constructChangeSpecification("€5", undefined, undefined, {vending: "books;bicycle_tubes" charge: "€42/book"})
* tags // =>new And([ new Tag("charge", "€5/bicycle_tube; €42/book")])
*
*
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
*
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
@ -658,6 +683,7 @@ export default class TagRenderingConfig {
if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim()
}
const validator = Validators.get(<ValidatorType>this.freeform?.type)
if (validator && freeformValue) {
freeformValue = validator.reformat(freeformValue, () => currentProperties["_country"])
@ -665,6 +691,18 @@ export default class TagRenderingConfig {
if (freeformValue === "") {
freeformValue = undefined
}
if (this.freeform?.postfixDistinguished && freeformValue !== undefined) {
const allValues = currentProperties[this.freeform.key].split(";").map(s => s.trim())
const perPostfix: Record<string, string> = {}
for (const value of allValues) {
const [v, postfix] = value.split("/")
perPostfix[postfix.trim()] = v.trim()
}
perPostfix[this.freeform.postfixDistinguished] = freeformValue
const keys = Object.keys(perPostfix)
keys.sort()
freeformValue = keys.map(k => perPostfix[k] + "/" + k).join("; ")
}
if (
freeformValue === undefined &&
singleSelectedMapping === undefined &&
@ -740,6 +778,7 @@ export default class TagRenderingConfig {
!someMappingIsShown ||
singleSelectedMapping === undefined)
if (useFreeform) {
return new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? [])
@ -762,30 +801,23 @@ export default class TagRenderingConfig {
}
}
GenerateDocumentation(): BaseUIElement {
let withRender: (BaseUIElement | string)[] = []
GenerateDocumentation(): string {
let withRender: string[] = []
if (this.freeform?.key !== undefined) {
withRender = [
`This rendering asks information about the property `,
Link.OsmWiki(this.freeform.key),
new Combine([
"This is rendered with ",
new FixedUiElement(this.render.txt).SetClass("code font-bold")
])
Link.OsmWiki(this.freeform.key).AsMarkdown(),
"This is rendered with `" + this.render.txt + "`"
]
}
let mappings: BaseUIElement = undefined
let mappings: string = undefined
if (this.mappings !== undefined) {
mappings = new List(
[].concat(
...this.mappings.map((m) => {
const msgs: (string | BaseUIElement)[] = [
new Combine([
new FixedUiElement(m.then.txt).SetClass("font-bold"),
" corresponds with ",
m.if.asHumanString(true, false, {})
])
mappings = MarkdownUtils.list(
this.mappings.flatMap((m) => {
const msgs: (string)[] = [
"*" + m.then.txt + "* corresponds with " +
m.if.asHumanString(true, false, {})
]
if (m.hideInAnswer === true) {
msgs.push("_This option cannot be chosen as answer_")
@ -798,44 +830,32 @@ export default class TagRenderingConfig {
}
return msgs
})
)
)
}
let condition: BaseUIElement = undefined
let condition: string = undefined
if (this.condition !== undefined && !this.condition?.matchesProperties({})) {
condition = new Combine([
"This tagrendering is only visible in the popup if the following condition is met:",
new FixedUiElement(
(<TagsFilter>this.condition.optimize()).asHumanString(true, false, {})
).SetClass("code")
])
const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString(true, false, {})
condition = "This tagrendering is only visible in the popup if the following condition is met: " + conditionAsLink
}
let labels: BaseUIElement = undefined
let labels: string = undefined
if (this.labels?.length > 0) {
labels = new Combine([
labels = [
"This tagrendering has labels ",
...this.labels.map((label) => new FixedUiElement(label).SetClass("code"))
]).SetClass("flex")
...this.labels.map((label) => "`" + label + "`")
].join("\n")
}
return new Combine([
new Title(this.id, 3),
return [
"### this.id",
this.description,
this.question !== undefined
? new Combine([
"The question is ",
new FixedUiElement(this.question.txt).SetClass("font-bold bold")
])
: new FixedUiElement(
"This tagrendering has no question and is thus read-only"
).SetClass("italic"),
new Combine(withRender),
this.question !== undefined ? ("The question is `" + this.question.txt + "`") : "_This tagrendering has no question and is thus read-only_",
withRender.join("\n"),
mappings,
condition,
labels
]).SetClass("flex flex-col")
].join("\n")
}
public usedTags(): TagsFilter[] {
@ -871,8 +891,8 @@ export class TagRenderingConfigUtils {
}
const extraMappings = tags
.bindD(tags => {
const country = tags._country
if(country === undefined){
const country = tags._country
if (country === undefined) {
return undefined
}
const center = GeoOperations.centerpointCoordinates(feature)
@ -883,7 +903,10 @@ export class TagRenderingConfigUtils {
return config
}
const clone: TagRenderingConfig = Object.create(config)
const oldMappingsCloned = clone.mappings?.map(m => ({ ...m, priorityIf: m.priorityIf ?? TagUtils.Tag("id~*") })) ?? []
const oldMappingsCloned = clone.mappings?.map(m => ({
...m,
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*")
})) ?? []
clone.mappings = [...oldMappingsCloned, ...extraMappings]
return clone
})

View file

@ -16,4 +16,7 @@ export default class MarkdownUtils {
return result
}
static list(strings: string[]): string {
return strings.map(item => " - "+item).join("\n")
}
}