mapcomplete/Models/ThemeConfig/TagRenderingConfig.ts

339 lines
13 KiB
TypeScript
Raw Normal View History

2020-11-06 01:58:26 +01:00
import {Translation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
import Translations from "../../UI/i18n/Translations";
2021-03-29 00:41:53 +02:00
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {And} from "../../Logic/Tags/And";
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
import {Utils} from "../../Utils";
2020-10-27 01:01:34 +01:00
/***
* The parsed version of TagRenderingConfigJSON
* Identical data, but with some methods and validation
*/
export default class TagRenderingConfig {
readonly render?: Translation;
readonly question?: Translation;
readonly condition?: TagsFilter;
readonly configuration_warnings: string[] = []
2020-10-27 01:01:34 +01:00
readonly freeform?: {
readonly key: string,
readonly type: string,
readonly addExtraTags: TagsFilter[];
readonly inline: boolean,
readonly default?: string,
readonly helperArgs?: (string | number | boolean)[]
2020-10-27 01:01:34 +01:00
};
readonly multiAnswer: boolean;
2020-10-27 01:01:34 +01:00
readonly mappings?: {
readonly if: TagsFilter,
readonly ifnot?: TagsFilter,
readonly then: Translation
readonly hideInAnswer: boolean | TagsFilter
2020-10-27 01:01:34 +01:00
}[]
readonly roaming: boolean;
2020-10-27 01:01:34 +01:00
constructor(json: string | TagRenderingConfigJson, conditionIfRoaming: TagsFilter, context?: string) {
2020-10-27 01:01:34 +01:00
2020-12-08 23:44:34 +01:00
if (json === "questions") {
// Very special value
this.render = null;
this.question = null;
this.condition = null;
}
if (json === undefined) {
throw "Initing a TagRenderingConfig with undefined in " + context;
2020-10-27 01:01:34 +01:00
}
if (typeof json === "string") {
this.render = Translations.T(json, context + ".render");
2020-10-27 01:01:34 +01:00
this.multiAnswer = false;
return;
}
this.render = Translations.T(json.render, context + ".render");
this.question = Translations.T(json.question, context + ".question");
this.roaming = json.roaming ?? false;
const condition = TagUtils.Tag(json.condition ?? {"and": []}, `${context}.condition`);
if (this.roaming && conditionIfRoaming !== undefined) {
this.condition = new And([condition, conditionIfRoaming]);
} else {
this.condition = condition;
}
2020-10-27 01:01:34 +01:00
if (json.freeform) {
if(json.freeform.addExtraTags !== undefined && json.freeform.addExtraTags.map === undefined){
throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})`
}
2020-10-27 01:01:34 +01:00
this.freeform = {
key: json.freeform.key,
type: json.freeform.type ?? "string",
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default,
helperArgs: json.freeform.helperArgs
2020-10-27 01:01:34 +01:00
}
if (json.freeform["extraTags"] !== undefined) {
throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
}
if (this.freeform.key === undefined || this.freeform.key === "") {
2021-03-10 20:18:05 +01:00
throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}`
}
if (json.freeform["args"] !== undefined) {
2021-07-29 00:29:29 +02:00
throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})`
}
2020-10-27 01:01:34 +01:00
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
2021-04-18 14:24:30 +02:00
const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", ");
throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
2020-10-27 01:01:34 +01:00
}
if (this.freeform.addExtraTags) {
2021-03-10 20:18:05 +01:00
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
if (usedKeys.indexOf(this.freeform.key) >= 0) {
2021-03-10 20:18:05 +01:00
throw `The freeform key ${this.freeform.key} will be overwritten by one of the extra tags, as they use the same key too. This is in ${context}`;
}
}
2020-10-27 01:01:34 +01:00
}
this.multiAnswer = json.multiAnswer ?? false
if (json.mappings) {
if (!Array.isArray(json.mappings)) {
throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
2021-04-10 03:50:44 +02:00
}
2020-10-27 01:01:34 +01:00
this.mappings = json.mappings.map((mapping, i) => {
2020-10-27 01:01:34 +01:00
if (mapping.then === undefined) {
throw `${context}.mapping[${i}]: Invalid mapping: if without body`
}
if (mapping.ifnot !== undefined && !this.multiAnswer) {
throw `${context}.mapping[${i}]: Invalid mapping: ifnot defined, but the tagrendering is not a multianswer`
2020-10-27 01:01:34 +01:00
}
if (mapping.if === undefined) {
throw `${context}.mapping[${i}]: Invalid mapping: "if" is not defined, but the tagrendering is not a multianswer`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
throw `${context}.mapping[${i}]: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
}
let hideInAnswer: boolean | TagsFilter = false;
2020-12-08 23:44:34 +01:00
if (typeof mapping.hideInAnswer === "boolean") {
2020-12-07 03:02:50 +01:00
hideInAnswer = mapping.hideInAnswer;
2020-12-08 23:44:34 +01:00
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
2020-12-07 03:02:50 +01:00
}
const mappingContext = `${context}.mapping[${i}]`
const mp = {
if: TagUtils.Tag(mapping.if, `${mappingContext}.if`),
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${mappingContext}.ifnot`) : undefined),
then: Translations.T(mapping.then, `{mappingContext}.then`),
2020-12-07 03:02:50 +01:00
hideInAnswer: hideInAnswer
2020-10-27 01:01:34 +01:00
};
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;
2020-10-27 01:01:34 +01:00
});
}
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
throw `${context}: A question is defined, but no mappings nor freeform (key) are. The question is ${this.question.txt} at ${context}`
2020-10-27 01:01:34 +01:00
}
if (this.freeform && this.render === undefined) {
throw `${context}: Detected a freeform key without rendering... Key: ${this.freeform.key} in ${context}`
2021-01-12 20:46:01 +01:00
}
if (this.render && this.question && this.freeform === undefined) {
throw `${context}: Detected a tagrendering which takes input without freeform key in ${context}; the question is ${this.question.txt}`
2021-03-10 12:55:39 +01:00
}
if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
let keys = []
for (let i = 0; i < this.mappings.length; i++) {
const mapping = this.mappings[i];
if (mapping.if === undefined) {
throw `${context}.mappings[${i}].if is undefined`
}
keys.push(...mapping.if.usedKeys())
}
keys = Utils.Dedup(keys)
for (let i = 0; i < this.mappings.length; i++) {
const mapping = this.mappings[i];
if (mapping.hideInAnswer) {
continue
}
const usedKeys = mapping.if.usedKeys();
for (const expectedKey of keys) {
if (usedKeys.indexOf(expectedKey) < 0) {
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(', ')}, but it should also give a value for ${expectedKey}`
2021-03-14 20:14:51 +01:00
this.configuration_warnings.push(msg)
}
}
}
}
2020-10-27 01:01:34 +01:00
if (this.question !== undefined && json.multiAnswer) {
2020-10-27 01:01:34 +01:00
if ((this.mappings?.length ?? 0) === 0) {
throw `${context} MultiAnswer is set, but no mappings are defined`
}
let allKeys = [];
let allHaveIfNot = true;
for (const mapping of this.mappings) {
if (mapping.hideInAnswer) {
continue;
}
if (mapping.ifnot === undefined) {
allHaveIfNot = false;
}
allKeys = allKeys.concat(mapping.if.usedKeys());
}
allKeys = Utils.Dedup(allKeys);
if (allKeys.length > 1 && !allHaveIfNot) {
throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping`
2020-10-27 01:01:34 +01:00
}
}
}
/**
* Returns true if it is known or not shown, false if the question should be asked
* @constructor
*/
public IsKnown(tags: any): boolean {
if (this.condition &&
!this.condition.matchesProperties(tags)) {
// Filtered away by the condition
return true;
}
if (this.multiAnswer) {
2021-04-18 14:24:30 +02:00
for (const m of this.mappings ?? []) {
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
return true;
}
}
const free = this.freeform?.key
if (free !== undefined) {
return tags[free] !== undefined
}
return false
}
if (this.GetRenderValue(tags) !== undefined) {
// This value is known and can be rendered
return true;
}
return false;
}
public IsQuestionBoxElement(): boolean {
return this.question === null && this.condition === null;
}
2021-06-14 02:39:23 +02:00
/**
* Gets all the render values. Will return multiple render values if 'multianswer' is enabled.
* The result will equal [GetRenderValue] if not 'multiAnswer'
* @param tags
* @constructor
*/
public GetRenderValues(tags: any): Translation[] {
if (!this.multiAnswer) {
2021-06-14 02:39:23 +02:00
return [this.GetRenderValue(tags)]
}
// 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
let freeformKeyUsed = this.freeform?.key === undefined;
2021-06-14 02:39:23 +02:00
// We run over all the mappings first, to check if the mapping matches
const applicableMappings: Translation[] = Utils.NoNull((this.mappings ?? [])?.map(mapping => {
if (mapping.if === undefined) {
return mapping.then;
}
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
if (!freeformKeyUsed) {
if (mapping.if.usedKeys().indexOf(this.freeform.key) >= 0) {
2021-06-14 02:39:23 +02:00
// This mapping matches the freeform key - we mark the freeform key to be ignored!
freeformKeyUsed = true;
}
}
return mapping.then;
}
return undefined;
}))
2021-06-14 02:39:23 +02:00
if (!freeformKeyUsed
&& tags[this.freeform.key] !== undefined) {
applicableMappings.push(this.render)
}
return applicableMappings
}
2020-10-27 01:01:34 +01:00
/**
* Gets the correct rendering value (or undefined if not known)
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
2020-10-27 01:01:34 +01:00
* @constructor
*/
public GetRenderValue(tags: any): Translation {
if (this.mappings !== undefined && !this.multiAnswer) {
for (const mapping of this.mappings) {
if (mapping.if === undefined) {
return mapping.then;
}
if (mapping.if.matchesProperties(tags)) {
return mapping.then;
}
}
}
if (this.freeform?.key === undefined) {
2020-10-27 01:01:34 +01:00
return this.render;
}
if (tags[this.freeform.key] !== undefined) {
return this.render;
}
return undefined;
}
public ExtractImages(isIcon: boolean): Set<string> {
const usedIcons = new Set<string>()
this.render?.ExtractImages(isIcon)?.forEach(usedIcons.add, usedIcons)
for (const mapping of this.mappings ?? []) {
mapping.then.ExtractImages(isIcon).forEach(usedIcons.add, usedIcons)
}
return usedIcons;
}
2020-10-27 01:01:34 +01:00
2020-10-27 01:01:34 +01:00
}