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",
|
"id": "title-privacy-legal",
|
||||||
"render": {
|
"render": {
|
||||||
|
|
|
@ -545,4 +545,14 @@ export default class UserRelatedState {
|
||||||
|
|
||||||
return amendedPrefs
|
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 open = new UIEventSource(false)
|
||||||
export let dotsSize = `w-6 h-6`
|
export let dotsSize = `w-6 h-6`
|
||||||
export let dotsPosition = `top-0 right-0`
|
export let dotsPosition = `top-0 right-0`
|
||||||
export let hideBackground= false
|
export let hideBackground: boolean = false
|
||||||
let menuPosition = ``
|
let menuPosition = ``
|
||||||
if(dotsPosition.indexOf("left-0") >= 0){
|
if (dotsPosition.indexOf("left-0") >= 0) {
|
||||||
menuPosition = "left-0"
|
menuPosition = "left-0"
|
||||||
}else{
|
} else {
|
||||||
menuPosition = `right-0`
|
menuPosition = `right-0`
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dotsPosition.indexOf("top-0") > 0){
|
if (dotsPosition.indexOf("top-0") > 0) {
|
||||||
menuPosition += " bottom-0"
|
menuPosition += " bottom-0"
|
||||||
}else{
|
} else {
|
||||||
menuPosition += ` top-0`
|
menuPosition += ` top-0`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dots-menu > path) {
|
:global(.dots-menu > path) {
|
||||||
fill: var(--interactive-background);
|
fill: var(--button-background-hover);
|
||||||
transition: fill 350ms linear;
|
transition: fill 350ms linear;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.transition-background {
|
.transition-background {
|
||||||
transition: background-color 150ms linear;
|
transition: background-color 150ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transition-background.collapsed {
|
.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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseQuestions = (layer?.tagRenderings ?? [])?.filter(
|
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 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 questionboxElem: HTMLDivElement
|
||||||
let questionsToAsk = tags.map(
|
let questionsToAsk = tags.map(
|
||||||
(tags) => {
|
(tags) => {
|
||||||
|
@ -69,10 +78,10 @@
|
||||||
}
|
}
|
||||||
return questionsToAsk
|
return questionsToAsk
|
||||||
},
|
},
|
||||||
[skippedQuestions]
|
[skippedQuestions],
|
||||||
)
|
)
|
||||||
let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(
|
let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(
|
||||||
undefined
|
undefined,
|
||||||
)
|
)
|
||||||
let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource<
|
let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource<
|
||||||
TagRenderingConfig[]
|
TagRenderingConfig[]
|
||||||
|
@ -95,6 +104,8 @@
|
||||||
let skipped: number = 0
|
let skipped: number = 0
|
||||||
|
|
||||||
let loginEnabled = state.featureSwitches.featureSwitchEnableLogin
|
let loginEnabled = state.featureSwitches.featureSwitchEnableLogin
|
||||||
|
let debug = state.featureSwitches.featureSwitchIsDebugging
|
||||||
|
|
||||||
|
|
||||||
function skip(question: { id: string }, didAnswer: boolean = false) {
|
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
|
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="marker-questionbox-root"
|
||||||
class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0}
|
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}
|
{#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}
|
{#if skipped + answered > 0}
|
||||||
<div class="thanks">
|
<div class="flex justify-center">
|
||||||
<Tr t={Translations.t.general.questionBox.done} />
|
{#if answered === 0}
|
||||||
</div>
|
{#if skipped === 1}
|
||||||
{#if answered === 0}
|
<Tr t={Translations.t.general.questionBox.skippedOne} />
|
||||||
{#if skipped === 1}
|
{:else}
|
||||||
<Tr t={Translations.t.general.questionBox.skippedOne} />
|
<Tr t={Translations.t.general.questionBox.skippedMultiple.Subs({ skipped })} />
|
||||||
{:else}
|
{/if}
|
||||||
<Tr t={Translations.t.general.questionBox.skippedMultiple.Subs({ skipped })} />
|
{:else if answered === 1}
|
||||||
{/if}
|
{#if skipped === 0}
|
||||||
{:else if answered === 1}
|
<Tr t={Translations.t.general.questionBox.answeredOne} />
|
||||||
{#if skipped === 0}
|
{:else if skipped === 1}
|
||||||
<Tr t={Translations.t.general.questionBox.answeredOne} />
|
<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}
|
{:else if skipped === 1}
|
||||||
<Tr t={Translations.t.general.questionBox.answeredOneSkippedOne} />
|
<Tr
|
||||||
|
t={Translations.t.general.questionBox.answeredMultipleSkippedOne.Subs({ answered })}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Tr
|
<Tr
|
||||||
t={Translations.t.general.questionBox.answeredOneSkippedMultiple.Subs({ skipped })}
|
t={Translations.t.general.questionBox.answeredMultipleSkippedMultiple.Subs({
|
||||||
/>
|
|
||||||
{/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({
|
|
||||||
answered,
|
answered,
|
||||||
skipped,
|
skipped,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if skipped > 0}
|
{#if skipped + $skippedQuestions.size > 0}
|
||||||
<button
|
<button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -163,45 +215,25 @@
|
||||||
>
|
>
|
||||||
<Tr t={Translations.t.general.questionBox.reactivate} />
|
<Tr t={Translations.t.general.questionBox.reactivate} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
|
||||||
<div>
|
{#if $skippedQuestions.size - skipped > 0}
|
||||||
{#if $showAllQuestionsAtOnce}
|
<button
|
||||||
<div class="flex flex-col gap-y-1">
|
class="w-full"
|
||||||
{#each $allQuestionsToAsk as question (question.id)}
|
on:click={() => {
|
||||||
<TagRenderingQuestionDynamic
|
skippedQuestions.setData(new Set())
|
||||||
config={question}
|
skipped = 0
|
||||||
{tags}
|
|
||||||
{selectedElement}
|
|
||||||
{state}
|
|
||||||
{layer}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else if $firstQuestion !== undefined}
|
|
||||||
<TagRenderingQuestionDynamic
|
|
||||||
config={$firstQuestion}
|
|
||||||
{layer}
|
|
||||||
{selectedElement}
|
|
||||||
{state}
|
|
||||||
{tags}
|
|
||||||
on:saved={() => {
|
|
||||||
skip($firstQuestion, true)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
Show the disabled questions for this object
|
||||||
class="secondary"
|
</button>
|
||||||
on:click={() => {
|
|
||||||
skip($firstQuestion)
|
{/if}
|
||||||
}}
|
{#if $debug}
|
||||||
slot="cancel"
|
Skipped questions are {Array.from($skippedQuestions).join(", ")}
|
||||||
>
|
{/if}
|
||||||
<Tr t={Translations.t.general.skip} />
|
</div>
|
||||||
</button>
|
|
||||||
</TagRenderingQuestionDynamic>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
import { Modal } from "flowbite-svelte"
|
import { Modal } from "flowbite-svelte"
|
||||||
import Popup from "../../Base/Popup.svelte"
|
import Popup from "../../Base/Popup.svelte"
|
||||||
import If from "../../Base/If.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 config: TagRenderingConfig
|
||||||
export let tags: UIEventSource<Record<string, string>>
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
|
@ -338,10 +340,41 @@
|
||||||
.then((changes) => state.changes.applyChanges(changes))
|
.then((changes) => state.changes.applyChanges(changes))
|
||||||
.catch(console.error)
|
.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>
|
</script>
|
||||||
|
|
||||||
{#if question !== undefined}
|
{#if question !== undefined}
|
||||||
<div class={clss}>
|
<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
|
<form
|
||||||
class="relative flex flex-col overflow-y-auto px-4"
|
class="relative flex flex-col overflow-y-auto px-4"
|
||||||
style="max-height: 75vh"
|
style="max-height: 75vh"
|
||||||
|
@ -525,7 +558,7 @@
|
||||||
<Tr t={Translations.t.unknown.explanation} />
|
<Tr t={Translations.t.unknown.explanation} />
|
||||||
<If condition={state.userRelatedState.showTags.map(v => v === "yes" || v === "full" || v === "always")}>
|
<If condition={state.userRelatedState.showTags.map(v => v === "yes" || v === "full" || v === "always")}>
|
||||||
<div class="subtle">
|
<div class="subtle">
|
||||||
<Tr t={Translations.t.unknown.removedKeys}/>
|
<Tr t={Translations.t.unknown.removedKeys} />
|
||||||
{#each $settableKeys as key}
|
{#each $settableKeys as key}
|
||||||
<code>
|
<code>
|
||||||
<del>
|
<del>
|
||||||
|
|
|
@ -97,6 +97,7 @@ import ClearCaches from "./Popup/ClearCaches.svelte"
|
||||||
import GroupedView from "./Popup/GroupedView.svelte"
|
import GroupedView from "./Popup/GroupedView.svelte"
|
||||||
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||||
import NoteCommentElement from "./Popup/Notes/NoteCommentElement.svelte"
|
import NoteCommentElement from "./Popup/Notes/NoteCommentElement.svelte"
|
||||||
|
import DisabledQuestions from "./Popup/DisabledQuestions.svelte"
|
||||||
|
|
||||||
class NearbyImageVis implements SpecialVisualization {
|
class NearbyImageVis implements SpecialVisualization {
|
||||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
// 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))
|
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||||
|
|
Loading…
Add table
Reference in a new issue