Feature: allow to remove an answer (in most cases), fix #2008

This commit is contained in:
Pieter Vander Vennet 2024-09-04 02:15:10 +02:00
parent 7565f13e39
commit 55c89085a3
8 changed files with 181 additions and 86 deletions

View file

@ -779,6 +779,14 @@
"missing": "{count} untranslated strings", "missing": "{count} untranslated strings",
"notImmediate": "Translations are not updated directly. This typically takes a few days" "notImmediate": "Translations are not updated directly. This typically takes a few days"
}, },
"unknown": {
"clear": "Clear answer",
"explanation": "Clear this bit of information if the current answer is incorrect but the actual value is not known. No other information will be removed.",
"keep": "Keep answer",
"markUnknown": "Mark as unknown",
"removedKeys": "The following keys will be removed:",
"title": "Mark as unknown?"
},
"userinfo": { "userinfo": {
"gotoInbox": "Open your inbox", "gotoInbox": "Open your inbox",
"gotoSettings": "Go to your settings on OpenStreetMap.org", "gotoSettings": "Go to your settings on OpenStreetMap.org",

View file

@ -3027,6 +3027,7 @@
"climbing_opportunity": { "climbing_opportunity": {
"description": "Fallback layer with items on which climbing _might_ be possible. It is loaded when zoomed in a lot, to prevent duplicate items to be added", "description": "Fallback layer with items on which climbing _might_ be possible. It is loaded when zoomed in a lot, to prevent duplicate items to be added",
"name": "Climbing opportunities?", "name": "Climbing opportunities?",
"snapName": "a wall, cliff or rock",
"tagRenderings": { "tagRenderings": {
"climbing-opportunity-name": { "climbing-opportunity-name": {
"render": "<strong>{name}</strong>" "render": "<strong>{name}</strong>"
@ -3428,6 +3429,7 @@
"cycleways_and_roads": { "cycleways_and_roads": {
"description": "All infrastructure that someone can cycle over, accompanied with questions about this infrastructure", "description": "All infrastructure that someone can cycle over, accompanied with questions about this infrastructure",
"name": "Cycleways and roads", "name": "Cycleways and roads",
"snapName": "a road or a cycleway",
"tagRenderings": { "tagRenderings": {
"Cycleway type for a road": { "Cycleway type for a road": {
"mappings": { "mappings": {
@ -5907,6 +5909,7 @@
"indoors": { "indoors": {
"description": "Basic indoor mapping: shows room outlines", "description": "Basic indoor mapping: shows room outlines",
"name": "Indoors", "name": "Indoors",
"snapName": "an indoor wall",
"tagRenderings": { "tagRenderings": {
"name": { "name": {
"freeform": { "freeform": {
@ -6076,6 +6079,7 @@
"title": "a kerb" "title": "a kerb"
} }
}, },
"snapName": "a kerb",
"tagRenderings": { "tagRenderings": {
"kerb-height": { "kerb-height": {
"freeform": { "freeform": {
@ -7293,7 +7297,8 @@
}, },
"pedestrian_path": { "pedestrian_path": {
"description": "Pedestrian footpaths, especially used for indoor navigation and snapping entrances to this layer", "description": "Pedestrian footpaths, especially used for indoor navigation and snapping entrances to this layer",
"name": "Pedestrian paths" "name": "Pedestrian paths",
"snapName": "a pedestrian path"
}, },
"pharmacy": { "pharmacy": {
"description": "A layer showing pharmacies, which (probably) dispense prescription drugs", "description": "A layer showing pharmacies, which (probably) dispense prescription drugs",
@ -9034,6 +9039,7 @@
"shelter": { "shelter": {
"description": "Layer showing shelter structures", "description": "Layer showing shelter structures",
"name": "Shelter", "name": "Shelter",
"snapName": "a shelter",
"tagRenderings": { "tagRenderings": {
"shelter-type": { "shelter-type": {
"mappings": { "mappings": {

View file

@ -2960,6 +2960,7 @@
"cycleways_and_roads": { "cycleways_and_roads": {
"description": "Alle infrastructuur waar je over kunt fietsen, met vragen over die infrastructuur", "description": "Alle infrastructuur waar je over kunt fietsen, met vragen over die infrastructuur",
"name": "Fietspaden, straten en wegen", "name": "Fietspaden, straten en wegen",
"snapName": "een weg, straat of fietspad",
"tagRenderings": { "tagRenderings": {
"Cycleway type for a road": { "Cycleway type for a road": {
"mappings": { "mappings": {
@ -4910,6 +4911,7 @@
"indoors": { "indoors": {
"description": "Een basis voor indoor-navigatie: toont binnenruimtes", "description": "Een basis voor indoor-navigatie: toont binnenruimtes",
"name": "Binnenruimtes", "name": "Binnenruimtes",
"snapName": "een binnenmuur",
"tagRenderings": { "tagRenderings": {
"name": { "name": {
"freeform": { "freeform": {

View file

@ -1,6 +1,6 @@
{ {
"name": "mapcomplete", "name": "mapcomplete",
"version": "0.46.2", "version": "0.46.3",
"repository": "https://github.com/pietervdvn/MapComplete", "repository": "https://github.com/pietervdvn/MapComplete",
"description": "A small website to edit OSM easily", "description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues", "bugs": "https://github.com/pietervdvn/MapComplete/issues",

View file

@ -84,7 +84,7 @@ export default class TagRenderingConfig {
| string | string
| TagRenderingConfigJson | TagRenderingConfigJson
| (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }), | (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }),
context?: string context?: string,
) { ) {
let json = <string | QuestionableTagRenderingConfigJson>config let json = <string | QuestionableTagRenderingConfigJson>config
if (json === undefined) { if (json === undefined) {
@ -143,7 +143,7 @@ export default class TagRenderingConfig {
this.description = Translations.T(json.description, translationKey + ".description") this.description = Translations.T(json.description, translationKey + ".description")
this.editButtonAriaLabel = Translations.T( this.editButtonAriaLabel = Translations.T(
json.editButtonAriaLabel, json.editButtonAriaLabel,
translationKey + ".editButtonAriaLabel" translationKey + ".editButtonAriaLabel",
) )
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`) this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
@ -159,7 +159,7 @@ export default class TagRenderingConfig {
} }
this.metacondition = TagUtils.Tag( this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] }, json.metacondition ?? { and: [] },
`${context}.metacondition` `${context}.metacondition`,
) )
if (json.freeform) { if (json.freeform) {
if ( if (
@ -177,7 +177,7 @@ export default class TagRenderingConfig {
}, perhaps you meant ${Utils.sortedByLevenshteinDistance( }, perhaps you meant ${Utils.sortedByLevenshteinDistance(
json.freeform.key, json.freeform.key,
<any>Validators.availableTypes, <any>Validators.availableTypes,
(s) => <any>s (s) => <any>s,
)}` )}`
} }
const type: ValidatorType = <any>json.freeform.type ?? "string" const type: ValidatorType = <any>json.freeform.type ?? "string"
@ -199,7 +199,7 @@ export default class TagRenderingConfig {
placeholder, placeholder,
addExtraTags: addExtraTags:
json.freeform.addExtraTags?.map((tg, i) => json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`) TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`),
) ?? [], ) ?? [],
inline: json.freeform.inline ?? false, inline: json.freeform.inline ?? false,
default: json.freeform.default, default: json.freeform.default,
@ -265,8 +265,8 @@ export default class TagRenderingConfig {
context, context,
this.multiAnswer, this.multiAnswer,
this.question !== undefined, this.question !== undefined,
commonIconSize commonIconSize,
) ),
) )
} else { } else {
this.mappings = [] this.mappings = []
@ -292,7 +292,7 @@ export default class TagRenderingConfig {
for (const expectedKey of keys) { for (const expectedKey of keys) {
if (usedKeys.indexOf(expectedKey) < 0) { if (usedKeys.indexOf(expectedKey) < 0) {
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join( const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
", " ", ",
)}, but it should also give a value for ${expectedKey}` )}, but it should also give a value for ${expectedKey}`
this.configuration_warnings.push(msg) this.configuration_warnings.push(msg)
} }
@ -339,7 +339,7 @@ export default class TagRenderingConfig {
context: string, context: string,
multiAnswer?: boolean, multiAnswer?: boolean,
isQuestionable?: boolean, isQuestionable?: boolean,
commonSize: string = "small" commonSize: string = "small",
): Mapping { ): Mapping {
const ctx = `${translationKey}.mappings.${i}` const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) { if (mapping.if === undefined) {
@ -348,7 +348,7 @@ export default class TagRenderingConfig {
if (mapping.then === undefined) { if (mapping.then === undefined) {
if (mapping["render"] !== 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( throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
mapping mapping,
)}` )}`
} }
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}` throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
@ -359,7 +359,7 @@ export default class TagRenderingConfig {
if (mapping["render"] !== undefined) { if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify( throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
mapping mapping,
)}` )}`
} }
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) { if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
@ -382,11 +382,11 @@ export default class TagRenderingConfig {
} else if (mapping.hideInAnswer !== undefined) { } else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag( hideInAnswer = TagUtils.Tag(
mapping.hideInAnswer, mapping.hideInAnswer,
`${context}.mapping[${i}].hideInAnswer` `${context}.mapping[${i}].hideInAnswer`,
) )
} }
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`) TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`),
) )
if (hideInAnswer === true && addExtraTags.length > 0) { 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.` 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.`
@ -482,7 +482,7 @@ export default class TagRenderingConfig {
* @constructor * @constructor
*/ */
public GetRenderValues( public GetRenderValues(
tags: Record<string, string> tags: Record<string, string>,
): { then: Translation; icon?: string; iconClass?: string }[] { ): { then: Translation; icon?: string; iconClass?: string }[] {
if (!this.multiAnswer) { if (!this.multiAnswer) {
return [this.GetRenderValueWithImage(tags)] return [this.GetRenderValueWithImage(tags)]
@ -505,7 +505,7 @@ export default class TagRenderingConfig {
return mapping return mapping
} }
return undefined return undefined
}) }),
) )
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) { if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
@ -513,7 +513,7 @@ export default class TagRenderingConfig {
applicableMappings applicableMappings
?.flatMap((m) => m.if?.usedTags() ?? []) ?.flatMap((m) => m.if?.usedTags() ?? [])
?.filter((kv) => kv.key === this.freeform.key) ?.filter((kv) => kv.key === this.freeform.key)
?.map((kv) => kv.value) ?.map((kv) => kv.value),
) )
const freeformValues = tags[this.freeform.key].split(";") const freeformValues = tags[this.freeform.key].split(";")
@ -522,7 +522,7 @@ export default class TagRenderingConfig {
applicableMappings.push({ applicableMappings.push({
then: new TypedTranslation<object>( then: new TypedTranslation<object>(
this.render.replace("{" + this.freeform.key + "}", leftover).translations, this.render.replace("{" + this.freeform.key + "}", leftover).translations,
this.render.context this.render.context,
), ),
}) })
} }
@ -540,7 +540,7 @@ export default class TagRenderingConfig {
* @constructor * @constructor
*/ */
public GetRenderValueWithImage( public GetRenderValueWithImage(
tags: Record<string, string> tags: Record<string, string>,
): { then: TypedTranslation<any>; icon?: string; iconClass?: string } | undefined { ): { then: TypedTranslation<any>; icon?: string; iconClass?: string } | undefined {
if (this.condition !== undefined) { if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) { if (!this.condition.matchesProperties(tags)) {
@ -609,7 +609,7 @@ export default class TagRenderingConfig {
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true) const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) { if (key === undefined) {
const values: { k: string; v: string }[][] = Utils.NoNull( const values: { k: string; v: string }[][] = Utils.NoNull(
answerMappings?.map((m) => m.if.asChange({})) ?? [] answerMappings?.map((m) => m.if.asChange({})) ?? [],
) )
if (values.length === 0) { if (values.length === 0) {
return return
@ -627,15 +627,15 @@ export default class TagRenderingConfig {
return { return {
key: commonKey, key: commonKey,
values: Utils.NoNull( values: Utils.NoNull(
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v) values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v),
), ),
} }
} }
let values = Utils.NoNull( let values = Utils.NoNull(
answerMappings?.map( answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v (m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v,
) ?? [] ) ?? [],
) )
if (values.length === undefined) { if (values.length === undefined) {
values = undefined values = undefined
@ -699,7 +699,7 @@ export default class TagRenderingConfig {
freeformValue: string | undefined, freeformValue: string | undefined,
singleSelectedMapping: number, singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined, multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string> currentProperties: Record<string, string>,
): UploadableTag { ): UploadableTag {
if (typeof freeformValue === "string") { if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim() freeformValue = freeformValue?.trim()
@ -774,7 +774,7 @@ export default class TagRenderingConfig {
new And([ new And([
new Tag(this.freeform.key, freeformValue), new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []), ...(this.freeform.addExtraTags ?? []),
]) ]),
) )
} }
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings]) const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
@ -844,11 +844,11 @@ export default class TagRenderingConfig {
} }
const msgs: string[] = [ const msgs: string[] = [
icon + icon +
" " + " " +
"*" + "*" +
m.then.textFor(lang) + m.then.textFor(lang) +
"* is shown if with " + "* is shown if with " +
m.if.asHumanString(true, false, {}), m.if.asHumanString(true, false, {}),
] ]
if (m.hideInAnswer === true) { if (m.hideInAnswer === true) {
@ -857,11 +857,11 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) { if (m.ifnot !== undefined) {
msgs.push( msgs.push(
"Unselecting this answer will add " + "Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {}) m.ifnot.asHumanString(true, false, {}),
) )
} }
return msgs.join(". ") return msgs.join(". ")
}) }),
) )
} }
@ -870,7 +870,7 @@ export default class TagRenderingConfig {
const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString( const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString(
true, true,
false, false,
{} {},
) )
condition = condition =
"This tagrendering is only visible in the popup if the following condition is met: " + "This tagrendering is only visible in the popup if the following condition is met: " +
@ -898,15 +898,13 @@ export default class TagRenderingConfig {
].join("\n") ].join("\n")
} }
public public usedTags(): TagsFilter[] {
usedTags(): TagsFilter[] {
const tags: TagsFilter[] = [] const tags: TagsFilter[] = []
tags.push( tags.push(
this.metacondition, this.metacondition,
this.condition, this.condition,
this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined, this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined,
this.invalidValues this.invalidValues,
) )
for (const m of this.mappings ?? []) { for (const m of this.mappings ?? []) {
tags.push(m.if) tags.push(m.if)
@ -924,16 +922,30 @@ export default class TagRenderingConfig {
/** /**
* The keys that should be erased if one has to revert to 'unknown'. * The keys that should be erased if one has to revert to 'unknown'.
* Might give undefined * Might give undefined if setting to unknown is not possible
*/ */
public settableKeys(): string[] | undefined { public removeToSetUnknown(): string[] | undefined {
const toDelete = new Set<string>() const toDelete = new Set<string>()
if (this.freeform) { if (this.freeform) {
toDelete.add(this.freeform.key) toDelete.add(this.freeform.key)
const extraTags = new And(this.freeform.addExtraTags ?? []).usedKeys().filter(k => k !== "fixme")
if (extraTags.length > 0) {
return undefined
}
} }
for (const mapping of this.mappings) { if (this.mappings?.length > 0) {
for (const usedKey of mapping.if.usedKeys()) { const mainkey = this.mappings[0].if.usedKeys()
toDelete.add(usedKey) mainkey.forEach(k => toDelete.add(k))
for (const mapping of this.mappings) {
if (mapping.addExtraTags?.length > 0) {
return undefined
}
for (const usedKey of mapping.if.usedKeys()) {
if (mainkey.indexOf(usedKey) < 0) {
// This is a complicated case, we ignore this for now
return undefined
}
}
} }
} }
@ -945,7 +957,7 @@ export class TagRenderingConfigUtils {
public static withNameSuggestionIndex( public static withNameSuggestionIndex(
config: TagRenderingConfig, config: TagRenderingConfig,
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
feature?: Feature feature?: Feature,
): Store<TagRenderingConfig> { ): Store<TagRenderingConfig> {
const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0 const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0
if (!isNSI) { if (!isNSI) {
@ -963,8 +975,8 @@ export class TagRenderingConfigUtils {
tags, tags,
country.split(";"), country.split(";"),
center, center,
{ sortByFrequency: true } { sortByFrequency: true },
) ),
) )
}) })
return extraMappings.map((extraMappings) => { return extraMappings.map((extraMappings) => {
@ -980,7 +992,7 @@ export class TagRenderingConfigUtils {
...m, ...m,
addExtraTags: [new Tag("nobrand", "")], addExtraTags: [new Tag("nobrand", "")],
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"), priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
} },
) ?? [] ) ?? []
clone.mappings = [...oldMappingsCloned, ...extraMappings] clone.mappings = [...oldMappingsCloned, ...extraMappings]
return clone return clone

View file

@ -12,11 +12,15 @@
</script> </script>
{#if !onlyLink} {#if !onlyLink}
<Popup {shown} {bodyPadding} {fullscreen}/> <Popup {shown} {bodyPadding} {fullscreen}>
<slot name="header" slot="header" />
<slot />
<slot name="footer" slot="footer" />
</Popup>
{:else} {:else}
<button class="as-link sidebar-button" on:click={() => shown.setData(true)}> <button class="as-link sidebar-button" on:click={() => shown.setData(true)}>
<slot name="link"> <slot name="link">
<slot name="header" /> <slot name="header" />
</slot> </slot>
</button> </button>
{/if} {/if}
@ -27,9 +31,9 @@
align-items: center; align-items: center;
} }
:global(.page-header svg) { :global(.page-header svg) {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
margin-right: 0.75rem; margin-right: 0.75rem;
} }
</style> </style>

View file

@ -34,7 +34,7 @@
<Modal open={_shown} on:close={() => shown.set(false)} outsideclose <Modal open={_shown} on:close={() => shown.set(false)} outsideclose
size="xl" size="xl"
dismissable={false} dismissable={false}P
{defaultClass} {bodyClass} {dialogClass} {headerClass} {defaultClass} {bodyClass} {dialogClass} {headerClass}
color="none"> color="none">
<h1 slot="header" class="page-header w-full"> <h1 slot="header" class="page-header w-full">

View file

@ -33,6 +33,9 @@
import Markdown from "../../Base/Markdown.svelte" import Markdown from "../../Base/Markdown.svelte"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import type { UploadableTag } from "../../../Logic/Tags/TagTypes" import type { UploadableTag } from "../../../Logic/Tags/TagTypes"
import { Modal } from "flowbite-svelte"
import Popup from "../../Base/Popup.svelte"
import If from "../../Base/If.svelte"
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -48,6 +51,8 @@
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key)) let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
let isKnown = tags.mapD(tags => config.GetRenderValue(tags) !== undefined)
let matchesEmpty = config.GetRenderValue({}) !== undefined
// Will be bound if a freeform is available // Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]) let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
@ -59,6 +64,12 @@
*/ */
let checkedMappings: boolean[] let checkedMappings: boolean[]
/**
* IF set: we can remove the current answer by deleting all those keys
*/
let settableKeys = config.removeToSetUnknown()
let unknownModal = new UIEventSource(false)
let searchTerm: UIEventSource<string> = new UIEventSource("") let searchTerm: UIEventSource<string> = new UIEventSource("")
let dispatch = createEventDispatcher<{ let dispatch = createEventDispatcher<{
@ -80,7 +91,7 @@
return !m.hideInAnswer.matchesProperties(tgs) return !m.hideInAnswer.matchesProperties(tgs)
}) })
selectedMapping = mappings?.findIndex( selectedMapping = mappings?.findIndex(
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs) (mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs),
) )
if (selectedMapping < 0) { if (selectedMapping < 0) {
selectedMapping = undefined selectedMapping = undefined
@ -142,7 +153,6 @@
let usedKeys: string[] = Utils.Dedup(config.usedTags().flatMap((t) => t.usedKeys())) let usedKeys: string[] = Utils.Dedup(config.usedTags().flatMap((t) => t.usedKeys()))
let keysToDeleteOnUnknown = config.settableKeys()
/** /**
* The 'minimalTags' is a subset of the tags of the feature, only containing the values relevant for this object. * The 'minimalTags' is a subset of the tags of the feature, only containing the values relevant for this object.
* The main goal is to be stable and only 'ping' when an actual change is relevant * The main goal is to be stable and only 'ping' when an actual change is relevant
@ -189,7 +199,7 @@
if (freeformValue?.length > 0) { if (freeformValue?.length > 0) {
selectedMapping = config.mappings.length selectedMapping = config.mappings.length
} }
}) }),
) )
$: { $: {
@ -207,7 +217,7 @@
$freeformInput, $freeformInput,
selectedMapping, selectedMapping,
checkedMappings, checkedMappings,
tags.data tags.data,
) )
if (featureSwitchIsDebugging?.data) { if (featureSwitchIsDebugging?.data) {
console.log( console.log(
@ -219,7 +229,7 @@
currentTags: tags.data, currentTags: tags.data,
}, },
" --> ", " --> ",
selectedTags selectedTags,
) )
} }
} catch (e) { } catch (e) {
@ -241,7 +251,7 @@
selectedTags = new And([...selectedTags.and, ...extraTagsArray]) selectedTags = new And([...selectedTags.and, ...extraTagsArray])
} else { } else {
console.error( console.error(
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags) "selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags),
) )
} }
} }
@ -310,9 +320,24 @@
onDestroy( onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => { state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount numberOfCs = ud.csCount
}) }),
) )
} }
function clearAnswer() {
const tagsToSet = settableKeys.map(k => new Tag(k, ""))
const change = new ChangeTagAction(tags.data.id, new And(tagsToSet), tags.data, {
theme: tags.data["_orig_theme"] ?? state.layout.id,
changeType: "answer",
})
freeformInput.set(undefined)
selectedMapping = undefined
selectedTags = undefined
change
.CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes))
.catch(console.error)
}
</script> </script>
{#if question !== undefined} {#if question !== undefined}
@ -491,36 +516,74 @@
<Tr t={$feedback} /> <Tr t={$feedback} />
</div> </div>
{/if} {/if}
<!--{#if keysToDeleteOnUnknown?.some(k => !! $tags[k])}
Mark as unknown (delete {keysToDeleteOnUnknown?.filter(k => !! $tags[k]).join(";")})
{/if}--> <Popup shown={unknownModal}>
<h2 slot="header">
<Tr t={Translations.t.unknown.title} />
</h2>
<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}/>
{#each settableKeys as key}
<code>
<del>
{key}
</del>
</code>
{/each}
</div>
</If>
<div class="flex justify-end w-full" slot="footer">
<button on:click={() => unknownModal.set(false)}>
<Tr t={Translations.t.unknown.keep} />
</button>
<button class="primary" on:click={() => {unknownModal.set(false); clearAnswer()}}>
<Tr t={Translations.t.unknown.clear} />
</button>
</div>
</Popup>
<div <div
class="sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap" class="sticky bottom-0 flex justify-between flex-wrap"
style="z-index: 11" style="z-index: 11"
> >
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel" /> {#if settableKeys && $isKnown && !matchesEmpty }
<slot name="save-button" {selectedTags}> <button class="as-link small text-sm" on:click={() => unknownModal.set(true)}>
{#if config.freeform?.key && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]} <Tr t={Translations.t.unknown.markUnknown} />
<button </button>
class="primary flex" {/if}
on:click|stopPropagation|preventDefault={() => onSave()}
>
<TrashIcon class="h-6 w-6 text-red-500" /> <div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap self-end flex-grow">
<Tr t={Translations.t.general.eraseValue} />
</button> <!-- TagRenderingQuestion-buttons -->
{:else} <slot name="cancel" />
<button <slot name="save-button" {selectedTags}>
on:click={() => onSave()} {#if config.freeform?.key && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]}
class={twJoin( <button
class="primary flex"
on:click|stopPropagation|preventDefault={() => onSave()}
>
<TrashIcon class="h-6 w-6 text-red-500" />
<Tr t={Translations.t.general.eraseValue} />
</button>
{:else}
<button
on:click={() => onSave()}
class={twJoin(
selectedTags === undefined ? "disabled" : "button-shadow", selectedTags === undefined ? "disabled" : "button-shadow",
"primary" "primary"
)} )}
> >
<Tr t={Translations.t.general.save} /> <Tr t={Translations.t.general.save} />
</button> </button>
{/if} {/if}
</slot> </slot>
</div>
</div> </div>
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} {#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
<span class="flex flex-wrap justify-between"> <span class="flex flex-wrap justify-between">