First draft of an element which deletes points

This commit is contained in:
Pieter Vander Vennet 2021-07-01 02:26:45 +02:00
parent bbfcee686f
commit 5d3365afb8
13 changed files with 407 additions and 169 deletions

View file

@ -64,6 +64,7 @@ export class Changes implements FeatureSource{
if (elementTags[change.k] !== change.v) { if (elementTags[change.k] !== change.v) {
elementTags[change.k] = change.v; elementTags[change.k] = change.v;
console.log("Applied ", 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}); this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v});
} }
} }

View file

@ -8,44 +8,56 @@ import Constants from "../../Models/Constants";
export default class DeleteAction { export default class DeleteAction {
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>; public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>;
public readonly isDeleted = new UIEventSource<boolean>(false);
private readonly _id: string; private readonly _id: string;
constructor(id: string) { constructor(id: string) {
this._id = id; this._id = id;
this.canBeDeleted = new UIEventSource<{canBeDeleted?: boolean; reason: Translation}>({ this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
canBeDeleted : false, canBeDeleted: undefined,
reason: Translations.t.delete.loading reason: Translations.t.delete.loading
}) })
this.CheckDeleteability() this.CheckDeleteability(false)
} }
public DoDelete(reason: string): UIEventSource<boolean> { /**
const isDeleted = new UIEventSource<boolean>(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<boolean> {
const isDeleted = this.isDeleted
const self = this; const self = this;
let deletionStarted = false; let deletionStarted = false;
this.canBeDeleted.addCallbackAndRun( this.canBeDeleted.addCallbackAndRun(
canBeDeleted => { 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) { if (!canBeDeleted) {
// We are not allowed to delete (yet), this might change in the future though // We are not allowed to delete (yet), this might change in the future though
return; return;
} }
if (isDeleted.data) {
// Already deleted...
return;
}
if (deletionStarted) {
// Deletion is already running...
return;
}
deletionStarted = true; deletionStarted = true;
OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => { OsmObject.DownloadObject(self._id).addCallbackAndRun(obj => {
if(obj === undefined){ if (obj === undefined) {
return; return;
} }
State.state.osmConnection.changesetHandler.DeleteElement( State.state.osmConnection.changesetHandler.DeleteElement(
@ -58,7 +70,7 @@ export default class DeleteAction {
} }
) )
}) })
} }
) )
@ -71,7 +83,7 @@ export default class DeleteAction {
* @constructor * @constructor
* @private * @private
*/ */
private CheckDeleteability(): void { public CheckDeleteability(useTheInternet: boolean): void {
const t = Translations.t.delete; const t = Translations.t.delete;
const id = this._id; const id = this._id;
const state = this.canBeDeleted const state = this.canBeDeleted
@ -89,7 +101,7 @@ export default class DeleteAction {
if (ud === undefined) { if (ud === undefined) {
return undefined; return undefined;
} }
if(!ud.loggedIn){ if (!ud.loggedIn) {
return false; return false;
} }
return ud.csCount >= Constants.userJourney.deletePointsOfOthersUnlock; 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_ // 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! // 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 // 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) 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<boolean> = new UIEventSource<boolean>(null) const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null)
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null) const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null)
deletetionAllowed.addCallbackAndRunD(deletetionAllowed => { deletetionAllowed.addCallbackAndRunD(deletetionAllowed => {
if (deletetionAllowed === false) { if (deletetionAllowed === false) {
// Nope, we are not allowed to delete // Nope, we are not allowed to delete
state.setData({ state.setData({
@ -152,6 +164,9 @@ export default class DeleteAction {
return; 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 // 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 => { OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rels => {
@ -171,6 +186,9 @@ export default class DeleteAction {
if (hasWays.data === true) { if (hasWays.data === true) {
return true; return true;
} }
if (hasWays.data === null || hasRelationsData === null) {
return null;
}
if (hasWays.data === false && hasRelationsData === false) { if (hasWays.data === false && hasRelationsData === false) {
return false; return false;
} }
@ -189,13 +207,15 @@ export default class DeleteAction {
canBeDeleted: false, canBeDeleted: false,
reason: t.partOfOthers 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
})
} }
) )

View file

@ -51,7 +51,7 @@ export class Tag extends TagsFilter {
return new Tag(this.key, TagUtils.ApplyTemplate(this.value as string, tags)); 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; let v = this.value;
if (shorten) { if (shorten) {
v = Utils.EllipsesAfter(v, 25); v = Utils.EllipsesAfter(v, 25);

View file

@ -9,7 +9,7 @@ export default class Constants {
moreScreenUnlock: 1, moreScreenUnlock: 1,
personalLayoutUnlock: 5, personalLayoutUnlock: 5,
historyLinkVisible: 10, historyLinkVisible: 10,
deletePointsOfOthersUnlock: 15, deletePointsOfOthersUnlock: 20,
tagsVisibleAt: 25, tagsVisibleAt: 25,
tagsVisibleAndWikiLinked: 30, tagsVisibleAndWikiLinked: 30,

View file

@ -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<string>(
[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
)
}
}

244
UI/Popup/DeleteWizard.ts Normal file
View file

@ -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<boolean>(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<TagsFilter>): 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<TagsFilter>, 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"
)
}
}

View file

@ -43,11 +43,14 @@ export default class EditableTagRendering extends Toggle {
editMode.setData(false) editMode.setData(false)
}); });
const question = new TagRenderingQuestion(tags, configuration,units, const question = new TagRenderingQuestion(tags, configuration,
() => { {
editMode.setData(false) units: units,
}, cancelButton: cancelbutton,
cancelbutton) afterSave: () => {
editMode.setData(false)
}
})
rendering = new Toggle( rendering = new Toggle(

View file

@ -27,17 +27,20 @@ export default class QuestionBox extends VariableUiElement {
} }
const tagRenderingQuestions = tagRenderings const tagRenderingQuestions = tagRenderings
.map((tagRendering, i) => new TagRenderingQuestion(tagsSource, tagRendering, units, .map((tagRendering, i) => new TagRenderingQuestion(tagsSource, tagRendering,
() => { {
// We save units: units,
skippedQuestions.ping(); afterSave: () => {
}, // We save
Translations.t.general.skip.Clone()
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
skippedQuestions.data.push(i);
skippedQuestions.ping(); 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() const skippedQuestionsButton = Translations.t.general.skippedQuestions.Clone()

View file

@ -17,16 +17,16 @@ export class SaveButton extends Toggle {
const isSaveable = value.map(v => v !== false && (v ?? "") !== "") const isSaveable = value.map(v => v !== false && (v ?? "") !== "")
const text = Translations.t.general.save
const saveEnabled = Translations.t.general.save.Clone().SetClass(`btn`); const saveEnabled = text.Clone().SetClass(`btn`);
const saveDisabled = Translations.t.general.save.Clone().SetClass(`btn btn-disabled`); const saveDisabled = text.SetClass(`btn btn-disabled`);
const save = new Toggle( const save = new Toggle(
saveEnabled, saveEnabled,
saveDisabled, saveDisabled,
isSaveable isSaveable
) )
super( super(
save, save,
pleaseLogin, pleaseLogin,
osmConnection?.isLoggedIn ?? new UIEventSource<any>(false) osmConnection?.isLoggedIn ?? new UIEventSource<any>(false)
) )

View file

@ -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 * Note that the value _migh_ already be known, e.g. when selected or when changing the value
*/ */
export default class TagRenderingQuestion extends Combine { export default class TagRenderingQuestion extends Combine {
constructor(tags: UIEventSource<any>, constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig, configuration: TagRenderingConfig,
units: Unit[], options?: {
afterSave?: () => void, units?: Unit[],
cancelButton?: BaseUIElement, afterSave?: () => void,
cancelButton?: BaseUIElement,
saveButtonConstr?: (src: UIEventSource<TagsFilter>) => BaseUIElement,
bottomText?: (src: UIEventSource<TagsFilter>) => BaseUIElement
}
) { ) {
if (configuration === undefined) { if (configuration === undefined) {
throw "A question is needed for a question visualization" 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) const question = new SubstitutedTranslation(configuration.question, tags)
.SetClass("question-text"); .SetClass("question-text");
const inputElement = TagRenderingQuestion.GenerateInputElement(configuration, applicableUnit, tags) const inputElement: InputElement<TagsFilter> = TagRenderingQuestion.GenerateInputElement(configuration, applicableUnit, tags)
const save = () => { const save = () => {
const selection = inputElement.GetValue().data; const selection = inputElement.GetValue().data;
console.log("Save button clicked, the tags are is", selection) 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); .addTag(tags.data.id, selection, tags);
} }
if (afterSave) { if (options.afterSave) {
afterSave(); options.afterSave();
} }
} }
if (options.saveButtonConstr === undefined) {
options.saveButtonConstr = v => new SaveButton(v,
State.state?.osmConnection)
.onClick(save)
}
const saveButton = new SaveButton(inputElement.GetValue(), const saveButton = options.saveButtonConstr(inputElement.GetValue())
State.state?.osmConnection)
.onClick(save)
let bottomTags: BaseUIElement;
const appliedTags = new VariableUiElement( if (options.bottomText !== undefined) {
inputElement.GetValue().map( bottomTags = options.bottomText(inputElement.GetValue())
(tagsFilter: TagsFilter) => { } else {
const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000; bottomTags = new VariableUiElement(
if (csCount < Constants.userJourney.tagsVisibleAt) { inputElement.GetValue().map(
return ""; (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"); ).SetClass("block break-all")
} }
if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { super([
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 ([
question, question,
inputElement, inputElement,
cancelButton, options.cancelButton,
saveButton, saveButton,
appliedTags] bottomTags]
) )
this .SetClass("question") this.SetClass("question")
} }
private static GenerateInputElement(configuration: TagRenderingConfig, applicableUnit: Unit, tagsSource: UIEventSource< any>): InputElement<TagsFilter> { private static GenerateInputElement(configuration: TagRenderingConfig, applicableUnit: Unit, tagsSource: UIEventSource<any>): InputElement<TagsFilter> {
let inputEls: InputElement<TagsFilter>[]; let inputEls: InputElement<TagsFilter>[];
const mappings = (configuration.mappings ?? []) const mappings = (configuration.mappings ?? [])
@ -105,7 +115,7 @@ export default class TagRenderingQuestion extends Combine {
return false; return false;
} }
return !(typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(tagsSource.data)); return !(typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(tagsSource.data));
}) })
@ -255,10 +265,10 @@ export default class TagRenderingQuestion extends Combine {
private static GenerateMappingElement( private static GenerateMappingElement(
tagsSource: UIEventSource<any>, tagsSource: UIEventSource<any>,
mapping: { mapping: {
if: TagsFilter, if: TagsFilter,
then: Translation, then: Translation,
hideInAnswer: boolean | TagsFilter hideInAnswer: boolean | TagsFilter
}, ifNot?: TagsFilter[]): InputElement<TagsFilter> { }, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
let tagging = mapping.if; let tagging = mapping.if;
if (ifNot.length > 0) { if (ifNot.length > 0) {

View file

@ -30,6 +30,9 @@ export default class Translations {
console.error(msg, t); console.error(msg, t);
throw msg throw msg
} }
if(t instanceof Translation){
return t;
}
return new Translation(t, context); return new Translation(t, context);
} }

View file

@ -29,17 +29,26 @@
}, },
"delete": { "delete": {
"delete": "Delete", "delete": "Delete",
"cancel": "Cancel",
"isDeleted": "This feature is deleted",
"loginToDelete": "You must be logged in to delete a point", "loginToDelete": "You must be logged in to delete a point",
"safeDelete": "This point can be safely deleted", "safeDelete": "This point can be safely deleted.",
"isntAPoint": "Only points can be 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", "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", "notEnoughExperience": "This point was made by someone else.",
"partOfOthers": "This point is part of some way or relation, so you can not delete it", "useSomethingElse": "Use another OpenStreetMap-editor to delete it instead",
"loading": "Inspecting properties to check if this feature can be deleted", "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": { "reasons": {
"test": "This was a testing point - the feature was never actually there", "test": "This was a testing point - the feature was never actually there",
"disused": "This feature is disused or removed", "disused": "This feature is disused or removed",
"notFound": "This feature couldn't be found" "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. <span class='subtle'>{reason}</span>"
} }
}, },
"general": { "general": {

21
test.ts
View file

@ -1,7 +1,12 @@
import {OsmObject} from "./Logic/Osm/OsmObject"; 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 Combine from "./UI/Base/Combine";
import State from "./State"; 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 ValidatedTextField from "./UI/Input/ValidatedTextField";
import Combine from "./UI/Base/Combine"; import Combine from "./UI/Base/Combine";
import {VariableUiElement} from "./UI/Base/VariableUIElement"; import {VariableUiElement} from "./UI/Base/VariableUIElement";
@ -143,7 +148,19 @@ function TestMiniMap() {
featureSource.ping() featureSource.ping()
} }
//*/ //*/
QueryParameters.GetQueryParameter("test", "true").setData("true")
State.state= new State(undefined) State.state= new State(undefined)
const id = "node/5414688303"
State.state.allElements.addElementById(id, new UIEventSource<any>({id: id}))
new Combine([ 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") ]).AttachTo("maindiv")