From fd90914c35a278235646052ea34ad1cb28005035 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 1 May 2022 04:17:40 +0200 Subject: [PATCH] Refactoring of deleteWizard --- Models/ThemeConfig/DeleteConfig.ts | 30 +- Models/ThemeConfig/Json/DeleteConfigJson.ts | 13 +- UI/BigComponents/MoreScreen.ts | 2 +- UI/Input/RadioButton.ts | 2 +- UI/Popup/DeleteWizard.ts | 286 ++++++++++---------- UI/Popup/FeatureInfoBox.ts | 35 +-- UI/Popup/TagRenderingQuestion.ts | 43 +-- langs/en.json | 2 + 8 files changed, 207 insertions(+), 206 deletions(-) diff --git a/Models/ThemeConfig/DeleteConfig.ts b/Models/ThemeConfig/DeleteConfig.ts index abc264584e..44c0543d0b 100644 --- a/Models/ThemeConfig/DeleteConfig.ts +++ b/Models/ThemeConfig/DeleteConfig.ts @@ -1,23 +1,43 @@ -import {Translation} from "../../UI/i18n/Translation"; +import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {DeleteConfigJson} from "./Json/DeleteConfigJson"; import Translations from "../../UI/i18n/Translations"; import {TagUtils} from "../../Logic/Tags/TagUtils"; export default class DeleteConfig { + public static readonly defaultDeleteReasons : {changesetMessage: string, explanation: Translation} [] = [ + { + changesetMessage: "testing point", + explanation: Translations.t.delete.reasons.test + }, + { + changesetMessage:"disused", + explanation: Translations.t.delete.reasons.disused + }, + { + changesetMessage: "not found", + explanation: Translations.t.delete.reasons.notFound + }, + { + changesetMessage: "duplicate", + explanation:Translations.t.delete.reasons.duplicate + } + ] + + public readonly extraDeleteReasons?: { - explanation: Translation, + explanation: TypedTranslation, changesetMessage: string }[] - public readonly nonDeleteMappings?: { if: TagsFilter, then: Translation }[] + public readonly nonDeleteMappings?: { if: TagsFilter, then: TypedTranslation }[] public readonly softDeletionTags?: TagsFilter public readonly neededChangesets?: number constructor(json: DeleteConfigJson, context: string) { - this.extraDeleteReasons = json.extraDeleteReasons?.map((reason, i) => { + this.extraDeleteReasons = (json.extraDeleteReasons ?? []).map((reason, i) => { const ctx = `${context}.extraDeleteReasons[${i}]` if ((reason.changesetMessage ?? "").length <= 5) { throw `${ctx}.explanation is too short, needs at least 4 characters` @@ -27,7 +47,7 @@ export default class DeleteConfig { changesetMessage: reason.changesetMessage } }) - this.nonDeleteMappings = json.nonDeleteMappings?.map((nonDelete, i) => { + this.nonDeleteMappings = (json.nonDeleteMappings??[]).map((nonDelete, i) => { const ctx = `${context}.extraDeleteReasons[${i}]` return { if: TagUtils.Tag(nonDelete.if, ctx + ".if"), diff --git a/Models/ThemeConfig/Json/DeleteConfigJson.ts b/Models/ThemeConfig/Json/DeleteConfigJson.ts index c6bb1e6753..9b147bfb75 100644 --- a/Models/ThemeConfig/Json/DeleteConfigJson.ts +++ b/Models/ThemeConfig/Json/DeleteConfigJson.ts @@ -36,7 +36,17 @@ export interface DeleteConfigJson { * By adding a 'nonDeleteMapping', an option can be added into the list which will retag the feature. * It is important that the feature will be retagged in such a way that it won't be picked up by the layer anymore! */ - nonDeleteMappings?: { if: AndOrTagConfigJson, then: string | any }[], + nonDeleteMappings?: { + /** + * The tags that will be given to the object. + * This must remove tags so that the 'source/osmTags' won't match anymore + */ + if: AndOrTagConfigJson, + /** + * The human explanation for the options + */ + then: string | any, + }[], /** * In some cases, the contributor is not allowed to delete the current feature (e.g. because it isn't a point, the point is referenced by a relation or the user isn't experienced enough). @@ -63,4 +73,5 @@ export interface DeleteConfigJson { * For some small features (e.g. bicycle racks) this is too much and this requirement can be lowered or dropped, which can be done here. */ neededChangesets?: number + } \ No newline at end of file diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 7477b2c5c5..6d5592783e 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -54,7 +54,7 @@ export default class MoreScreen extends Combine { if(searchTerm === "personal"){ window.location.href = MoreScreen.createUrlFor({id: "personal"}, false, state).data } - if(searchTerm === "bugs") { + if(searchTerm === "bugs" || searchTerm === "issues") { window.location.href = "https://github.com/pietervdvn/MapComplete/issues" } if(searchTerm === "source") { diff --git a/UI/Input/RadioButton.ts b/UI/Input/RadioButton.ts index 390a781223..0ee275ffc8 100644 --- a/UI/Input/RadioButton.ts +++ b/UI/Input/RadioButton.ts @@ -12,7 +12,7 @@ export class RadioButton extends InputElement { constructor( elements: InputElement[], options?: { - selectFirstAsDefault?: boolean, + selectFirstAsDefault?: true | boolean, dontStyle?: boolean } ) { diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts index 1cfece179c..b0ce39556c 100644 --- a/UI/Popup/DeleteWizard.ts +++ b/UI/Popup/DeleteWizard.ts @@ -5,23 +5,26 @@ import Svg from "../../Svg"; import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; import {UIEventSource} from "../../Logic/UIEventSource"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; -import TagRenderingQuestion from "./TagRenderingQuestion"; import Combine from "../Base/Combine"; import {SubtleButton} from "../Base/SubtleButton"; -import {FixedUiElement} from "../Base/FixedUiElement"; import {Translation} from "../i18n/Translation"; import BaseUIElement from "../BaseUIElement"; import Constants from "../../Models/Constants"; -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; -import {AndOrTagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson"; import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"; import {OsmObject} from "../../Logic/Osm/OsmObject"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {Changes} from "../../Logic/Osm/Changes"; import {OsmConnection} from "../../Logic/Osm/OsmConnection"; +import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; +import {InputElement} from "../Input/InputElement"; +import {RadioButton} from "../Input/RadioButton"; +import {FixedInputElement} from "../Input/FixedInputElement"; +import Title from "../Base/Title"; +import {SubstitutedTranslation} from "../SubstitutedTranslation"; +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; +import TagRenderingQuestion from "./TagRenderingQuestion"; export default class DeleteWizard extends Toggle { + /** * The UI-element which triggers 'deletion' (either soft or hard). * @@ -42,12 +45,7 @@ export default class DeleteWizard extends Toggle { * @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted */ constructor(id: string, - state: { - osmConnection: OsmConnection; - allElements: ElementStorage, - layoutToUse?: LayoutConfig, - changes?: Changes - }, + state: FeaturePipelineState, options: DeleteConfig) { @@ -60,70 +58,88 @@ export default class DeleteWizard extends Toggle { const confirm = new UIEventSource(false) - function doDelete(selected: TagsFilter) { - // Selected == the reasons, not the tags of the object - const tgs = selected.asChange(tagsSource.data) - const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason") - if (deleteReasonMatch.length === 0) { - return; + /** + * This function is the actual delete function + */ + function doDelete(selected: { deleteReason: string } | { retagTo: TagsFilter }) { + let actionToTake: OsmChangeAction; + if (selected["retagTo"] !== undefined) { + // no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping + actionToTake = new ChangeTagAction( + id, + selected["retagTo"], + tagsSource.data, + { + theme: state?.layoutToUse?.id ?? "unkown", + changeType: "special-delete" + } + ) + } else { + + actionToTake = new DeleteAction(id, + options.softDeletionTags, + { + theme: state?.layoutToUse?.id ?? "unkown", + specialMotivation: selected["deleteReason"] + }, + deleteAbility.canBeDeleted.data.canBeDeleted + ) } - const deleteAction = new DeleteAction(id, - options.softDeletionTags, - { - theme: state?.layoutToUse?.id ?? "unkown", - specialMotivation: deleteReasonMatch[0]?.v - }, - deleteAbility.canBeDeleted.data.canBeDeleted - ) - state.changes?.applyAction(deleteAction) + state.changes?.applyAction(actionToTake) isDeleted.setData(true) } const t = Translations.t.delete - const cancelButton = t.cancel.Clone().SetClass("block btn btn-secondary").onClick(() => confirm.setData(false)); - const question = new VariableUiElement(tagsSource.map(currentTags => { - const config = DeleteWizard.generateDeleteTagRenderingConfig(options.softDeletionTags, options.nonDeleteMappings, options.extraDeleteReasons, currentTags) - return new TagRenderingQuestion( - tagsSource, - config, - state, - { - cancelButton, - /*Using a custom save button constructor erases all logic to actually save, so we have to listen for the click!*/ - saveButtonConstr: (v) => DeleteWizard.constructConfirmButton(v).onClick(() => { - doDelete(v.data) - }), - bottomText: (v) => DeleteWizard.constructExplanation(v, deleteAbility) - } - ) - })) - + const cancelButton = t.cancel.SetClass("block btn btn-secondary").onClick(() => confirm.setData(false)); /** * The button which is shown first. Opening it will trigger the check for deletions */ const deleteButton = new SubtleButton( - Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), t.delete.Clone()).onClick( - () => { - deleteAbility.CheckDeleteability(true) - confirm.setData(true); - } - ) + Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), t.delete) + .onClick( + () => { + deleteAbility.CheckDeleteability(true) + confirm.setData(true); + } + ) + + const isShown: UIEventSource = tagsSource.map(tgs => tgs.id.indexOf("-") < 0) + + const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state); + const deleteDialog = new Combine([ + + + new Title(new SubstitutedTranslation(t.whyDelete, tagsSource, state) + .SetClass("question-text"), 3), + deleteOptionPicker, + new Combine([ + DeleteWizard.constructExplanation(deleteOptionPicker.GetValue(), deleteAbility, tagsSource, state), + new Combine([ + + cancelButton, + DeleteWizard.constructConfirmButton(deleteOptionPicker.GetValue()) + .onClick(() => doDelete(deleteOptionPicker.GetValue().data)) + ]).SetClass("flex justify-end flex-wrap-reverse") + + ]).SetClass("flex mt-2 justify-between") + + + ]).SetClass("question") - const isShown = new UIEventSource(id.indexOf("-") < 0) super( new Toggle( new Combine([Svg.delete_icon_svg().SetClass("h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full"), - t.isDeleted.Clone()]).SetClass("flex m-2 rounded-full"), + t.isDeleted]).SetClass("flex m-2 rounded-full"), new Toggle( new Toggle( new Toggle( new Toggle( - question, - new SubtleButton(Svg.envelope_ui(), t.readMessages.Clone()), + deleteDialog, + new SubtleButton(Svg.envelope_ui(), t.readMessages), state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0) ), @@ -134,16 +150,16 @@ export default class DeleteWizard extends Toggle { new Combine([ Svg.delete_not_allowed_svg().SetStyle("height: 2rem; width: auto").SetClass("mr-2"), new Combine([ - t.cannotBeDeleted.Clone(), - cbd.reason.Clone().SetClass("subtle"), - t.useSomethingElse.Clone().SetClass("subtle")]).SetClass("flex flex-col") + t.cannotBeDeleted, + cbd.reason.SetClass("subtle"), + t.useSomethingElse.SetClass("subtle")]).SetClass("flex flex-col") ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200"))) , deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), - t.loginToDelete.Clone().onClick(state.osmConnection.AttemptLogin), + t.loginToDelete.onClick(state.osmConnection.AttemptLogin), state.osmConnection.isLoggedIn ), isDeleted), @@ -153,17 +169,17 @@ export default class DeleteWizard extends Toggle { } - private static constructConfirmButton(deleteReasons: UIEventSource): BaseUIElement { + private static constructConfirmButton(deleteReasons: UIEventSource): BaseUIElement { const t = Translations.t.delete; const btn = new Combine([ Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"), - t.delete.Clone() + t.delete ]).SetClass("flex btn bg-red-500") const btnNonActive = new Combine([ Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"), - t.delete.Clone() + t.delete ]).SetClass("flex btn btn-disabled bg-red-200") return new Toggle( @@ -175,111 +191,83 @@ export default class DeleteWizard extends Toggle { } - private static constructExplanation(tags: UIEventSource, deleteAction: DeleteabilityChecker) { + private static constructExplanation(selectedOption: UIEventSource< + {deleteReason: string} | {retagTo: TagsFilter}>, deleteAction: DeleteabilityChecker, + currentTags: UIEventSource, + state?: {osmConnection?: OsmConnection}) { const t = Translations.t.delete; - return new VariableUiElement(tags.map( - currentTags => { - const cbd = deleteAction.canBeDeleted.data; - if (currentTags === undefined) { - return t.explanations.selectReason.Clone().SetClass("subtle"); + return new VariableUiElement(selectedOption.map( + selectedOption => { + if (selectedOption === undefined) { + return t.explanations.selectReason.SetClass("subtle"); } - const hasDeletionTag = currentTags.asChange(currentTags).some(kv => kv.k === "_delete_reason") - - if (cbd.canBeDeleted && hasDeletionTag) { - return t.explanations.hardDelete.Clone() + const retag: TagsFilter | undefined = selectedOption["retagTo"] + if(retag !== undefined) { + // This is a retagging, not a deletion of any kind + return new Combine([t.explanations.retagNoOtherThemes, + TagRenderingQuestion.CreateTagExplanation(new UIEventSource(retag), + currentTags, state + ).SetClass("subtle") + ]) } - return new Combine([t.explanations.softDelete.Subs({reason: cbd.reason}), - new FixedUiElement(currentTags.asHumanString(false, true, currentTags)).SetClass("subtle") - ]).SetClass("flex flex-col") + const deleteReason = selectedOption["deleteReason"]; + if(deleteReason !== undefined){ + return new VariableUiElement(deleteAction.canBeDeleted.map(({ + canBeDeleted, reason + }) => { + if(canBeDeleted){ + // This is a hard delete for which we give an explanation + return t.explanations.hardDelete; + } + // This is a soft deletion: we explain _why_ the deletion is soft + return t.explanations.softDelete.Subs({reason: reason}) + })) + + } } , [deleteAction.canBeDeleted] )).SetClass("block") } - private static generateDeleteTagRenderingConfig(softDeletionTags: TagsFilter, - nonDeleteOptions: { if: TagsFilter; then: Translation }[], - extraDeleteReasons: { explanation: Translation; changesetMessage: string }[], - currentTags: any): TagRenderingConfig { - const t = Translations.t.delete - nonDeleteOptions = nonDeleteOptions ?? [] - let softDeletionTagsStr = [] - if (softDeletionTags !== undefined) { - softDeletionTags.asChange(currentTags) - } - const extraOptionsStr: { if: AndOrTagConfigJson, then: any }[] = [] - for (const nonDeleteOption of nonDeleteOptions) { - const newIf: string[] = nonDeleteOption.if.asChange({}).map(kv => kv.k + "=" + kv.v) + private static constructMultipleChoice(config: DeleteConfig, tagsSource: UIEventSource, state: FeaturePipelineState): + InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> { - extraOptionsStr.push({ - if: {and: newIf}, - then: nonDeleteOption.then - }) + const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[ ] = [] + + for (const nonDeleteOption of config.nonDeleteMappings) { + elements.push(new FixedInputElement( + new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state), + { + retagTo: nonDeleteOption.if + } + )) } - for (const extraDeleteReason of (extraDeleteReasons ?? [])) { - extraOptionsStr.push({ - if: {and: ["_delete_reason=" + extraDeleteReason.changesetMessage]}, - then: extraDeleteReason.explanation - }) + for (const extraDeleteReason of (config.extraDeleteReasons ?? [])) { + elements.push(new FixedInputElement( + new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state), + { + deleteReason: extraDeleteReason.changesetMessage + } + )) } - return new TagRenderingConfig( - { - question: t.whyDelete, - render: "Deleted because {_delete_reason}", - freeform: { - key: "_delete_reason", - addExtraTags: softDeletionTagsStr - }, - mappings: [ - ...extraOptionsStr, + for (const extraDeleteReason of DeleteConfig.defaultDeleteReasons) { + elements.push(new FixedInputElement( + extraDeleteReason.explanation.Clone(/*Must clone here, as this explanation might be used on many locations*/), + { + deleteReason: extraDeleteReason.changesetMessage + } + )) + } - { - if: { - and: [ - "_delete_reason=testing point", - ...softDeletionTagsStr - ] - }, - then: t.reasons.test - }, - { - if: { - and: [ - "_delete_reason=disused", - ...softDeletionTagsStr - ] - }, - then: t.reasons.disused - }, - { - if: { - and: [ - "_delete_reason=not found", - ...softDeletionTagsStr - ] - }, - then: t.reasons.notFound - }, - { - if: { - and: [ - "_delete_reason=duplicate", - ...softDeletionTagsStr - ] - }, - then: t.reasons.duplicate - } - ] - - - }, "Delete wizard" - ) + return new RadioButton(elements, {selectFirstAsDefault: false}); } + } class DeleteabilityChecker { diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 43c56778a8..ff28d1de0d 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -15,13 +15,8 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import {Utils} from "../../Utils"; import MoveWizard from "./MoveWizard"; import Toggle from "../Input/Toggle"; -import {OsmConnection} from "../../Logic/Osm/OsmConnection"; -import {Changes} from "../../Logic/Osm/Changes"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -import {ElementStorage} from "../../Logic/ElementStorage"; -import FilteredLayer from "../../Models/FilteredLayer"; -import BaseLayer from "../../Models/BaseLayer"; import Lazy from "../Base/Lazy"; +import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; export default class FeatureInfoBox extends ScrollableFullScreen { @@ -29,18 +24,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { public constructor( tags: UIEventSource, layerConfig: LayerConfig, - state: { - filteredLayers: UIEventSource; - backgroundLayer: UIEventSource; - featureSwitchIsTesting: UIEventSource; - featureSwitchIsDebugging: UIEventSource; - featureSwitchShowAllQuestions: UIEventSource; - osmConnection: OsmConnection, - featureSwitchUserbadge: UIEventSource, - changes: Changes, - layoutToUse: LayoutConfig, - allElements: ElementStorage - }, + state: FeaturePipelineState, hashToShow?: string, isShown?: UIEventSource, ) { @@ -78,18 +62,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { private static GenerateContent(tags: UIEventSource, layerConfig: LayerConfig, - state: { - filteredLayers: UIEventSource; - backgroundLayer: UIEventSource; - featureSwitchIsTesting: UIEventSource; - featureSwitchIsDebugging: UIEventSource; - featureSwitchShowAllQuestions: UIEventSource; - osmConnection: OsmConnection, - featureSwitchUserbadge: UIEventSource, - changes: Changes, - layoutToUse: LayoutConfig, - allElements: ElementStorage - }): BaseUIElement { + state: FeaturePipelineState): BaseUIElement { let questionBoxes: Map = new Map(); const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group)) @@ -179,7 +152,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { private static createEditElements(questionBoxes: Map, layerConfig: LayerConfig, tags: UIEventSource, - state: { filteredLayers: UIEventSource; backgroundLayer: UIEventSource; featureSwitchIsTesting: UIEventSource; featureSwitchIsDebugging: UIEventSource; featureSwitchShowAllQuestions: UIEventSource; osmConnection: OsmConnection; featureSwitchUserbadge: UIEventSource; changes: Changes; layoutToUse: LayoutConfig; allElements: ElementStorage }) + state: FeaturePipelineState) : BaseUIElement { let editElements: BaseUIElement[] = [] questionBoxes.forEach(questionBox => { diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 7e32b12638..ff38b2f439 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -29,6 +29,7 @@ import Toggle from "../Input/Toggle"; import Img from "../Base/Img"; import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; import Title from "../Base/Title"; +import {OsmConnection} from "../../Logic/Osm/OsmConnection"; /** * Shows the question element. @@ -118,24 +119,7 @@ export default class TagRenderingQuestion extends Combine { if (options.bottomText !== undefined) { bottomTags = options.bottomText(inputElement.GetValue()) } else { - bottomTags = new VariableUiElement( - inputElement.GetValue().map( - (tagsFilter: TagsFilter) => { - const csCount = state?.osmConnection?.userDetails?.data?.csCount ?? 1000; - if (csCount < Constants.userJourney.tagsVisibleAt) { - return ""; - } - if (tagsFilter === undefined) { - return Translations.t.general.noTagsSelected.SetClass("subtle"); - } - if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { - const tagsStr = tagsFilter.asHumanString(false, true, tags.data); - return new FixedUiElement(tagsStr).SetClass("subtle"); - } - return tagsFilter.asHumanString(true, true, tags.data); - } - ) - ).SetClass("block break-all") + bottomTags = TagRenderingQuestion.CreateTagExplanation(inputElement.GetValue(), tags, state) } super([ question, @@ -465,5 +449,28 @@ export default class TagRenderingQuestion extends Combine { return inputTagsFilter; } + + public static CreateTagExplanation(selectedValue: UIEventSource, + tags: UIEventSource, + state?: {osmConnection?: OsmConnection}){ + return new VariableUiElement( + selectedValue.map( + (tagsFilter: TagsFilter) => { + const csCount = state?.osmConnection?.userDetails?.data?.csCount ?? Constants.userJourney.tagsVisibleAndWikiLinked + 1; + if (csCount < Constants.userJourney.tagsVisibleAt) { + return ""; + } + if (tagsFilter === undefined) { + return Translations.t.general.noTagsSelected.SetClass("subtle"); + } + if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { + const tagsStr = tagsFilter.asHumanString(false, true, tags.data); + return new FixedUiElement(tagsStr).SetClass("subtle"); + } + return tagsFilter.asHumanString(true, true, tags.data); + } + ) + ).SetClass("block break-all") + } } \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index 105fca8f9c..870a6c3d5b 100644 --- a/langs/en.json +++ b/langs/en.json @@ -11,6 +11,8 @@ "delete": "Delete", "explanations": { "hardDelete": "This point will be deleted in OpenStreetMap. It can be recovered by an experienced contributor", + "retagNoOtherThemes": "This feature will be reclassified and hidden from this application", + "retagOtherThemes": "This feature will be retagged and visible in {othterThemes}", "selectReason": "Please, select why this feature should be deleted", "softDelete": "This feature will be updated and hidden from this application. {reason}" },