Refactoring of deleteWizard

This commit is contained in:
Pieter Vander Vennet 2022-05-01 04:17:40 +02:00
parent b941fb2983
commit fd90914c35
8 changed files with 207 additions and 206 deletions

View file

@ -1,23 +1,43 @@
import {Translation} from "../../UI/i18n/Translation"; import {Translation, TypedTranslation} from "../../UI/i18n/Translation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {DeleteConfigJson} from "./Json/DeleteConfigJson"; import {DeleteConfigJson} from "./Json/DeleteConfigJson";
import Translations from "../../UI/i18n/Translations"; import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils"; import {TagUtils} from "../../Logic/Tags/TagUtils";
export default class DeleteConfig { 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?: { public readonly extraDeleteReasons?: {
explanation: Translation, explanation: TypedTranslation<object>,
changesetMessage: string changesetMessage: string
}[] }[]
public readonly nonDeleteMappings?: { if: TagsFilter, then: Translation }[] public readonly nonDeleteMappings?: { if: TagsFilter, then: TypedTranslation<object> }[]
public readonly softDeletionTags?: TagsFilter public readonly softDeletionTags?: TagsFilter
public readonly neededChangesets?: number public readonly neededChangesets?: number
constructor(json: DeleteConfigJson, context: string) { constructor(json: DeleteConfigJson, context: string) {
this.extraDeleteReasons = json.extraDeleteReasons?.map((reason, i) => { this.extraDeleteReasons = (json.extraDeleteReasons ?? []).map((reason, i) => {
const ctx = `${context}.extraDeleteReasons[${i}]` const ctx = `${context}.extraDeleteReasons[${i}]`
if ((reason.changesetMessage ?? "").length <= 5) { if ((reason.changesetMessage ?? "").length <= 5) {
throw `${ctx}.explanation is too short, needs at least 4 characters` throw `${ctx}.explanation is too short, needs at least 4 characters`
@ -27,7 +47,7 @@ export default class DeleteConfig {
changesetMessage: reason.changesetMessage changesetMessage: reason.changesetMessage
} }
}) })
this.nonDeleteMappings = json.nonDeleteMappings?.map((nonDelete, i) => { this.nonDeleteMappings = (json.nonDeleteMappings??[]).map((nonDelete, i) => {
const ctx = `${context}.extraDeleteReasons[${i}]` const ctx = `${context}.extraDeleteReasons[${i}]`
return { return {
if: TagUtils.Tag(nonDelete.if, ctx + ".if"), if: TagUtils.Tag(nonDelete.if, ctx + ".if"),

View file

@ -36,7 +36,17 @@ export interface DeleteConfigJson {
* By adding a 'nonDeleteMapping', an option can be added into the list which will retag the feature. * 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! * 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). * 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. * 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 neededChangesets?: number
} }

View file

@ -54,7 +54,7 @@ export default class MoreScreen extends Combine {
if(searchTerm === "personal"){ if(searchTerm === "personal"){
window.location.href = MoreScreen.createUrlFor({id: "personal"}, false, state).data 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" window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
} }
if(searchTerm === "source") { if(searchTerm === "source") {

View file

@ -12,7 +12,7 @@ export class RadioButton<T> extends InputElement<T> {
constructor( constructor(
elements: InputElement<T>[], elements: InputElement<T>[],
options?: { options?: {
selectFirstAsDefault?: boolean, selectFirstAsDefault?: true | boolean,
dontStyle?: boolean dontStyle?: boolean
} }
) { ) {

View file

@ -5,23 +5,26 @@ import Svg from "../../Svg";
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; import DeleteAction from "../../Logic/Osm/Actions/DeleteAction";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import TagRenderingQuestion from "./TagRenderingQuestion";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation"; import {Translation} from "../i18n/Translation";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import Constants from "../../Models/Constants"; 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 DeleteConfig from "../../Models/ThemeConfig/DeleteConfig";
import {OsmObject} from "../../Logic/Osm/OsmObject"; 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 {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 { export default class DeleteWizard extends Toggle {
/** /**
* The UI-element which triggers 'deletion' (either soft or hard). * 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 * @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, constructor(id: string,
state: { state: FeaturePipelineState,
osmConnection: OsmConnection;
allElements: ElementStorage,
layoutToUse?: LayoutConfig,
changes?: Changes
},
options: DeleteConfig) { options: DeleteConfig) {
@ -60,70 +58,88 @@ export default class DeleteWizard extends Toggle {
const confirm = new UIEventSource<boolean>(false) const confirm = new UIEventSource<boolean>(false)
function doDelete(selected: TagsFilter) { /**
// Selected == the reasons, not the tags of the object * This function is the actual delete function
const tgs = selected.asChange(tagsSource.data) */
const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason") function doDelete(selected: { deleteReason: string } | { retagTo: TagsFilter }) {
if (deleteReasonMatch.length === 0) { let actionToTake: OsmChangeAction;
return; 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, state.changes?.applyAction(actionToTake)
options.softDeletionTags,
{
theme: state?.layoutToUse?.id ?? "unkown",
specialMotivation: deleteReasonMatch[0]?.v
},
deleteAbility.canBeDeleted.data.canBeDeleted
)
state.changes?.applyAction(deleteAction)
isDeleted.setData(true) isDeleted.setData(true)
} }
const t = Translations.t.delete const t = Translations.t.delete
const cancelButton = t.cancel.Clone().SetClass("block btn btn-secondary").onClick(() => confirm.setData(false)); const cancelButton = t.cancel.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)
}
)
}))
/** /**
* The button which is shown first. Opening it will trigger the check for deletions * The button which is shown first. Opening it will trigger the check for deletions
*/ */
const deleteButton = new SubtleButton( const deleteButton = new SubtleButton(
Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), t.delete.Clone()).onClick( Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), t.delete)
() => { .onClick(
deleteAbility.CheckDeleteability(true) () => {
confirm.setData(true); deleteAbility.CheckDeleteability(true)
} confirm.setData(true);
) }
)
const isShown: UIEventSource<boolean> = 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<boolean>(id.indexOf("-") < 0)
super( super(
new Toggle( new Toggle(
new Combine([Svg.delete_icon_svg().SetClass("h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full"), 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(
new Toggle( new Toggle(
new Toggle( new Toggle(
question, deleteDialog,
new SubtleButton(Svg.envelope_ui(), t.readMessages.Clone()), new SubtleButton(Svg.envelope_ui(), t.readMessages),
state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0) 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([ new Combine([
Svg.delete_not_allowed_svg().SetStyle("height: 2rem; width: auto").SetClass("mr-2"), Svg.delete_not_allowed_svg().SetStyle("height: 2rem; width: auto").SetClass("mr-2"),
new Combine([ new Combine([
t.cannotBeDeleted.Clone(), t.cannotBeDeleted,
cbd.reason.Clone().SetClass("subtle"), cbd.reason.SetClass("subtle"),
t.useSomethingElse.Clone().SetClass("subtle")]).SetClass("flex flex-col") t.useSomethingElse.SetClass("subtle")]).SetClass("flex flex-col")
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200"))) ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200")))
, ,
deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)),
t.loginToDelete.Clone().onClick(state.osmConnection.AttemptLogin), t.loginToDelete.onClick(state.osmConnection.AttemptLogin),
state.osmConnection.isLoggedIn state.osmConnection.isLoggedIn
), ),
isDeleted), isDeleted),
@ -153,17 +169,17 @@ export default class DeleteWizard extends Toggle {
} }
private static constructConfirmButton(deleteReasons: UIEventSource<TagsFilter>): BaseUIElement { private static constructConfirmButton(deleteReasons: UIEventSource<any | undefined>): BaseUIElement {
const t = Translations.t.delete; const t = Translations.t.delete;
const btn = new Combine([ const btn = new Combine([
Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"), Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"),
t.delete.Clone() t.delete
]).SetClass("flex btn bg-red-500") ]).SetClass("flex btn bg-red-500")
const btnNonActive = new Combine([ const btnNonActive = new Combine([
Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"), 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") ]).SetClass("flex btn btn-disabled bg-red-200")
return new Toggle( return new Toggle(
@ -175,111 +191,83 @@ export default class DeleteWizard extends Toggle {
} }
private static constructExplanation(tags: UIEventSource<TagsFilter>, deleteAction: DeleteabilityChecker) { private static constructExplanation(selectedOption: UIEventSource<
{deleteReason: string} | {retagTo: TagsFilter}>, deleteAction: DeleteabilityChecker,
currentTags: UIEventSource<object>,
state?: {osmConnection?: OsmConnection}) {
const t = Translations.t.delete; const t = Translations.t.delete;
return new VariableUiElement(tags.map( return new VariableUiElement(selectedOption.map(
currentTags => { selectedOption => {
const cbd = deleteAction.canBeDeleted.data; if (selectedOption === undefined) {
if (currentTags === undefined) { return t.explanations.selectReason.SetClass("subtle");
return t.explanations.selectReason.Clone().SetClass("subtle");
} }
const hasDeletionTag = currentTags.asChange(currentTags).some(kv => kv.k === "_delete_reason") const retag: TagsFilter | undefined = selectedOption["retagTo"]
if(retag !== undefined) {
if (cbd.canBeDeleted && hasDeletionTag) { // This is a retagging, not a deletion of any kind
return t.explanations.hardDelete.Clone() return new Combine([t.explanations.retagNoOtherThemes,
TagRenderingQuestion.CreateTagExplanation(new UIEventSource<TagsFilter>(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] , [deleteAction.canBeDeleted]
)).SetClass("block") )).SetClass("block")
} }
private static generateDeleteTagRenderingConfig(softDeletionTags: TagsFilter, private static constructMultipleChoice(config: DeleteConfig, tagsSource: UIEventSource<object>, state: FeaturePipelineState):
nonDeleteOptions: { if: TagsFilter; then: Translation }[], InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> {
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)
extraOptionsStr.push({ const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[ ] = []
if: {and: newIf},
then: nonDeleteOption.then for (const nonDeleteOption of config.nonDeleteMappings) {
}) elements.push(new FixedInputElement(
new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state),
{
retagTo: nonDeleteOption.if
}
))
} }
for (const extraDeleteReason of (extraDeleteReasons ?? [])) { for (const extraDeleteReason of (config.extraDeleteReasons ?? [])) {
extraOptionsStr.push({ elements.push(new FixedInputElement(
if: {and: ["_delete_reason=" + extraDeleteReason.changesetMessage]}, new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state),
then: extraDeleteReason.explanation {
}) 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
}
))
}
{ return new RadioButton(elements, {selectFirstAsDefault: false});
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"
)
} }
} }
class DeleteabilityChecker { class DeleteabilityChecker {

View file

@ -15,13 +15,8 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import MoveWizard from "./MoveWizard"; import MoveWizard from "./MoveWizard";
import Toggle from "../Input/Toggle"; 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 Lazy from "../Base/Lazy";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
export default class FeatureInfoBox extends ScrollableFullScreen { export default class FeatureInfoBox extends ScrollableFullScreen {
@ -29,18 +24,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
public constructor( public constructor(
tags: UIEventSource<any>, tags: UIEventSource<any>,
layerConfig: LayerConfig, layerConfig: LayerConfig,
state: { state: FeaturePipelineState,
filteredLayers: UIEventSource<FilteredLayer[]>;
backgroundLayer: UIEventSource<BaseLayer>;
featureSwitchIsTesting: UIEventSource<boolean>;
featureSwitchIsDebugging: UIEventSource<boolean>;
featureSwitchShowAllQuestions: UIEventSource<boolean>;
osmConnection: OsmConnection,
featureSwitchUserbadge: UIEventSource<boolean>,
changes: Changes,
layoutToUse: LayoutConfig,
allElements: ElementStorage
},
hashToShow?: string, hashToShow?: string,
isShown?: UIEventSource<boolean>, isShown?: UIEventSource<boolean>,
) { ) {
@ -78,18 +62,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
private static GenerateContent(tags: UIEventSource<any>, private static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig, layerConfig: LayerConfig,
state: { state: FeaturePipelineState): BaseUIElement {
filteredLayers: UIEventSource<FilteredLayer[]>;
backgroundLayer: UIEventSource<BaseLayer>;
featureSwitchIsTesting: UIEventSource<boolean>;
featureSwitchIsDebugging: UIEventSource<boolean>;
featureSwitchShowAllQuestions: UIEventSource<boolean>;
osmConnection: OsmConnection,
featureSwitchUserbadge: UIEventSource<boolean>,
changes: Changes,
layoutToUse: LayoutConfig,
allElements: ElementStorage
}): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>(); let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>();
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group)) 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<string, QuestionBox>, private static createEditElements(questionBoxes: Map<string, QuestionBox>,
layerConfig: LayerConfig, layerConfig: LayerConfig,
tags: UIEventSource<any>, tags: UIEventSource<any>,
state: { filteredLayers: UIEventSource<FilteredLayer[]>; backgroundLayer: UIEventSource<BaseLayer>; featureSwitchIsTesting: UIEventSource<boolean>; featureSwitchIsDebugging: UIEventSource<boolean>; featureSwitchShowAllQuestions: UIEventSource<boolean>; osmConnection: OsmConnection; featureSwitchUserbadge: UIEventSource<boolean>; changes: Changes; layoutToUse: LayoutConfig; allElements: ElementStorage }) state: FeaturePipelineState)
: BaseUIElement { : BaseUIElement {
let editElements: BaseUIElement[] = [] let editElements: BaseUIElement[] = []
questionBoxes.forEach(questionBox => { questionBoxes.forEach(questionBox => {

View file

@ -29,6 +29,7 @@ import Toggle from "../Input/Toggle";
import Img from "../Base/Img"; import Img from "../Base/Img";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import Title from "../Base/Title"; import Title from "../Base/Title";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
/** /**
* Shows the question element. * Shows the question element.
@ -118,24 +119,7 @@ export default class TagRenderingQuestion extends Combine {
if (options.bottomText !== undefined) { if (options.bottomText !== undefined) {
bottomTags = options.bottomText(inputElement.GetValue()) bottomTags = options.bottomText(inputElement.GetValue())
} else { } else {
bottomTags = new VariableUiElement( bottomTags = TagRenderingQuestion.CreateTagExplanation(inputElement.GetValue(), tags, state)
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")
} }
super([ super([
question, question,
@ -465,5 +449,28 @@ export default class TagRenderingQuestion extends Combine {
return inputTagsFilter; return inputTagsFilter;
} }
public static CreateTagExplanation(selectedValue: UIEventSource<TagsFilter>,
tags: UIEventSource<object>,
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")
}
} }

View file

@ -11,6 +11,8 @@
"delete": "Delete", "delete": "Delete",
"explanations": { "explanations": {
"hardDelete": "This point will be deleted in OpenStreetMap. It can be recovered by an experienced contributor", "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", "selectReason": "Please, select why this feature should be deleted",
"softDelete": "This feature will be updated and hidden from this application. <span class='subtle'>{reason}</span>" "softDelete": "This feature will be updated and hidden from this application. <span class='subtle'>{reason}</span>"
}, },