Move 'mappings' to a separate interface
This commit is contained in:
parent
bb10f60636
commit
0146d1e4f7
4 changed files with 270 additions and 225 deletions
|
@ -200,6 +200,24 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
|
|||
}
|
||||
}
|
||||
|
||||
export class FirstOf<T, X> extends Conversion<T, X>{
|
||||
private readonly _conversion: Conversion<T, X[]>;
|
||||
|
||||
constructor(conversion: Conversion<T, X[]>) {
|
||||
super("Picks the first result of the conversion step", [], "FirstOf("+conversion.name+")");
|
||||
this._conversion = conversion;
|
||||
}
|
||||
|
||||
convert(json: T, context: string): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
const reslt = this._conversion.convert(json, context);
|
||||
return {
|
||||
...reslt,
|
||||
result: reslt.result[0]
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Fuse<T> extends DesugaringStep<T> {
|
||||
private readonly steps: DesugaringStep<T>[];
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, SetDefault} from "./Conversion";
|
||||
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault} from "./Conversion";
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson";
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
@ -8,7 +8,7 @@ import Translations from "../../../UI/i18n/Translations";
|
|||
import {Translation} from "../../../UI/i18n/Translation";
|
||||
import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
|
||||
import {AddContextToTranslations} from "./AddContextToTranslations";
|
||||
import spec = Mocha.reporters.spec;
|
||||
|
||||
|
||||
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
|
||||
private readonly _state: DesugaringContext;
|
||||
|
@ -85,17 +85,20 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
if (typeof tr === "string") {
|
||||
const lookup = this.lookup(tr);
|
||||
if (lookup === undefined) {
|
||||
warnings.push(ctx + "A literal rendering was detected: " + tr)
|
||||
const isTagRendering = ctx.indexOf("On(mapRendering") < 0
|
||||
if(isTagRendering){
|
||||
warnings.push(ctx + "A literal rendering was detected: " + tr)
|
||||
}
|
||||
return [{
|
||||
render: tr,
|
||||
id: tr.replace(/![a-zA-Z0-9]/g, "")
|
||||
id: tr.replace(/[^a-zA-Z0-9]/g, "")
|
||||
}]
|
||||
}
|
||||
return lookup
|
||||
}
|
||||
|
||||
if (tr["builtin"] !== undefined) {
|
||||
let names = tr["builtin"]
|
||||
let names: string | string[] = tr["builtin"]
|
||||
if (typeof names === "string") {
|
||||
names = [names]
|
||||
}
|
||||
|
@ -111,7 +114,13 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
|
|||
for (const name of names) {
|
||||
const lookup = this.lookup(name)
|
||||
if (lookup === undefined) {
|
||||
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
|
||||
let candidates = Array.from(state.tagRenderings.keys())
|
||||
if(name.indexOf(".") > 0){
|
||||
const [layer, search] = name.split(".")
|
||||
candidates = Utils.NoNull( state.sharedLayers.get(layer).tagRenderings.map(tr => tr["id"])).map(id => layer+"."+id)
|
||||
}
|
||||
candidates = Utils.sortedByLevenshteinDistance(name, candidates, i => i);
|
||||
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " +candidates.join(", ") + "?")
|
||||
continue
|
||||
}
|
||||
for (let foundTr of lookup) {
|
||||
|
@ -485,6 +494,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
new On("tagRenderings", new Concat(new ExpandTagRendering(state))),
|
||||
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
new On("mapRendering",new Each( new On("icon", new FirstOf(new ExpandTagRendering(state))))),
|
||||
new SetDefault("titleIcons", ["defaults"]),
|
||||
new On("titleIcons", new Concat(new ExpandTagRendering(state)))
|
||||
);
|
||||
|
|
|
@ -1,6 +1,118 @@
|
|||
import {AndOrTagConfigJson} from "./TagConfigJson";
|
||||
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
|
||||
|
||||
|
||||
export interface MappingConfigJson {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
if: AndOrTagConfigJson | string,
|
||||
/**
|
||||
* Shown if the 'if is fulfilled
|
||||
* Type: rendered
|
||||
*/
|
||||
then: string | any,
|
||||
/**
|
||||
* An extra icon supporting the choice
|
||||
* Type: icon
|
||||
*/
|
||||
icon?: string | {
|
||||
/**
|
||||
* The path to the icon
|
||||
* Type: icon
|
||||
*/
|
||||
path: string,
|
||||
/**
|
||||
* Size of the image
|
||||
*/
|
||||
class: "small" | "medium" | "large" | string
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
|
||||
*
|
||||
* In the latter case, a correct text should be shown, but only a single, canonical tagging should be selectable by the user.
|
||||
* In this case, one of the mappings can be hiden by setting this flag.
|
||||
*
|
||||
* To demonstrate an example making a default assumption:
|
||||
*
|
||||
* mappings: [
|
||||
* {
|
||||
* if: "access=", -- no access tag present, we assume accessible
|
||||
* then: "Accessible to the general public",
|
||||
* hideInAnswer: true
|
||||
* },
|
||||
* {
|
||||
* if: "access=yes",
|
||||
* then: "Accessible to the general public", -- the user selected this, we add that to OSM
|
||||
* },
|
||||
* {
|
||||
* if: "access=no",
|
||||
* then: "Not accessible to the public"
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* For example, for an operator, we have `operator=Agentschap Natuur en Bos`, which is often abbreviated to `operator=ANB`.
|
||||
* Then, we would add two mappings:
|
||||
* {
|
||||
* if: "operator=Agentschap Natuur en Bos" -- the non-abbreviated version which should be uploaded
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* },
|
||||
* {
|
||||
* if: "operator=ANB", -- we don't want to upload abbreviations
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* hideInAnswer: true
|
||||
* }
|
||||
*
|
||||
* Hide in answer can also be a tagsfilter, e.g. to make sure an option is only shown when appropriate.
|
||||
* Keep in mind that this is reverse logic: it will be hidden in the answer if the condition is true, it will thus only show in the case of a mismatch
|
||||
*
|
||||
* e.g., for toilets: if "wheelchair=no", we know there is no wheelchair dedicated room.
|
||||
* For the location of the changing table, the option "in the wheelchair accessible toilet is weird", so we write:
|
||||
*
|
||||
* {
|
||||
* "question": "Where is the changing table located?"
|
||||
* "mappings": [
|
||||
* {"if":"changing_table:location=female","then":"In the female restroom"},
|
||||
* {"if":"changing_table:location=male","then":"In the male restroom"},
|
||||
* {"if":"changing_table:location=wheelchair","then":"In the wheelchair accessible restroom", "hideInAnswer": "wheelchair=no"},
|
||||
*
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* Also have a look for the meta-tags
|
||||
* {
|
||||
* if: "operator=Agentschap Natuur en Bos",
|
||||
* then: "Maintained by Agentschap Natuur en Bos",
|
||||
* hideInAnswer: "_country!=be"
|
||||
* }
|
||||
*/
|
||||
hideInAnswer?: boolean | string | AndOrTagConfigJson,
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This is for situations such as:
|
||||
* `accepts:coins=no` where one can select all the possible payment methods. However, we want to make explicit that some options _were not_ selected.
|
||||
* This can be done with `ifnot`
|
||||
* Note that we can not explicitly render this negative case to the user, we cannot show `does _not_ accept coins`.
|
||||
* If this is important to your usecase, consider using multiple radiobutton-fields without `multiAnswer`
|
||||
*/
|
||||
ifnot?: AndOrTagConfigJson | string
|
||||
|
||||
/**
|
||||
* If chosen as answer, these tags will be applied as well onto the object.
|
||||
* Not compatible with multiAnswer
|
||||
*/
|
||||
addExtraTags?: string[]
|
||||
|
||||
/**
|
||||
* Searchterms (per language) to easily find an option if there are many options
|
||||
*/
|
||||
searchTerms?: Record<string, string[]>
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A QuestionableTagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.
|
||||
* If the desired tags are missing and a question is defined, a question will be shown instead.
|
||||
|
@ -68,109 +180,5 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
/**
|
||||
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
|
||||
*/
|
||||
mappings?: {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
if: AndOrTagConfigJson | string,
|
||||
/**
|
||||
* Shown if the 'if is fulfilled
|
||||
* Type: rendered
|
||||
*/
|
||||
then: string | any,
|
||||
/**
|
||||
* An extra icon supporting the choice
|
||||
* Type: icon
|
||||
*/
|
||||
icon?: string | {
|
||||
/**
|
||||
* The path to the icon
|
||||
* Type: icon
|
||||
*/
|
||||
path: string,
|
||||
/**
|
||||
* Size of the image
|
||||
*/
|
||||
class: "small" | "medium" | "large" | string
|
||||
}
|
||||
|
||||
/**
|
||||
* In some cases, multiple taggings exist (e.g. a default assumption, or a commonly mapped abbreviation and a fully written variation).
|
||||
*
|
||||
* In the latter case, a correct text should be shown, but only a single, canonical tagging should be selectable by the user.
|
||||
* In this case, one of the mappings can be hiden by setting this flag.
|
||||
*
|
||||
* To demonstrate an example making a default assumption:
|
||||
*
|
||||
* mappings: [
|
||||
* {
|
||||
* if: "access=", -- no access tag present, we assume accessible
|
||||
* then: "Accessible to the general public",
|
||||
* hideInAnswer: true
|
||||
* },
|
||||
* {
|
||||
* if: "access=yes",
|
||||
* then: "Accessible to the general public", -- the user selected this, we add that to OSM
|
||||
* },
|
||||
* {
|
||||
* if: "access=no",
|
||||
* then: "Not accessible to the public"
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* For example, for an operator, we have `operator=Agentschap Natuur en Bos`, which is often abbreviated to `operator=ANB`.
|
||||
* Then, we would add two mappings:
|
||||
* {
|
||||
* if: "operator=Agentschap Natuur en Bos" -- the non-abbreviated version which should be uploaded
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* },
|
||||
* {
|
||||
* if: "operator=ANB", -- we don't want to upload abbreviations
|
||||
* then: "Maintained by Agentschap Natuur en Bos"
|
||||
* hideInAnswer: true
|
||||
* }
|
||||
*
|
||||
* Hide in answer can also be a tagsfilter, e.g. to make sure an option is only shown when appropriate.
|
||||
* Keep in mind that this is reverse logic: it will be hidden in the answer if the condition is true, it will thus only show in the case of a mismatch
|
||||
*
|
||||
* e.g., for toilets: if "wheelchair=no", we know there is no wheelchair dedicated room.
|
||||
* For the location of the changing table, the option "in the wheelchair accessible toilet is weird", so we write:
|
||||
*
|
||||
* {
|
||||
* "question": "Where is the changing table located?"
|
||||
* "mappings": [
|
||||
* {"if":"changing_table:location=female","then":"In the female restroom"},
|
||||
* {"if":"changing_table:location=male","then":"In the male restroom"},
|
||||
* {"if":"changing_table:location=wheelchair","then":"In the wheelchair accessible restroom", "hideInAnswer": "wheelchair=no"},
|
||||
*
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* Also have a look for the meta-tags
|
||||
* {
|
||||
* if: "operator=Agentschap Natuur en Bos",
|
||||
* then: "Maintained by Agentschap Natuur en Bos",
|
||||
* hideInAnswer: "_country!=be"
|
||||
* }
|
||||
*/
|
||||
hideInAnswer?: boolean | string | AndOrTagConfigJson,
|
||||
/**
|
||||
* Only applicable if 'multiAnswer' is set.
|
||||
* This is for situations such as:
|
||||
* `accepts:coins=no` where one can select all the possible payment methods. However, we want to make explicit that some options _were not_ selected.
|
||||
* This can be done with `ifnot`
|
||||
* Note that we can not explicitly render this negative case to the user, we cannot show `does _not_ accept coins`.
|
||||
* If this is important to your usecase, consider using multiple radiobutton-fields without `multiAnswer`
|
||||
*/
|
||||
ifnot?: AndOrTagConfigJson | string
|
||||
|
||||
/**
|
||||
* If chosen as answer, these tags will be applied as well onto the object.
|
||||
* Not compatible with multiAnswer
|
||||
*/
|
||||
addExtraTags?: string[]
|
||||
|
||||
}[]
|
||||
mappings?: MappingConfigJson[]
|
||||
}
|
|
@ -11,10 +11,21 @@ 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 {QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
|
||||
import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
|
||||
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
|
||||
import {Paragraph} from "../../UI/Base/Paragraph";
|
||||
|
||||
export interface Mapping {
|
||||
readonly if: TagsFilter,
|
||||
readonly ifnot?: TagsFilter,
|
||||
readonly then: TypedTranslation<object>,
|
||||
readonly icon: string,
|
||||
readonly iconClass: string
|
||||
readonly hideInAnswer: boolean | TagsFilter
|
||||
readonly addExtraTags: Tag[],
|
||||
readonly searchTerms?: Record<string, string[]>
|
||||
}
|
||||
|
||||
/***
|
||||
* The parsed version of TagRenderingConfigJSON
|
||||
* Identical data, but with some methods and validation
|
||||
|
@ -41,15 +52,7 @@ export default class TagRenderingConfig {
|
|||
|
||||
public readonly multiAnswer: boolean;
|
||||
|
||||
public readonly mappings?: {
|
||||
readonly if: TagsFilter,
|
||||
readonly ifnot?: TagsFilter,
|
||||
readonly then: TypedTranslation<object>,
|
||||
readonly icon: string,
|
||||
readonly iconClass: string
|
||||
readonly hideInAnswer: boolean | TagsFilter
|
||||
readonly addExtraTags: Tag[]
|
||||
}[]
|
||||
public readonly mappings?: Mapping[]
|
||||
public readonly labels: string[]
|
||||
|
||||
constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
|
||||
|
@ -72,15 +75,15 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
|
||||
let translationKey = context;
|
||||
if(json["id"] !== undefined){
|
||||
if (json["id"] !== undefined) {
|
||||
const layerId = context.split(".")[0]
|
||||
if(json["source"]){
|
||||
let src = json["source"]+":"
|
||||
if(json["source"] === "shared-questions"){
|
||||
if (json["source"]) {
|
||||
let src = json["source"] + ":"
|
||||
if (json["source"] === "shared-questions") {
|
||||
src += "shared_questions."
|
||||
}
|
||||
translationKey = `${src}${json["id"] ?? ""}`
|
||||
}else{
|
||||
} else {
|
||||
translationKey = `layers:${layerId}.tagRenderings.${json["id"] ?? ""}`
|
||||
}
|
||||
}
|
||||
|
@ -110,19 +113,19 @@ export default class TagRenderingConfig {
|
|||
throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})`
|
||||
}
|
||||
const type = json.freeform.type ?? "string"
|
||||
|
||||
if(ValidatedTextField.AvailableTypes().indexOf(type ) < 0){
|
||||
throw "At "+context+".freeform.type is an unknown type: "+type+"; try one of "+ValidatedTextField.AvailableTypes().join(", ")
|
||||
|
||||
if (ValidatedTextField.AvailableTypes().indexOf(type) < 0) {
|
||||
throw "At " + context + ".freeform.type is an unknown type: " + type + "; try one of " + ValidatedTextField.AvailableTypes().join(", ")
|
||||
}
|
||||
|
||||
let placeholder: Translation = Translations.T(json.freeform.placeholder)
|
||||
if (placeholder === undefined) {
|
||||
const typeDescription = <Translation> Translations.t.validation[type]?.description
|
||||
const typeDescription = <Translation>Translations.t.validation[type]?.description
|
||||
const key = json.freeform.key;
|
||||
if(typeDescription !== undefined){
|
||||
placeholder = typeDescription.OnEveryLanguage(l => key+" ("+l+")")
|
||||
}else{
|
||||
placeholder = Translations.T(key+" ("+type+")")
|
||||
if (typeDescription !== undefined) {
|
||||
placeholder = typeDescription.OnEveryLanguage(l => key + " (" + l + ")")
|
||||
} else {
|
||||
placeholder = Translations.T(key + " (" + type + ")")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,75 +177,7 @@ export default class TagRenderingConfig {
|
|||
throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
|
||||
}
|
||||
|
||||
this.mappings = json.mappings.map((mapping, i) => {
|
||||
|
||||
const ctx = `${translationKey}.mappings.${i}`
|
||||
if (mapping.if === undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (mapping.then === undefined) {
|
||||
if(mapping["render"] !== undefined){
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (mapping.ifnot !== undefined && !this.multiAnswer) {
|
||||
throw `${ctx}: Invalid mapping: 'ifnot' is defined, but the tagrendering is not a multianswer. Either remove ifnot or set 'multiAnswer:true' to enable checkboxes instead of radiobuttons`
|
||||
}
|
||||
|
||||
if(mapping["render"] !== undefined){
|
||||
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
|
||||
}
|
||||
|
||||
if (mapping.addExtraTags !== undefined && this.multiAnswer) {
|
||||
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed`
|
||||
}
|
||||
|
||||
let hideInAnswer: boolean | TagsFilter = false;
|
||||
if (typeof mapping.hideInAnswer === "boolean") {
|
||||
hideInAnswer = mapping.hideInAnswer;
|
||||
} else if (mapping.hideInAnswer !== undefined) {
|
||||
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
|
||||
}
|
||||
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`));
|
||||
if(hideInAnswer === true && addExtraTags.length > 0){
|
||||
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
|
||||
}
|
||||
|
||||
let icon = undefined;
|
||||
let iconClass = "small"
|
||||
if(mapping.icon !== undefined){
|
||||
if (typeof mapping.icon === "string" && mapping.icon !== "") {
|
||||
icon = mapping.icon
|
||||
}else{
|
||||
icon = mapping.icon["path"]
|
||||
iconClass = mapping.icon["class"] ?? iconClass
|
||||
}
|
||||
}
|
||||
const mp = {
|
||||
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
|
||||
then: Translations.T(mapping.then, `${ctx}.then`),
|
||||
hideInAnswer,
|
||||
icon,
|
||||
iconClass,
|
||||
addExtraTags
|
||||
};
|
||||
if (this.question) {
|
||||
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
|
||||
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
}
|
||||
|
||||
if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) {
|
||||
throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
}
|
||||
}
|
||||
|
||||
return mp;
|
||||
});
|
||||
this.mappings = json.mappings.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, translationKey, context, this.multiAnswer, this.question !== undefined));
|
||||
}
|
||||
|
||||
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
|
||||
|
@ -351,6 +286,79 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
public static ExtractMapping(mapping: MappingConfigJson, i: number, translationKey: string,
|
||||
context: string,
|
||||
multiAnswer?: boolean, isQuestionable?: boolean) {
|
||||
|
||||
const ctx = `${translationKey}.mappings.${i}`
|
||||
if (mapping.if === undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (mapping.then === undefined) {
|
||||
if (mapping["render"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (mapping.ifnot !== undefined && !multiAnswer) {
|
||||
throw `${ctx}: Invalid mapping: 'ifnot' is defined, but the tagrendering is not a multianswer. Either remove ifnot or set 'multiAnswer:true' to enable checkboxes instead of radiobuttons`
|
||||
}
|
||||
|
||||
if (mapping["render"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}`
|
||||
}
|
||||
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
|
||||
}
|
||||
|
||||
if (mapping.addExtraTags !== undefined && multiAnswer) {
|
||||
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed`
|
||||
}
|
||||
|
||||
let hideInAnswer: boolean | TagsFilter = false;
|
||||
if (typeof mapping.hideInAnswer === "boolean") {
|
||||
hideInAnswer = mapping.hideInAnswer;
|
||||
} else if (mapping.hideInAnswer !== undefined) {
|
||||
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
|
||||
}
|
||||
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`));
|
||||
if (hideInAnswer === true && addExtraTags.length > 0) {
|
||||
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
|
||||
}
|
||||
|
||||
let icon = undefined;
|
||||
let iconClass = "small"
|
||||
if (mapping.icon !== undefined) {
|
||||
if (typeof mapping.icon === "string" && mapping.icon !== "") {
|
||||
icon = mapping.icon
|
||||
} else {
|
||||
icon = mapping.icon["path"]
|
||||
iconClass = mapping.icon["class"] ?? iconClass
|
||||
}
|
||||
}
|
||||
const mp = <Mapping>{
|
||||
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
|
||||
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
|
||||
then: Translations.T(mapping.then, `${ctx}.then`),
|
||||
hideInAnswer,
|
||||
icon,
|
||||
iconClass,
|
||||
addExtraTags,
|
||||
searchTerms: mapping.searchTerms
|
||||
};
|
||||
if (isQuestionable) {
|
||||
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
|
||||
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
}
|
||||
|
||||
if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) {
|
||||
throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
|
||||
}
|
||||
}
|
||||
|
||||
return mp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if it is known or not shown, false if the question should be asked
|
||||
* @constructor
|
||||
|
@ -417,16 +425,17 @@ export default class TagRenderingConfig {
|
|||
return undefined;
|
||||
}))
|
||||
|
||||
if(freeformKeyDefined && tags[this.freeform.key] !== undefined){
|
||||
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
|
||||
const freeformValues = tags[this.freeform.key].split(";")
|
||||
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
|
||||
for (const leftover of leftovers) {
|
||||
applicableMappings.push({then:
|
||||
new TypedTranslation<object>(this.render.replace("{"+this.freeform.key+"}", leftover).translations)
|
||||
applicableMappings.push({
|
||||
then:
|
||||
new TypedTranslation<object>(this.render.replace("{" + this.freeform.key + "}", leftover).translations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return applicableMappings
|
||||
}
|
||||
|
||||
|
@ -440,12 +449,12 @@ export default class TagRenderingConfig {
|
|||
* @constructor
|
||||
*/
|
||||
public GetRenderValueWithImage(tags: any, defltValue: any = undefined): { then: TypedTranslation<any>, icon?: string } | undefined {
|
||||
if(this.condition !== undefined){
|
||||
if(!this.condition.matchesProperties(tags)){
|
||||
if (this.condition !== undefined) {
|
||||
if (!this.condition.matchesProperties(tags)) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.mappings !== undefined && !this.multiAnswer) {
|
||||
for (const mapping of this.mappings) {
|
||||
if (mapping.if === undefined) {
|
||||
|
@ -549,10 +558,10 @@ export default class TagRenderingConfig {
|
|||
if (this.mappings !== undefined) {
|
||||
mappings = new List(
|
||||
[].concat(...this.mappings.map(m => {
|
||||
const msgs: (string| BaseUIElement)[] = [
|
||||
const msgs: (string | BaseUIElement)[] = [
|
||||
new Combine(
|
||||
[
|
||||
new FixedUiElement(m.then.txt).SetClass("bold"),
|
||||
new FixedUiElement(m.then.txt).SetClass("bold"),
|
||||
"corresponds with ",
|
||||
m.if.asHumanString(true, false, {})
|
||||
]
|
||||
|
@ -562,30 +571,30 @@ export default class TagRenderingConfig {
|
|||
msgs.push(new FixedUiElement("This option cannot be chosen as answer").SetClass("italic"))
|
||||
}
|
||||
if (m.ifnot !== undefined) {
|
||||
msgs.push( "Unselecting this answer will add " + m.ifnot.asHumanString(true, false, {}))
|
||||
msgs.push("Unselecting this answer will add " + m.ifnot.asHumanString(true, false, {}))
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
let condition : BaseUIElement = undefined
|
||||
if(this.condition !== undefined && !this.condition?.matchesProperties({})){
|
||||
|
||||
let condition: BaseUIElement = undefined
|
||||
if (this.condition !== undefined && !this.condition?.matchesProperties({})) {
|
||||
condition = new Combine(["Only visible if ",
|
||||
new FixedUiElement( this.condition.asHumanString(false, false, {})
|
||||
new FixedUiElement(this.condition.asHumanString(false, false, {})
|
||||
).SetClass("code")
|
||||
, " is shown"])
|
||||
, " is shown"])
|
||||
}
|
||||
|
||||
let group : BaseUIElement = undefined
|
||||
if(this.group !== undefined && this.group !== ""){
|
||||
|
||||
let group: BaseUIElement = undefined
|
||||
if (this.group !== undefined && this.group !== "") {
|
||||
group = new Combine([
|
||||
"This tagrendering is part of group ", new FixedUiElement(this.group).SetClass("code")
|
||||
])
|
||||
}
|
||||
let labels : BaseUIElement = undefined
|
||||
if(this.labels?.length > 0){
|
||||
let labels: BaseUIElement = undefined
|
||||
if (this.labels?.length > 0) {
|
||||
labels = new Combine([
|
||||
"This tagrendering has labels ",
|
||||
...this.labels.map(label => new FixedUiElement(label).SetClass("code"))
|
||||
|
@ -594,9 +603,9 @@ export default class TagRenderingConfig {
|
|||
return new Combine([
|
||||
new Title(this.id, 3),
|
||||
this.question !== undefined ?
|
||||
new Combine([ "The question is " , new FixedUiElement( this.question.txt).SetClass("bold")]) :
|
||||
new Combine(["The question is ", new FixedUiElement(this.question.txt).SetClass("bold")]) :
|
||||
new FixedUiElement(
|
||||
"This tagrendering has no question and is thus read-only"
|
||||
"This tagrendering has no question and is thus read-only"
|
||||
).SetClass("italic"),
|
||||
new Combine(withRender),
|
||||
mappings,
|
||||
|
|
Loading…
Reference in a new issue