diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index e7fd28698f..ff5de32bf7 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -64,6 +64,7 @@ export class Changes implements FeatureSource{ if (elementTags[change.k] !== change.v) { elementTags[change.k] = change.v; console.log("Applied ", change.k, "=", change.v) + // We use 'elementTags.id' here, as we might have retrieved with the id 'node/-1' as new point, but should use the rewritten id this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v}); } } diff --git a/Logic/Osm/DeleteAction.ts b/Logic/Osm/DeleteAction.ts index 8694a522f8..5671504dd1 100644 --- a/Logic/Osm/DeleteAction.ts +++ b/Logic/Osm/DeleteAction.ts @@ -8,44 +8,56 @@ import Constants from "../../Models/Constants"; export default class DeleteAction { public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; + public readonly isDeleted = new UIEventSource(false); private readonly _id: string; + constructor(id: string) { this._id = id; - this.canBeDeleted = new UIEventSource<{canBeDeleted?: boolean; reason: Translation}>({ - canBeDeleted : false, + this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({ + canBeDeleted: undefined, reason: Translations.t.delete.loading }) - - this.CheckDeleteability() + + this.CheckDeleteability(false) } - public DoDelete(reason: string): UIEventSource { - const isDeleted = new UIEventSource(false) - + /** + * Does actually delete the feature; returns the event source 'this.isDeleted' + * If deletion is not allowed, triggers the callback instead + */ + public DoDelete(reason: string, onNotAllowed : () => void): UIEventSource { + const isDeleted = this.isDeleted const self = this; let deletionStarted = false; this.canBeDeleted.addCallbackAndRun( canBeDeleted => { + if (isDeleted.data || deletionStarted) { + // Already deleted... + return; + } + + if(canBeDeleted.canBeDeleted === false){ + // We aren't allowed to delete + deletionStarted = true; + onNotAllowed(); + isDeleted.setData(true); + return; + } + if (!canBeDeleted) { // We are not allowed to delete (yet), this might change in the future though return; } - if (isDeleted.data) { - // Already deleted... - return; - } + - if (deletionStarted) { - // Deletion is already running... - return; - } + deletionStarted = true; OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => { - if(obj === undefined){ + if (obj === undefined) { return; } State.state.osmConnection.changesetHandler.DeleteElement( @@ -58,7 +70,7 @@ export default class DeleteAction { } ) }) - + } ) @@ -71,7 +83,7 @@ export default class DeleteAction { * @constructor * @private */ - private CheckDeleteability(): void { + public CheckDeleteability(useTheInternet: boolean): void { const t = Translations.t.delete; const id = this._id; const state = this.canBeDeleted @@ -89,7 +101,7 @@ export default class DeleteAction { if (ud === undefined) { return undefined; } - if(!ud.loggedIn){ + if (!ud.loggedIn) { return false; } return ud.csCount >= Constants.userJourney.deletePointsOfOthersUnlock; @@ -120,7 +132,7 @@ export default class DeleteAction { // At this point, the logged in user is not allowed to delete points created/edited by _others_ // however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all! - if (allByMyself.data === null) { + if (allByMyself.data === null && useTheInternet) { // We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"])).syncWith(previousEditors) } @@ -142,7 +154,7 @@ export default class DeleteAction { const hasRelations: UIEventSource = new UIEventSource(null) const hasWays: UIEventSource = new UIEventSource(null) deletetionAllowed.addCallbackAndRunD(deletetionAllowed => { - + if (deletetionAllowed === false) { // Nope, we are not allowed to delete state.setData({ @@ -152,6 +164,9 @@ export default class DeleteAction { return; } + if (!useTheInternet) { + return; + } // All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rels => { @@ -171,6 +186,9 @@ export default class DeleteAction { if (hasWays.data === true) { return true; } + if (hasWays.data === null || hasRelationsData === null) { + return null; + } if (hasWays.data === false && hasRelationsData === false) { return false; } @@ -189,13 +207,15 @@ export default class DeleteAction { canBeDeleted: false, reason: t.partOfOthers }) + }else{ + // alright, this point can be safely deleted! + state.setData({ + canBeDeleted: true, + reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete + }) } - // alright, this point can be safely deleted! - state.setData({ - canBeDeleted: true, - reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete - }) + } ) diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index a3a465006d..c16833ae58 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -51,7 +51,7 @@ export class Tag extends TagsFilter { return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); } - asHumanString(linkToWiki: boolean, shorten: boolean) { + asHumanString(linkToWiki?: boolean, shorten?: boolean) { let v = this.value; if (shorten) { v = Utils.EllipsesAfter(v, 25); diff --git a/Models/Constants.ts b/Models/Constants.ts index 3482b3193d..5dea2653ec 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -9,7 +9,7 @@ export default class Constants { moreScreenUnlock: 1, personalLayoutUnlock: 5, historyLinkVisible: 10, - deletePointsOfOthersUnlock: 15, + deletePointsOfOthersUnlock: 20, tagsVisibleAt: 25, tagsVisibleAndWikiLinked: 30, diff --git a/UI/Popup/DeleteButton.ts b/UI/Popup/DeleteButton.ts deleted file mode 100644 index 37e0d12706..0000000000 --- a/UI/Popup/DeleteButton.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {VariableUiElement} from "../Base/VariableUIElement"; -import State from "../../State"; -import Toggle from "../Input/Toggle"; -import Translations from "../i18n/Translations"; -import {SubtleButton} from "../Base/SubtleButton"; -import Svg from "../../Svg"; -import DeleteAction from "../../Logic/Osm/DeleteAction"; -import {Tag} from "../../Logic/Tags/Tag"; -import CheckBoxes from "../Input/Checkboxes"; -import {RadioButton} from "../Input/RadioButton"; -import {FixedInputElement} from "../Input/FixedInputElement"; -import {TextField} from "../Input/TextField"; - - -export default class DeleteWizard extends Toggle { - /** - * The UI-element which triggers 'deletion' (either soft or hard). - * - * - A 'hard deletion' is if the point is actually deleted from the OSM database - * - A 'soft deletion' is if the point is not deleted, but the tagging is modified which will result in the point not being picked up by the filters anymore. - * Apart having needing theme-specific tags added (which must be supplied by the theme creator), fixme='marked for deletion' will be added too - * - * A deletion is only possible if the user is logged in. - * A soft deletion is only possible if tags are provided - * A hard deletion is only possible if the user has sufficient rigts - * - * If no deletion is possible at all, the delete button will not be shown - but a reason will be shown instead. - * - * @param id: The id of the element to remove - * @param 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, softDeletionTags? : Tag[]) { - const t = Translations.t.delete - - const deleteAction = new DeleteAction(id); - - const deleteReasons = new RadioButton( - [new FixedInputElement( - t.reasons.test, "test" - ), - new FixedInputElement(t.reasons.disused, "disused"), - new FixedInputElement(t.reasons.notFound, "not found"), - new TextField()] - - ) - - const deleteButton = new SubtleButton( - Svg.delete_icon_svg(), - t.delete.Clone() - ).onClick(() => deleteAction.DoDelete(deleteReasons.GetValue().data)) - - - - - super( - - - - new Toggle( - deleteButton, - new VariableUiElement(deleteAction.canBeDeleted.map(cbd => cbd.reason.Clone())), - deleteAction.canBeDeleted.map(cbd => cbd.canBeDeleted) - ), - - - - t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), - State.state.osmConnection.isLoggedIn - ) - - } -} \ No newline at end of file diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts new file mode 100644 index 0000000000..37f6efe5bf --- /dev/null +++ b/UI/Popup/DeleteWizard.ts @@ -0,0 +1,244 @@ +import {VariableUiElement} from "../Base/VariableUIElement"; +import State from "../../State"; +import Toggle from "../Input/Toggle"; +import Translations from "../i18n/Translations"; +import Svg from "../../Svg"; +import DeleteAction from "../../Logic/Osm/DeleteAction"; +import {Tag} from "../../Logic/Tags/Tag"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import TagRenderingQuestion from "./TagRenderingQuestion"; +import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; +import Combine from "../Base/Combine"; +import {SubtleButton} from "../Base/SubtleButton"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {Translation} from "../i18n/Translation"; +import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson"; +import BaseUIElement from "../BaseUIElement"; +import {Changes} from "../../Logic/Osm/Changes"; +import {And} from "../../Logic/Tags/And"; + + +export default class DeleteWizard extends Toggle { + /** + * The UI-element which triggers 'deletion' (either soft or hard). + * + * - A 'hard deletion' is if the point is actually deleted from the OSM database + * - A 'soft deletion' is if the point is not deleted, but the tagging is modified which will result in the point not being picked up by the filters anymore. + * Apart having needing theme-specific tags added (which must be supplied by the theme creator), fixme='marked for deletion' will be added too + * + * A deletion is only possible if the user is logged in. + * A soft deletion is only possible if tags are provided + * A hard deletion is only possible if the user has sufficient rigts + * + * There is also the possibility to have a 'trojan horse' option. If the user selects that option, it is NEVER removed, but the tags are applied. + * Ideal for the case of "THIS PATH IS ON MY GROUND AND SHOULD BE DELETED IMMEDIATELY OR I WILL GET MY LAWYER" but to mark it as private instead. + * (Note that _delete_reason is used as trigger to do actual deletion - setting such a tag WILL delete from the database with that as changeset comment) + * + * @param id: The id of the element to remove + * @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, + options?: { + noDeleteOptions?: { if: Tag[], then: Translation }[] + softDeletionTags?: Tag[] + }) { + + options = options ?? {} + const deleteAction = new DeleteAction(id); + const tagsSource = State.state.allElements.getEventSourceById(id) + + let softDeletionTags = options.softDeletionTags ?? [] + const allowSoftDeletion = softDeletionTags.length > 0 + + const confirm = new UIEventSource(false) + + + function softDelete(reason: string, tagsToApply: { k: string, v: string }[]) { + if (reason !== undefined) { + tagsToApply.splice(0, 0, { + k: "fixme", + v: `A mapcomplete user marked this feature to be deleted (${reason})` + }) + } + (State.state?.changes ?? new Changes()) + .addTag(id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource); + } + + function doDelete(selected: TagsFilter) { + const tgs = selected.asChange(tagsSource.data) + const deleteReasonMatch = tgs.filter(kv => kv.k === "_delete_reason") + if (deleteReasonMatch.length > 0) { + // We should actually delete! + const deleteReason = deleteReasonMatch[0].v + deleteAction.DoDelete(deleteReason, () => { + // The user doesn't have sufficient permissions to _actually_ delete the feature + // We 'soft delete' instead (and add a fixme) + softDelete(deleteReason, tgs.filter(kv => kv.k !== "_delete_reason")) + + }); + return + } else { + // This is an injected tagging + softDelete(undefined, tgs) + } + + } + + + const t = Translations.t.delete + const cancelButton = t.cancel.Clone().SetClass("block btn btn-secondary").onClick(() => confirm.setData(false)); + const config = DeleteWizard.generateDeleteTagRenderingConfig(softDeletionTags, options.noDeleteOptions) + const question = new TagRenderingQuestion( + tagsSource, + config, + { + cancelButton: 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, deleteAction) + } + ) + + /** + * The button which is shown first. Opening it will trigger the check for deletions + */ + const deleteButton = new SubtleButton(Svg.delete_icon_svg(), t.delete).onClick( + () => { + deleteAction.CheckDeleteability(true) + confirm.setData(true); + } + ); + + super( + 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"), + new Toggle( + new Toggle( + new Toggle( + question, + + deleteButton, + confirm), + new VariableUiElement(deleteAction.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse]))), + deleteAction.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)), + t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin), + State.state.osmConnection.isLoggedIn + ), + deleteAction.isDeleted) + + } + + + 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() + ]).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() + ]).SetClass("flex btn btn-disabled bg-red-200") + + return new Toggle( + btn, + btnNonActive, + deleteReasons.map(reason => reason !== undefined) + ) + + } + + + private static constructExplanation(tags: UIEventSource, deleteAction: DeleteAction) { + 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"); + } + + const hasDeletionTag = currentTags.asChange(currentTags).some(kv => kv.k === "_delete_reason") + + if (cbd.canBeDeleted && hasDeletionTag) { + return t.explanations.hardDelete.Clone() + } + return new Combine([t.explanations.softDelete.Subs({reason: cbd.reason}), + new FixedUiElement(currentTags.asHumanString(false, true, currentTags)).SetClass("subtle") + ]).SetClass("flex flex-col") + + + } + , [deleteAction.canBeDeleted] + )).SetClass("block") + } + + private static generateDeleteTagRenderingConfig(softDeletionTags: Tag[], nonDeleteOptions: { + if: Tag[], + then: Translation + }[]) { + const t = Translations.t.delete + nonDeleteOptions = nonDeleteOptions ?? [] + const softDeletionTagsStr = (softDeletionTags ?? []).map(t => t.asHumanString(false, false)) + const nonDeleteOptionsStr: { if: AndOrTagConfigJson, then: any }[] = [] + for (const nonDeleteOption of nonDeleteOptions) { + const newIf: string[] = nonDeleteOption.if.map(tag => tag.asHumanString()) + + nonDeleteOptionsStr.push({ + if: {and: newIf}, + then: nonDeleteOption.then + }) + } + + return new TagRenderingConfig( + { + question: t.whyDelete, + render: "Deleted because {_delete_reason}", + freeform: { + key: "_delete_reason", + addExtraTags: softDeletionTagsStr + }, + mappings: [ + + ...nonDeleteOptionsStr, + + { + 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 + } + ] + + + }, undefined, "Delete wizard" + ) + } + +} \ No newline at end of file diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 10ac7be1aa..0d735fa923 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -43,11 +43,14 @@ export default class EditableTagRendering extends Toggle { editMode.setData(false) }); - const question = new TagRenderingQuestion(tags, configuration,units, - () => { - editMode.setData(false) - }, - cancelbutton) + const question = new TagRenderingQuestion(tags, configuration, + { + units: units, + cancelButton: cancelbutton, + afterSave: () => { + editMode.setData(false) + } + }) rendering = new Toggle( diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index 7c17aa0b38..f2c09625ea 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -27,17 +27,20 @@ export default class QuestionBox extends VariableUiElement { } const tagRenderingQuestions = tagRenderings - .map((tagRendering, i) => new TagRenderingQuestion(tagsSource, tagRendering, units, - () => { - // We save - skippedQuestions.ping(); - }, - Translations.t.general.skip.Clone() - .SetClass("btn btn-secondary mr-3") - .onClick(() => { - skippedQuestions.data.push(i); + .map((tagRendering, i) => new TagRenderingQuestion(tagsSource, tagRendering, + { + units: units, + afterSave: () => { + // We save skippedQuestions.ping(); - }) + }, + cancelButton: Translations.t.general.skip.Clone() + .SetClass("btn btn-secondary mr-3") + .onClick(() => { + skippedQuestions.data.push(i); + skippedQuestions.ping(); + }) + } )); const skippedQuestionsButton = Translations.t.general.skippedQuestions.Clone() diff --git a/UI/Popup/SaveButton.ts b/UI/Popup/SaveButton.ts index 618a673e2d..f8ad9ba502 100644 --- a/UI/Popup/SaveButton.ts +++ b/UI/Popup/SaveButton.ts @@ -17,16 +17,16 @@ export class SaveButton extends Toggle { const isSaveable = value.map(v => v !== false && (v ?? "") !== "") - - const saveEnabled = Translations.t.general.save.Clone().SetClass(`btn`); - const saveDisabled = Translations.t.general.save.Clone().SetClass(`btn btn-disabled`); + const text = Translations.t.general.save + const saveEnabled = text.Clone().SetClass(`btn`); + const saveDisabled = text.SetClass(`btn btn-disabled`); const save = new Toggle( saveEnabled, saveDisabled, isSaveable ) super( - save, + save, pleaseLogin, osmConnection?.isLoggedIn ?? new UIEventSource(false) ) diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 4fc48f2cc9..1da92105b9 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -30,23 +30,27 @@ import {Unit} from "../../Customizations/JSON/Denomination"; * Note that the value _migh_ already be known, e.g. when selected or when changing the value */ export default class TagRenderingQuestion extends Combine { - + constructor(tags: UIEventSource, configuration: TagRenderingConfig, - units: Unit[], - afterSave?: () => void, - cancelButton?: BaseUIElement, + options?: { + units?: Unit[], + afterSave?: () => void, + cancelButton?: BaseUIElement, + saveButtonConstr?: (src: UIEventSource) => BaseUIElement, + bottomText?: (src: UIEventSource) => BaseUIElement + } ) { if (configuration === undefined) { throw "A question is needed for a question visualization" } - const applicableUnit = (units ?? []).filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0]; + options = options ?? {} + const applicableUnit = (options.units ?? []).filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0]; const question = new SubstitutedTranslation(configuration.question, tags) .SetClass("question-text"); - - const inputElement = TagRenderingQuestion.GenerateInputElement(configuration, applicableUnit, tags) + const inputElement: InputElement = TagRenderingQuestion.GenerateInputElement(configuration, applicableUnit, tags) const save = () => { const selection = inputElement.GetValue().data; console.log("Save button clicked, the tags are is", selection) @@ -55,48 +59,54 @@ export default class TagRenderingQuestion extends Combine { .addTag(tags.data.id, selection, tags); } - if (afterSave) { - afterSave(); + if (options.afterSave) { + options.afterSave(); } } + if (options.saveButtonConstr === undefined) { + options.saveButtonConstr = v => new SaveButton(v, + State.state?.osmConnection) + .onClick(save) + } - const saveButton = new SaveButton(inputElement.GetValue(), - State.state?.osmConnection) - .onClick(save) + const saveButton = options.saveButtonConstr(inputElement.GetValue()) - - const appliedTags = new VariableUiElement( - inputElement.GetValue().map( - (tagsFilter: TagsFilter) => { - const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; - if (csCount < Constants.userJourney.tagsVisibleAt) { - return ""; + let bottomTags: BaseUIElement; + if (options.bottomText !== undefined) { + bottomTags = options.bottomText(inputElement.GetValue()) + } else { + bottomTags = new VariableUiElement( + inputElement.GetValue().map( + (tagsFilter: TagsFilter) => { + const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; + if (csCount < Constants.userJourney.tagsVisibleAt) { + return ""; + } + if (tagsFilter === undefined) { + return Translations.t.general.noTagsSelected.Clone().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); } - if (tagsFilter === undefined) { - return Translations.t.general.noTagsSelected.Clone().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 ([ + ) + ).SetClass("block break-all") + } + super([ question, - inputElement, - cancelButton, + inputElement, + options.cancelButton, saveButton, - appliedTags] + bottomTags] ) - this .SetClass("question") + this.SetClass("question") } - private static GenerateInputElement(configuration: TagRenderingConfig, applicableUnit: Unit, tagsSource: UIEventSource< any>): InputElement { + private static GenerateInputElement(configuration: TagRenderingConfig, applicableUnit: Unit, tagsSource: UIEventSource): InputElement { let inputEls: InputElement[]; const mappings = (configuration.mappings ?? []) @@ -105,7 +115,7 @@ export default class TagRenderingQuestion extends Combine { return false; } return !(typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(tagsSource.data)); - + }) @@ -255,10 +265,10 @@ export default class TagRenderingQuestion extends Combine { private static GenerateMappingElement( tagsSource: UIEventSource, mapping: { - if: TagsFilter, - then: Translation, - hideInAnswer: boolean | TagsFilter - }, ifNot?: TagsFilter[]): InputElement { + if: TagsFilter, + then: Translation, + hideInAnswer: boolean | TagsFilter + }, ifNot?: TagsFilter[]): InputElement { let tagging = mapping.if; if (ifNot.length > 0) { diff --git a/UI/i18n/Translations.ts b/UI/i18n/Translations.ts index 5a0c81459b..eb91b6cd86 100644 --- a/UI/i18n/Translations.ts +++ b/UI/i18n/Translations.ts @@ -30,6 +30,9 @@ export default class Translations { console.error(msg, t); throw msg } + if(t instanceof Translation){ + return t; + } return new Translation(t, context); } diff --git a/langs/en.json b/langs/en.json index 9ec7875010..e1a8b99538 100644 --- a/langs/en.json +++ b/langs/en.json @@ -29,17 +29,26 @@ }, "delete": { "delete": "Delete", + "cancel": "Cancel", + "isDeleted": "This feature is deleted", "loginToDelete": "You must be logged in to delete a point", - "safeDelete": "This point can be safely deleted", - "isntAPoint": "Only points can be deleted", - "onlyEditedByLoggedInUser": "This point has only be edited by yourself, you can safely delete it", - "notEnoughExperience": "You don't have enough experience to delete points made by other people. Make more edits to improve your skills", - "partOfOthers": "This point is part of some way or relation, so you can not delete it", - "loading": "Inspecting properties to check if this feature can be deleted", + "safeDelete": "This point can be safely deleted.", + "isntAPoint": "Only points can be deleted, the selected feature is a way, area or relation.", + "onlyEditedByLoggedInUser": "This point has only be edited by yourself, you can safely delete it.", + "notEnoughExperience": "This point was made by someone else.", + "useSomethingElse": "Use another OpenStreetMap-editor to delete it instead", + "partOfOthers": "This point is part of some way or relation and can not be deleted directly.", + "loading": "Inspecting properties to check if this feature can be deleted.", + "whyDelete": "Why should this point be deleted?", "reasons": { "test": "This was a testing point - the feature was never actually there", "disused": "This feature is disused or removed", "notFound": "This feature couldn't be found" + }, + "explanations": { + "selectReason": "Please, select why this feature should be deleted", + "hardDelete": "This point will be deleted in OpenStreetMap. It can be recovered by an experienced contributor", + "softDelete": "This feature will be updated and hidden from this application. {reason}" } }, "general": { diff --git a/test.ts b/test.ts index 25608b2b0b..eb29b9921d 100644 --- a/test.ts +++ b/test.ts @@ -1,7 +1,12 @@ import {OsmObject} from "./Logic/Osm/OsmObject"; -import DeleteButton from "./UI/Popup/DeleteButton"; +import DeleteButton from "./UI/Popup/DeleteWizard"; import Combine from "./UI/Base/Combine"; import State from "./State"; +import DeleteWizard from "./UI/Popup/DeleteWizard"; +import {UIEventSource} from "./Logic/UIEventSource"; +import {Tag} from "./Logic/Tags/Tag"; +import {QueryParameters} from "./Logic/Web/QueryParameters"; +import {Translation} from "./UI/i18n/Translation"; /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; @@ -143,7 +148,19 @@ function TestMiniMap() { featureSource.ping() } //*/ +QueryParameters.GetQueryParameter("test", "true").setData("true") State.state= new State(undefined) +const id = "node/5414688303" +State.state.allElements.addElementById(id, new UIEventSource({id: id})) new Combine([ - new DeleteButton("node/8598664388"), + new DeleteWizard(id, { + noDeleteOptions: [ + { + if:[ new Tag("access","private")], + then: new Translation({ + en: "Very private! Delete now or me send lawfull lawyer" + }) + } + ] + }), ]).AttachTo("maindiv")