forked from MapComplete/MapComplete
		
	Move 'mappings' to a separate interface
This commit is contained in:
		
							parent
							
								
									bb10f60636
								
							
						
					
					
						commit
						0146d1e4f7
					
				
					 4 changed files with 270 additions and 225 deletions
				
			
		|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue