forked from MapComplete/MapComplete
		
	Feat: allow to disable questions (and to enable them again), fix #256
This commit is contained in:
		
							parent
							
								
									f8ef32f123
								
							
						
					
					
						commit
						93ebdd8e16
					
				
					 8 changed files with 238 additions and 76 deletions
				
			
		|  | @ -738,6 +738,14 @@ | |||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "id": "disabled-questions", | ||||
|       "render": { | ||||
|         "special": { | ||||
|           "type": "disabled_questions" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "title-privacy-legal", | ||||
|       "render": { | ||||
|  |  | |||
|  | @ -545,4 +545,14 @@ export default class UserRelatedState { | |||
| 
 | ||||
|         return amendedPrefs | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * The disabled questions for this theme and layer | ||||
|      */ | ||||
|     public getThemeDisabled(themeId: string, layerId: string): UIEventSource<string[]> { | ||||
|         const flatSource = this.osmConnection.getPreference("disabled-questions-" + themeId + "-" + layerId, "[]") | ||||
|         return UIEventSource.asObject<string[]>(flatSource, []) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -9,17 +9,17 @@ | |||
|   export let open = new UIEventSource(false) | ||||
|   export let dotsSize = `w-6 h-6` | ||||
|   export let dotsPosition = `top-0 right-0` | ||||
|   export let hideBackground=  false | ||||
|   export let hideBackground: boolean = false | ||||
|   let menuPosition = `` | ||||
|   if(dotsPosition.indexOf("left-0") >= 0){ | ||||
|   if (dotsPosition.indexOf("left-0") >= 0) { | ||||
|     menuPosition = "left-0" | ||||
|   }else{ | ||||
|   } else { | ||||
|     menuPosition = `right-0` | ||||
|   } | ||||
| 
 | ||||
|   if(dotsPosition.indexOf("top-0") > 0){ | ||||
|   if (dotsPosition.indexOf("top-0") > 0) { | ||||
|     menuPosition += " bottom-0" | ||||
|   }else{ | ||||
|   } else { | ||||
|     menuPosition += ` top-0` | ||||
|   } | ||||
| 
 | ||||
|  | @ -49,7 +49,7 @@ | |||
|     } | ||||
| 
 | ||||
|     :global(.dots-menu > path) { | ||||
|         fill: var(--interactive-background); | ||||
|         fill: var(--button-background-hover); | ||||
|         transition: fill 350ms linear; | ||||
|         cursor: pointer; | ||||
| 
 | ||||
|  | @ -74,7 +74,7 @@ | |||
|     } | ||||
| 
 | ||||
|     .transition-background { | ||||
|         transition:  background-color 150ms linear; | ||||
|         transition: background-color 150ms linear; | ||||
|     } | ||||
| 
 | ||||
|     .transition-background.collapsed { | ||||
|  |  | |||
							
								
								
									
										23
									
								
								src/UI/Popup/DisabledQuestions.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/UI/Popup/DisabledQuestions.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| <script lang="ts"> | ||||
|   import DisabledQuestionsLayer from "./DisabledQuestionsLayer.svelte" | ||||
|   import { Stores } from "../../Logic/UIEventSource" | ||||
| 
 | ||||
|   /** | ||||
|    * Shows _all_ disabled questions | ||||
|    */ | ||||
|   export let state | ||||
|   let layers = state.layout.layers.filter(l => l.isNormal()) | ||||
| 
 | ||||
|   let allDisabled = Stores.concat<string>(layers.map(l => state.userRelatedState.getThemeDisabled(state.layout.id, l.id))).map(l => [].concat(...l)) | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <h3>Disabled questions</h3> | ||||
| {#if $allDisabled.length === 0} | ||||
|   To disable a question, click the three dots in the upper-right corner | ||||
| {:else} | ||||
|   To enable a question again, click it | ||||
|   {#each layers as layer (layer.id)} | ||||
|     <DisabledQuestionsLayer {state} {layer} /> | ||||
|   {/each} | ||||
| {/if} | ||||
							
								
								
									
										45
									
								
								src/UI/Popup/DisabledQuestionsLayer.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/UI/Popup/DisabledQuestionsLayer.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| <script lang="ts">/** | ||||
|  * Gives an overview of questions which are disabled for the given theme | ||||
|  */ | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import ThemeViewState from "../../Models/ThemeViewState" | ||||
| import Tr from "../Base/Tr.svelte" | ||||
| import { Translation } from "../i18n/Translation" | ||||
| import { XMarkIcon } from "@babeard/svelte-heroicons/mini" | ||||
| import ToSvelte from "../Base/ToSvelte.svelte" | ||||
| 
 | ||||
| export let layer: LayerConfig | ||||
| export let state: ThemeViewState | ||||
| 
 | ||||
| let disabledQuestions = state.userRelatedState.getThemeDisabled(state.layout.id, layer.id) | ||||
| 
 | ||||
| function getQuestion(id: string): Translation { | ||||
|   return layer.tagRenderings.find(q => q.id === id).question.Subs({}) | ||||
| } | ||||
| 
 | ||||
| function enable(idToEnable: string) { | ||||
|   const newList = disabledQuestions.data.filter(id => id !== idToEnable) | ||||
|   disabledQuestions.set(newList) | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| {#if $disabledQuestions.length > 0} | ||||
|   <div class="low-interaction p-2"> | ||||
| 
 | ||||
|     <h4 class="flex my-2"> | ||||
|       <div class="no-image-background block h-6 w-6"> | ||||
|         <ToSvelte construct={() => layer.defaultIcon()} /> | ||||
|       </div> | ||||
|       <Tr t={layer.name} /> | ||||
|     </h4> | ||||
|     <div class="flex"> | ||||
|       {#each $disabledQuestions as id} | ||||
|         <button class="badge button-unstyled" on:click={() => enable(id)}> | ||||
|           <Tr cls="ml-2" t={getQuestion(id)} /> | ||||
|           <XMarkIcon class="w-4 h-4 mr-2" /> | ||||
|         </button> | ||||
|       {/each} | ||||
|     </div> | ||||
|   </div> | ||||
| {/if} | ||||
|  | @ -43,11 +43,20 @@ | |||
|     } | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   const baseQuestions = (layer?.tagRenderings ?? [])?.filter( | ||||
|     (tr) => allowed(tr.labels) && tr.question !== undefined | ||||
|     (tr) => allowed(tr.labels) && tr.question !== undefined, | ||||
|   ) | ||||
| 
 | ||||
| 
 | ||||
|   /** | ||||
|    * Ids of skipped questions | ||||
|    */ | ||||
|   let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>()) | ||||
|   let layerDisabledForTheme = state.userRelatedState.getThemeDisabled(state.layout.id, layer.id) | ||||
|   layerDisabledForTheme.addCallbackAndRunD(disabled => { | ||||
|     skippedQuestions.set(new Set(disabled.concat(Array.from(skippedQuestions.data)))) | ||||
|   }) | ||||
|   let questionboxElem: HTMLDivElement | ||||
|   let questionsToAsk = tags.map( | ||||
|     (tags) => { | ||||
|  | @ -69,10 +78,10 @@ | |||
|       } | ||||
|       return questionsToAsk | ||||
|     }, | ||||
|     [skippedQuestions] | ||||
|     [skippedQuestions], | ||||
|   ) | ||||
|   let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>( | ||||
|     undefined | ||||
|     undefined, | ||||
|   ) | ||||
|   let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource< | ||||
|     TagRenderingConfig[] | ||||
|  | @ -95,6 +104,8 @@ | |||
|   let skipped: number = 0 | ||||
| 
 | ||||
|   let loginEnabled = state.featureSwitches.featureSwitchEnableLogin | ||||
|   let debug = state.featureSwitches.featureSwitchIsDebugging | ||||
| 
 | ||||
| 
 | ||||
|   function skip(question: { id: string }, didAnswer: boolean = false) { | ||||
|     skippedQuestions.data.add(question.id) // Must use ID, the config object might be a copy of the original | ||||
|  | @ -117,43 +128,84 @@ | |||
|     class="marker-questionbox-root" | ||||
|     class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0} | ||||
|   > | ||||
|     {#if $showAllQuestionsAtOnce} | ||||
|       <div class="flex flex-col gap-y-1"> | ||||
|         {#each $allQuestionsToAsk as question (question.id)} | ||||
|           <TagRenderingQuestionDynamic | ||||
|             config={question} | ||||
|             {tags} | ||||
|             {selectedElement} | ||||
|             {state} | ||||
|             {layer} | ||||
|           /> | ||||
|         {/each} | ||||
|       </div> | ||||
|     {:else if $firstQuestion !== undefined} | ||||
|       <TagRenderingQuestionDynamic | ||||
|         config={$firstQuestion} | ||||
|         {layer} | ||||
|         {selectedElement} | ||||
|         {state} | ||||
|         {tags} | ||||
|         on:saved={() => { | ||||
|               skip($firstQuestion, true) | ||||
|             }} | ||||
|       > | ||||
|         <button | ||||
|           class="secondary" | ||||
|           on:click={() => { | ||||
|                 skip($firstQuestion) | ||||
|               }} | ||||
|           slot="cancel" | ||||
|         > | ||||
|           <Tr t={Translations.t.general.skip} /> | ||||
|         </button> | ||||
|       </TagRenderingQuestionDynamic> | ||||
|     {/if} | ||||
| 
 | ||||
|     {#if $allQuestionsToAsk.length === 0} | ||||
|       <div class="thanks"> | ||||
|         <Tr t={Translations.t.general.questionBox.done} /> | ||||
|       </div> | ||||
|     {/if} | ||||
| 
 | ||||
|     <div class="mt-4 mb-8"> | ||||
| 
 | ||||
|       {#if skipped + answered > 0} | ||||
|         <div class="thanks"> | ||||
|           <Tr t={Translations.t.general.questionBox.done} /> | ||||
|         </div> | ||||
|         {#if answered === 0} | ||||
|           {#if skipped === 1} | ||||
|             <Tr t={Translations.t.general.questionBox.skippedOne} /> | ||||
|           {:else} | ||||
|             <Tr t={Translations.t.general.questionBox.skippedMultiple.Subs({ skipped })} /> | ||||
|           {/if} | ||||
|         {:else if answered === 1} | ||||
|           {#if skipped === 0} | ||||
|             <Tr t={Translations.t.general.questionBox.answeredOne} /> | ||||
|         <div class="flex justify-center"> | ||||
|           {#if answered === 0} | ||||
|             {#if skipped === 1} | ||||
|               <Tr t={Translations.t.general.questionBox.skippedOne} /> | ||||
|             {:else} | ||||
|               <Tr t={Translations.t.general.questionBox.skippedMultiple.Subs({ skipped })} /> | ||||
|             {/if} | ||||
|           {:else if answered === 1} | ||||
|             {#if skipped === 0} | ||||
|               <Tr t={Translations.t.general.questionBox.answeredOne} /> | ||||
|             {:else if skipped === 1} | ||||
|               <Tr t={Translations.t.general.questionBox.answeredOneSkippedOne} /> | ||||
|             {:else} | ||||
|               <Tr | ||||
|                 t={Translations.t.general.questionBox.answeredOneSkippedMultiple.Subs({ skipped })} | ||||
|               /> | ||||
|             {/if} | ||||
|           {:else if skipped === 0} | ||||
|             <Tr t={Translations.t.general.questionBox.answeredMultiple.Subs({ answered })} /> | ||||
|           {:else if skipped === 1} | ||||
|             <Tr t={Translations.t.general.questionBox.answeredOneSkippedOne} /> | ||||
|             <Tr | ||||
|               t={Translations.t.general.questionBox.answeredMultipleSkippedOne.Subs({ answered })} | ||||
|             /> | ||||
|           {:else} | ||||
|             <Tr | ||||
|               t={Translations.t.general.questionBox.answeredOneSkippedMultiple.Subs({ skipped })} | ||||
|             /> | ||||
|           {/if} | ||||
|         {:else if skipped === 0} | ||||
|           <Tr t={Translations.t.general.questionBox.answeredMultiple.Subs({ answered })} /> | ||||
|         {:else if skipped === 1} | ||||
|           <Tr | ||||
|             t={Translations.t.general.questionBox.answeredMultipleSkippedOne.Subs({ answered })} | ||||
|           /> | ||||
|         {:else} | ||||
|           <Tr | ||||
|             t={Translations.t.general.questionBox.answeredMultipleSkippedMultiple.Subs({ | ||||
|               t={Translations.t.general.questionBox.answeredMultipleSkippedMultiple.Subs({ | ||||
|               answered, | ||||
|               skipped, | ||||
|             })} | ||||
|           /> | ||||
|         {/if} | ||||
|             /> | ||||
|           {/if} | ||||
|         </div> | ||||
| 
 | ||||
|         {#if skipped > 0} | ||||
|         {#if skipped + $skippedQuestions.size > 0} | ||||
|           <button | ||||
|             class="w-full" | ||||
|             on:click={() => { | ||||
|  | @ -163,45 +215,25 @@ | |||
|           > | ||||
|             <Tr t={Translations.t.general.questionBox.reactivate} /> | ||||
|           </button> | ||||
| 
 | ||||
|         {/if} | ||||
|       {/if} | ||||
|     {:else} | ||||
|       <div> | ||||
|         {#if $showAllQuestionsAtOnce} | ||||
|           <div class="flex flex-col gap-y-1"> | ||||
|             {#each $allQuestionsToAsk as question (question.id)} | ||||
|               <TagRenderingQuestionDynamic | ||||
|                 config={question} | ||||
|                 {tags} | ||||
|                 {selectedElement} | ||||
|                 {state} | ||||
|                 {layer} | ||||
|               /> | ||||
|             {/each} | ||||
|           </div> | ||||
|         {:else if $firstQuestion !== undefined} | ||||
|           <TagRenderingQuestionDynamic | ||||
|             config={$firstQuestion} | ||||
|             {layer} | ||||
|             {selectedElement} | ||||
|             {state} | ||||
|             {tags} | ||||
|             on:saved={() => { | ||||
|               skip($firstQuestion, true) | ||||
| 
 | ||||
|       {#if $skippedQuestions.size - skipped > 0} | ||||
|         <button | ||||
|           class="w-full" | ||||
|           on:click={() => { | ||||
|               skippedQuestions.setData(new Set()) | ||||
|               skipped = 0 | ||||
|             }} | ||||
|           > | ||||
|             <button | ||||
|               class="secondary" | ||||
|               on:click={() => { | ||||
|                 skip($firstQuestion) | ||||
|               }} | ||||
|               slot="cancel" | ||||
|             > | ||||
|               <Tr t={Translations.t.general.skip} /> | ||||
|             </button> | ||||
|           </TagRenderingQuestionDynamic> | ||||
|         {/if} | ||||
|       </div> | ||||
|     {/if} | ||||
|         > | ||||
|           Show the disabled questions for this object | ||||
|         </button> | ||||
| 
 | ||||
|       {/if} | ||||
|       {#if $debug} | ||||
|         Skipped questions are {Array.from($skippedQuestions).join(", ")} | ||||
|       {/if} | ||||
|     </div> | ||||
|   </div> | ||||
| {/if} | ||||
|  |  | |||
|  | @ -36,6 +36,8 @@ | |||
|   import { Modal } from "flowbite-svelte" | ||||
|   import Popup from "../../Base/Popup.svelte" | ||||
|   import If from "../../Base/If.svelte" | ||||
|   import DotMenu from "../../Base/DotMenu.svelte" | ||||
|   import SidebarUnit from "../../Base/SidebarUnit.svelte" | ||||
| 
 | ||||
|   export let config: TagRenderingConfig | ||||
|   export let tags: UIEventSource<Record<string, string>> | ||||
|  | @ -338,10 +340,41 @@ | |||
|       .then((changes) => state.changes.applyChanges(changes)) | ||||
|       .catch(console.error) | ||||
|   } | ||||
| 
 | ||||
|   let disabledInTheme = state.userRelatedState.getThemeDisabled(state.layout.id, layer?.id) | ||||
|   let menuIsOpened = new UIEventSource(false) | ||||
| 
 | ||||
|   function disableQuestion() { | ||||
|     const newList = Utils.Dedup([config.id, ...disabledInTheme.data]) | ||||
|     disabledInTheme.set(newList) | ||||
|     menuIsOpened.set(false) | ||||
|   } | ||||
| 
 | ||||
|   function enableQuestion() { | ||||
|     const newList = disabledInTheme.data?.filter(id => id !== config.id) | ||||
|     disabledInTheme.set(newList) | ||||
|     menuIsOpened.set(false) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| {#if question !== undefined} | ||||
|   <div class={clss}> | ||||
| 
 | ||||
|     {#if layer.isNormal()} | ||||
|     <DotMenu hideBackground={true} open={menuIsOpened}> | ||||
|       <SidebarUnit> | ||||
|         {#if $disabledInTheme.indexOf(config.id) >= 0} | ||||
|           <button on:click={() => enableQuestion()}> | ||||
|             Ask this question for all features | ||||
|           </button> | ||||
|         {:else} | ||||
|           <button on:click={() => disableQuestion()}> | ||||
|             Don't ask this question again | ||||
|           </button> | ||||
|         {/if} | ||||
|       </SidebarUnit> | ||||
|     </DotMenu> | ||||
|       {/if} | ||||
|     <form | ||||
|       class="relative flex flex-col overflow-y-auto px-4" | ||||
|       style="max-height: 75vh" | ||||
|  | @ -525,7 +558,7 @@ | |||
|             <Tr t={Translations.t.unknown.explanation} /> | ||||
|             <If condition={state.userRelatedState.showTags.map(v => v === "yes" || v === "full" || v === "always")}> | ||||
|               <div class="subtle"> | ||||
|                 <Tr t={Translations.t.unknown.removedKeys}/> | ||||
|                 <Tr t={Translations.t.unknown.removedKeys} /> | ||||
|                 {#each $settableKeys as key} | ||||
|                   <code> | ||||
|                     <del> | ||||
|  |  | |||
|  | @ -97,6 +97,7 @@ import ClearCaches from "./Popup/ClearCaches.svelte" | |||
| import GroupedView from "./Popup/GroupedView.svelte" | ||||
| import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" | ||||
| import NoteCommentElement from "./Popup/Notes/NoteCommentElement.svelte" | ||||
| import DisabledQuestions from "./Popup/DisabledQuestions.svelte" | ||||
| 
 | ||||
| class NearbyImageVis implements SpecialVisualization { | ||||
|     // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
 | ||||
|  | @ -2113,6 +2114,16 @@ export default class SpecialVisualizations { | |||
|                     }) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|                 funcName: "disabled_questions", | ||||
|                 docs: "Shows which questions are disabled for every layer. Used in 'settings'", | ||||
|                 needsUrls: [], | ||||
|                 args: [], | ||||
|                 constr(state) { | ||||
|                     return new SvelteUIElement(DisabledQuestions, { state }) | ||||
|                 }, | ||||
| 
 | ||||
|             }, | ||||
|         ] | ||||
| 
 | ||||
|         specialVisualizations.push(new AutoApplyButton(specialVisualizations)) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue