forked from MapComplete/MapComplete
		
	Fix questions: applicable mappings are now calculated dynamically; charging station theme now only shows applicable plugs based on the allowed vehicle types
This commit is contained in:
		
							parent
							
								
									bedc576313
								
							
						
					
					
						commit
						df34239256
					
				
					 14 changed files with 3677 additions and 3359 deletions
				
			
		|  | @ -11,11 +11,15 @@ export class FixedInputElement<T> extends InputElement<T> { | |||
|     private readonly _el: HTMLElement; | ||||
| 
 | ||||
|     constructor(rendering: BaseUIElement | string, | ||||
|                 value: T, | ||||
|                 value: T | UIEventSource<T>, | ||||
|                 comparator: ((t0: T, t1: T) => boolean) = undefined) { | ||||
|         super(); | ||||
|         this._comparator = comparator ?? ((t0, t1) => t0 == t1); | ||||
|         this.value = new UIEventSource<T>(value); | ||||
|         if(value instanceof UIEventSource){ | ||||
|             this.value = value | ||||
|         }else{ | ||||
|             this.value = new UIEventSource<T>(value); | ||||
|         } | ||||
| 
 | ||||
|         const selected = this.IsSelected; | ||||
|         this._el = document.createElement("span") | ||||
|  |  | |||
|  | @ -179,26 +179,4 @@ export class RadioButton<T> extends InputElement<T> { | |||
|         return form; | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|       public ShowValue(t: T): boolean { | ||||
|           if (t === undefined) { | ||||
|               return false; | ||||
|           } | ||||
|           if (!this.IsValid(t)) { | ||||
|               return false; | ||||
|           } | ||||
|           // We check that what is selected matches the previous rendering
 | ||||
|           for (let i = 0; i < this._elements.length; i++) { | ||||
|               const e = this._elements[i]; | ||||
|               if (e.IsValid(t)) { | ||||
|                   this._selectedElementIndex.setData(i); | ||||
|                   e.GetValue().setData(t); | ||||
|                   const radio = document.getElementById(this.IdFor(i)); | ||||
|                   // @ts-ignore
 | ||||
|                   radio?.checked = true; | ||||
|                   return; | ||||
|               } | ||||
|    | ||||
|           } | ||||
|       }*/ | ||||
| } | ||||
|  |  | |||
|  | @ -18,41 +18,53 @@ export default class EditableTagRendering extends Toggle { | |||
|                 units: Unit [], | ||||
|                 editMode = new UIEventSource<boolean>(false) | ||||
|     ) { | ||||
| 
 | ||||
|         // The tagrendering is hidden if:
 | ||||
|         // The answer is unknown. The questionbox will then show the question
 | ||||
|         // There is a condition hiding the answer
 | ||||
|         const renderingIsShown = tags.map(tags => | ||||
|             configuration.IsKnown(tags) && | ||||
|             (configuration?.condition?.matchesProperties(tags) ?? true)) | ||||
|         super( | ||||
|             new Lazy(() => EditableTagRendering.CreateRendering(tags, configuration, units, editMode)), | ||||
|             undefined, | ||||
|             renderingIsShown | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private static CreateRendering(tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>) : BaseUIElement{ | ||||
|         const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) | ||||
|         answer.SetClass("w-full") | ||||
|         let rendering = answer; | ||||
| 
 | ||||
|         if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) { | ||||
|             // We have a question and editing is enabled
 | ||||
|             const editButton = | ||||
|                 new Combine([Svg.pencil_ui()]).SetClass("block relative h-10 w-10 p-2 float-right").SetStyle("border: 1px solid black; border-radius: 0.7em") | ||||
|                     .onClick(() => { | ||||
|                         editMode.setData(true); | ||||
|                     }); | ||||
| 
 | ||||
| 
 | ||||
|             const answerWithEditButton = new Combine([answer, | ||||
|                 new Toggle(editButton,  | ||||
|                 new Toggle(new Combine([Svg.pencil_ui()]).SetClass("block relative h-10 w-10 p-2 float-right").SetStyle("border: 1px solid black; border-radius: 0.7em") | ||||
|                         .onClick(() => { | ||||
|                             editMode.setData(true); | ||||
|                         }), | ||||
|                     undefined, | ||||
|                     State.state.osmConnection.isLoggedIn) | ||||
|             ]).SetClass("flex justify-between w-full") | ||||
| 
 | ||||
| 
 | ||||
|             const cancelbutton = | ||||
|                 Translations.t.general.cancel.Clone() | ||||
|                     .SetClass("btn btn-secondary mr-3") | ||||
|                     .onClick(() => { | ||||
|                         editMode.setData(false) | ||||
|                     }); | ||||
|             const question = new Lazy(() => { | ||||
|                 return   new TagRenderingQuestion(tags, configuration, | ||||
|                     { | ||||
|                         units: units, | ||||
|                         cancelButton: Translations.t.general.cancel.Clone() | ||||
|                             .SetClass("btn btn-secondary mr-3") | ||||
|                             .onClick(() => { | ||||
|                                 editMode.setData(false) | ||||
|                             }), | ||||
|                         afterSave: () => { | ||||
|                             editMode.setData(false) | ||||
|                         } | ||||
|                     }) | ||||
| 
 | ||||
|             const question = new Lazy(() => new TagRenderingQuestion(tags, configuration, | ||||
|                 { | ||||
|                     units: units, | ||||
|                     cancelButton: cancelbutton, | ||||
|                     afterSave: () => { | ||||
|                         editMode.setData(false) | ||||
|                     } | ||||
|                 })) | ||||
| 
 | ||||
|             }) | ||||
| 
 | ||||
| 
 | ||||
|             rendering = new Toggle( | ||||
|  | @ -62,17 +74,7 @@ export default class EditableTagRendering extends Toggle { | |||
|             ) | ||||
|         } | ||||
|         rendering.SetClass("block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2") | ||||
|         // The tagrendering is hidden if:
 | ||||
|         // The answer is unknown. The questionbox will then show the question
 | ||||
|         // There is a condition hiding the answer
 | ||||
|         const renderingIsShown = tags.map(tags => | ||||
|             configuration.IsKnown(tags) && | ||||
|             (configuration?.condition?.matchesProperties(tags) ?? true)) | ||||
|         super( | ||||
|             rendering, | ||||
|             undefined, | ||||
|             renderingIsShown | ||||
|         ) | ||||
|         return rendering; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -31,7 +31,7 @@ import {Unit} from "../../Models/Unit"; | |||
|  * Shows the question element. | ||||
|  * Note that the value _migh_ already be known, e.g. when selected or when changing the value | ||||
|  */ | ||||
| export default class TagRenderingQuestion extends Combine { | ||||
| export default class TagRenderingQuestion extends VariableUiElement { | ||||
| 
 | ||||
|     constructor(tags: UIEventSource<any>, | ||||
|                 configuration: TagRenderingConfig, | ||||
|  | @ -43,6 +43,46 @@ export default class TagRenderingQuestion extends Combine { | |||
|                     bottomText?: (src: UIEventSource<TagsFilter>) => BaseUIElement | ||||
|                 } | ||||
|     ) { | ||||
| 
 | ||||
| 
 | ||||
|         const applicableMappings = | ||||
|             UIEventSource.ListStabilized(tags.map(tags => { | ||||
|                 const applicableMappings : {if: TagsFilter, then: any, ifnot?: TagsFilter}[] = [] | ||||
|                 for (const mapping of configuration.mappings) { | ||||
|                     if (mapping.hideInAnswer === true) { | ||||
|                         continue | ||||
|                     } | ||||
|                     if (mapping.hideInAnswer === false || mapping.hideInAnswer === undefined) { | ||||
|                        applicableMappings.push(mapping) | ||||
|                         continue | ||||
|                     } | ||||
|                     const condition = <TagsFilter> mapping.hideInAnswer; | ||||
|                     const isShown = !condition.matchesProperties(tags) | ||||
|                     if(isShown){ | ||||
|                         applicableMappings.push(mapping) | ||||
|                     } | ||||
|                 } | ||||
|                 return applicableMappings | ||||
|             })); | ||||
| 
 | ||||
|         super( | ||||
|             applicableMappings.map(applicableMappings => { | ||||
|                 return TagRenderingQuestion.GenerateFullQuestion(tags, applicableMappings, configuration, options) | ||||
|             }) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     private static GenerateFullQuestion(tags: UIEventSource<any>, | ||||
|                                         applicableMappings:  {if: TagsFilter, then: any, ifnot?: TagsFilter}[], | ||||
|                                         configuration: TagRenderingConfig, | ||||
|                                         options?: { | ||||
|                                             units?: Unit[], | ||||
|                                             afterSave?: () => void, | ||||
|                                             cancelButton?: BaseUIElement, | ||||
|                                             saveButtonConstr?: (src: UIEventSource<TagsFilter>) => BaseUIElement, | ||||
|                                             bottomText?: (src: UIEventSource<TagsFilter>) => BaseUIElement | ||||
|                                         } | ||||
|     ) { | ||||
|         if (configuration === undefined) { | ||||
|             throw "A question is needed for a question visualization" | ||||
|         } | ||||
|  | @ -52,7 +92,7 @@ export default class TagRenderingQuestion extends Combine { | |||
|             .SetClass("question-text"); | ||||
| 
 | ||||
| 
 | ||||
|         const inputElement: InputElement<TagsFilter> = TagRenderingQuestion.GenerateInputElement(configuration, applicableUnit, tags) | ||||
|         const inputElement: InputElement<TagsFilter> = TagRenderingQuestion.GenerateInputElement(configuration, applicableMappings, applicableUnit, tags) | ||||
| 
 | ||||
|         if (inputElement === undefined) { | ||||
|             console.error("MultiAnswer failed - probably not a single option was possible", configuration) | ||||
|  | @ -61,7 +101,7 @@ export default class TagRenderingQuestion extends Combine { | |||
|         const save = async () => { | ||||
|             const selection = inputElement.GetValue().data; | ||||
|             if (selection) { | ||||
|                await (State.state?.changes ?? new Changes()) | ||||
|                 await (State.state?.changes ?? new Changes()) | ||||
|                     .applyAction(new ChangeTagAction( | ||||
|                         tags.data.id, selection, tags.data | ||||
|                     )) | ||||
|  | @ -103,52 +143,77 @@ export default class TagRenderingQuestion extends Combine { | |||
|                 ) | ||||
|             ).SetClass("block break-all") | ||||
|         } | ||||
|         super([ | ||||
|         return new Combine([ | ||||
|             question, | ||||
|             inputElement, | ||||
|             options.cancelButton, | ||||
|             saveButton, | ||||
|             bottomTags] | ||||
|         ) | ||||
|         this.SetClass("question") | ||||
|         ).SetClass("question") | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private static GenerateInputElement(configuration: TagRenderingConfig, applicableUnit: Unit, tagsSource: UIEventSource<any>): InputElement<TagsFilter> { | ||||
|     private static GenerateInputElement(configuration: TagRenderingConfig,  | ||||
|                                         applicableMappings: {if: TagsFilter, then: any, ifnot?: TagsFilter}[], | ||||
|                                         applicableUnit: Unit, tagsSource: UIEventSource<any>): InputElement<TagsFilter> { | ||||
|         let inputEls: InputElement<TagsFilter>[]; | ||||
| 
 | ||||
|         const mappings = (configuration.mappings ?? []) | ||||
|             .filter(mapping => { | ||||
|                 if (mapping.hideInAnswer === true) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 return !(typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(tagsSource.data)); | ||||
| 
 | ||||
|             }) | ||||
|         const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined) | ||||
| 
 | ||||
| 
 | ||||
|         function allIfNotsExcept(excludeIndex: number): TagsFilter[] { | ||||
|             if (configuration.mappings === undefined) { | ||||
|                 return [] | ||||
|         function allIfNotsExcept(excludeIndex: number): UIEventSource<TagsFilter[]> { | ||||
|             if (configuration.mappings === undefined || configuration.mappings.length === 0) { | ||||
|                 return undefined | ||||
|             } | ||||
|             if (!ifNotsPresent) { | ||||
|                 return undefined | ||||
|             } | ||||
|             if (configuration.multiAnswer) { | ||||
|                 // The multianswer will do the ifnot configuration themself
 | ||||
|                 return [] | ||||
|                 return undefined | ||||
|             } | ||||
|             return Utils.NoNull(configuration.mappings?.map((m, i) => excludeIndex === i ? undefined : m.ifnot)) | ||||
|             return tagsSource.map(currentTags => { | ||||
|                 const negativeMappings = [] | ||||
| 
 | ||||
|                 for (let i = 0; i < configuration.mappings.length; i++) { | ||||
|                     const mapping = configuration.mappings[i]; | ||||
|                     if (i === excludeIndex || mapping.ifnot === undefined) { | ||||
|                         continue | ||||
|                     } | ||||
| 
 | ||||
|                     const hidden = mapping.hideInAnswer | ||||
|                     if (hidden === undefined) { | ||||
|                         negativeMappings.push(mapping.ifnot) | ||||
|                         continue | ||||
|                     } | ||||
|                     if (hidden === true) { | ||||
|                         continue | ||||
|                     } | ||||
| 
 | ||||
|                     if ((<TagsFilter>hidden).matchesProperties(currentTags)) { | ||||
|                         // This option is currently hidden
 | ||||
|                         continue | ||||
|                     } | ||||
|                     negativeMappings.push(mapping.ifnot) | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
|                 return Utils.NoNull(negativeMappings) | ||||
|             }) | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource); | ||||
|         const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 | ||||
|         const hasImages = applicableMappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0 | ||||
| 
 | ||||
|         if (mappings.length < 8 || configuration.multiAnswer || hasImages) { | ||||
|             inputEls = (mappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(tagsSource, mapping, allIfNotsExcept(i))); | ||||
|         if (applicableMappings.length < 8 || configuration.multiAnswer || hasImages || ifNotsPresent) { | ||||
|             inputEls = (applicableMappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(tagsSource, mapping, allIfNotsExcept(i))); | ||||
|             inputEls = Utils.NoNull(inputEls); | ||||
|         } else { | ||||
|             const dropdown: InputElement<TagsFilter> = new DropDown("", | ||||
|                 mappings.map((mapping, i) => { | ||||
|                 applicableMappings.map((mapping, i) => { | ||||
|                     return { | ||||
|                         value: new And([mapping.if, ...allIfNotsExcept(i)]), | ||||
|                         value: new And([mapping.if, ...allIfNotsExcept(i).data]), | ||||
|                         shown: Translations.WT(mapping.then).Clone() | ||||
|                     } | ||||
|                 }) | ||||
|  | @ -178,6 +243,7 @@ export default class TagRenderingQuestion extends Combine { | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static GenerateMultiAnswer( | ||||
|         configuration: TagRenderingConfig, | ||||
|         elements: InputElement<TagsFilter>[], freeformField: InputElement<TagsFilter>, ifNotSelected: TagsFilter[]): InputElement<TagsFilter> { | ||||
|  | @ -278,17 +344,23 @@ export default class TagRenderingQuestion extends Combine { | |||
|         return inputEl; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Generates a (Fixed) input element for this mapping. | ||||
|      * Note that the mapping might hide itself if the condition is not met anymore. | ||||
|      * | ||||
|      * Returns: [the element itself, the value to select if not selected. The contents of this UIEventSource might swap to undefined if the conditions to show the answer are unmet] | ||||
|      */ | ||||
|     private static GenerateMappingElement( | ||||
|         tagsSource: UIEventSource<any>, | ||||
|         mapping: { | ||||
|             if: TagsFilter, | ||||
|             then: Translation, | ||||
|             hideInAnswer: boolean | TagsFilter | ||||
|         }, ifNot?: TagsFilter[]): InputElement<TagsFilter> { | ||||
|         }, ifNot?: UIEventSource<TagsFilter[]>): InputElement<TagsFilter> { | ||||
| 
 | ||||
|         let tagging = mapping.if; | ||||
|         if (ifNot.length > 0) { | ||||
|             tagging = new And([tagging, ...ifNot]) | ||||
|         let tagging: TagsFilter | UIEventSource<TagsFilter> = mapping.if; | ||||
|         if (ifNot !== undefined) { | ||||
|             tagging = ifNot.map(ifNots => new And([mapping.if, ...ifNots])) | ||||
|         } | ||||
| 
 | ||||
|         return new FixedInputElement( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue