forked from MapComplete/MapComplete
First version of a delete button, is working
This commit is contained in:
parent
de5f8f95bb
commit
e4c29ce660
13 changed files with 309 additions and 136 deletions
|
@ -17,7 +17,8 @@ import {AndOrTagConfigJson} from "../../Customizations/JSON/TagConfigJson";
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Changes} from "../../Logic/Osm/Changes";
|
||||
import {And} from "../../Logic/Tags/And";
|
||||
|
||||
import Constants from "../../Models/Constants";
|
||||
import DeleteConfig from "../../Customizations/JSON/DeleteConfig";
|
||||
|
||||
export default class DeleteWizard extends Toggle {
|
||||
/**
|
||||
|
@ -39,18 +40,12 @@ 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,
|
||||
options?: {
|
||||
noDeleteOptions?: { if: Tag[], then: Translation }[]
|
||||
softDeletionTags?: Tag[],
|
||||
neededChangesets?: number
|
||||
}) {
|
||||
options: DeleteConfig) {
|
||||
|
||||
options = options ?? {}
|
||||
const deleteAction = new DeleteAction(id, options.neededChangesets);
|
||||
const tagsSource = State.state.allElements.getEventSourceById(id)
|
||||
|
||||
let softDeletionTags = options.softDeletionTags ?? []
|
||||
const allowSoftDeletion = softDeletionTags.length > 0
|
||||
const allowSoftDeletion = !!options.softDeletionTags
|
||||
|
||||
const confirm = new UIEventSource<boolean>(false)
|
||||
|
||||
|
@ -80,7 +75,7 @@ export default class DeleteWizard extends Toggle {
|
|||
});
|
||||
return
|
||||
} else {
|
||||
// This is an injected tagging
|
||||
// This is a 'non-delete'-option that was selected
|
||||
softDelete(undefined, tgs)
|
||||
}
|
||||
|
||||
|
@ -89,46 +84,60 @@ export default class DeleteWizard extends Toggle {
|
|||
|
||||
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)
|
||||
}
|
||||
)
|
||||
const question = new VariableUiElement(tagsSource.map(currentTags => {
|
||||
const config = DeleteWizard.generateDeleteTagRenderingConfig(options.softDeletionTags, options.nonDeleteMappings, options.extraDeleteReasons, currentTags)
|
||||
return 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(
|
||||
const deleteButton = new SubtleButton(
|
||||
Svg.delete_icon_ui().SetStyle("width: 2rem; height: 2rem;"), t.delete.Clone()).onClick(
|
||||
() => {
|
||||
deleteAction.CheckDeleteability(true)
|
||||
confirm.setData(true);
|
||||
}
|
||||
);
|
||||
).SetClass("w-1/2 float-right");
|
||||
|
||||
const isShown = new UIEventSource<boolean>(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"),
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
new Toggle(
|
||||
question,
|
||||
new Toggle(
|
||||
question,
|
||||
new SubtleButton(Svg.envelope_ui(), t.readMessages.Clone()),
|
||||
State.state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0)
|
||||
),
|
||||
|
||||
deleteButton,
|
||||
confirm),
|
||||
new VariableUiElement(deleteAction.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse]))),
|
||||
new VariableUiElement(deleteAction.canBeDeleted.map(cbd => new Combine([cbd.reason.Clone(), t.useSomethingElse.Clone()]))),
|
||||
deleteAction.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)),
|
||||
|
||||
t.loginToDelete.Clone().onClick(State.state.osmConnection.AttemptLogin),
|
||||
State.state.osmConnection.isLoggedIn
|
||||
),
|
||||
deleteAction.isDeleted)
|
||||
deleteAction.isDeleted),
|
||||
undefined,
|
||||
isShown)
|
||||
|
||||
}
|
||||
|
||||
|
@ -163,7 +172,7 @@ export default class DeleteWizard extends Toggle {
|
|||
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) {
|
||||
|
@ -179,23 +188,32 @@ export default class DeleteWizard extends Toggle {
|
|||
)).SetClass("block")
|
||||
}
|
||||
|
||||
private static generateDeleteTagRenderingConfig(softDeletionTags: Tag[], nonDeleteOptions: {
|
||||
if: Tag[],
|
||||
then: Translation
|
||||
}[]) {
|
||||
private static generateDeleteTagRenderingConfig(softDeletionTags: TagsFilter,
|
||||
nonDeleteOptions: { if: TagsFilter; then: Translation }[],
|
||||
extraDeleteReasons: { explanation: Translation; changesetMessage: string }[],
|
||||
currentTags: any) {
|
||||
const t = Translations.t.delete
|
||||
nonDeleteOptions = nonDeleteOptions ?? []
|
||||
const softDeletionTagsStr = (softDeletionTags ?? []).map(t => t.asHumanString(false, false))
|
||||
const nonDeleteOptionsStr: { if: AndOrTagConfigJson, then: any }[] = []
|
||||
let softDeletionTagsStr = []
|
||||
if (softDeletionTags !== undefined) {
|
||||
softDeletionTags.asChange(currentTags)
|
||||
}
|
||||
const extraOptionsStr: { if: AndOrTagConfigJson, then: any }[] = []
|
||||
for (const nonDeleteOption of nonDeleteOptions) {
|
||||
const newIf: string[] = nonDeleteOption.if.map(tag => tag.asHumanString())
|
||||
const newIf: string[] = nonDeleteOption.if.asChange({}).map(kv => kv.k + "=" + kv.v)
|
||||
|
||||
nonDeleteOptionsStr.push({
|
||||
extraOptionsStr.push({
|
||||
if: {and: newIf},
|
||||
then: nonDeleteOption.then
|
||||
})
|
||||
}
|
||||
|
||||
for (const extraDeleteReason of (extraDeleteReasons ?? [])) {
|
||||
extraOptionsStr.push({
|
||||
if: {and: ["_delete_reason=" + extraDeleteReason.changesetMessage]},
|
||||
then: extraDeleteReason.explanation
|
||||
})
|
||||
}
|
||||
return new TagRenderingConfig(
|
||||
{
|
||||
question: t.whyDelete,
|
||||
|
@ -206,7 +224,7 @@ export default class DeleteWizard extends Toggle {
|
|||
},
|
||||
mappings: [
|
||||
|
||||
...nonDeleteOptionsStr,
|
||||
...extraOptionsStr,
|
||||
|
||||
{
|
||||
if: {
|
||||
|
|
|
@ -12,6 +12,7 @@ import Constants from "../../Models/Constants";
|
|||
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import DeleteWizard from "./DeleteWizard";
|
||||
|
||||
export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||
|
||||
|
@ -21,7 +22,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
) {
|
||||
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig),
|
||||
() => FeatureInfoBox.GenerateContent(tags, layerConfig),
|
||||
tags.data.id);
|
||||
undefined);
|
||||
|
||||
if (layerConfig === undefined) {
|
||||
throw "Undefined layerconfig";
|
||||
|
@ -69,19 +70,31 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
if (!hasMinimap) {
|
||||
renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap")))
|
||||
}
|
||||
|
||||
|
||||
if (layerConfig.deletion) {
|
||||
renderings.push(
|
||||
new VariableUiElement(tags.map(tags => tags.id).map(id =>
|
||||
new DeleteWizard(
|
||||
id,
|
||||
layerConfig.deletion
|
||||
))
|
||||
))
|
||||
}
|
||||
|
||||
renderings.push(
|
||||
new VariableUiElement(
|
||||
State.state.osmConnection.userDetails.map(userdetails => {
|
||||
if (userdetails.csCount <= Constants.userJourney.historyLinkVisible
|
||||
&& State.state.featureSwitchIsDebugging.data == false
|
||||
&& State.state.featureSwitchIsTesting.data === false) {
|
||||
return undefined
|
||||
}
|
||||
State.state.osmConnection.userDetails
|
||||
.map(ud => ud.csCount)
|
||||
.map(csCount => {
|
||||
if (csCount <= Constants.userJourney.historyLinkVisible
|
||||
&& State.state.featureSwitchIsDebugging.data == false
|
||||
&& State.state.featureSwitchIsTesting.data === false) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("last_edit"));
|
||||
return new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("last_edit"));
|
||||
|
||||
}, [State.state.featureSwitchIsDebugging])
|
||||
}, [State.state.featureSwitchIsDebugging, State.state.featureSwitchIsTesting])
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class ShowDataLayer {
|
|||
const allFeats = features.data.map(ff => ff.feature);
|
||||
geoLayer = self.CreateGeojsonLayer();
|
||||
for (const feat of allFeats) {
|
||||
if(feat === undefined){
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
// @ts-ignore
|
||||
|
@ -79,11 +79,11 @@ export default class ShowDataLayer {
|
|||
}
|
||||
|
||||
if (zoomToFeatures) {
|
||||
try{
|
||||
|
||||
mp.fitBounds(geoLayer.getBounds())
|
||||
try {
|
||||
|
||||
}catch(e){
|
||||
mp.fitBounds(geoLayer.getBounds())
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,6 @@ export default class ShowDataLayer {
|
|||
})
|
||||
});
|
||||
}
|
||||
|
||||
private postProcessFeature(feature, leafletLayer: L.Layer) {
|
||||
const layer: LayerConfig = this._layerDict[feature._matching_layer_id];
|
||||
if (layer === undefined) {
|
||||
|
@ -161,6 +160,7 @@ export default class ShowDataLayer {
|
|||
|
||||
leafletLayer.on("popupopen", () => {
|
||||
State.state.selectedElement.setData(feature)
|
||||
|
||||
if (infobox === undefined) {
|
||||
const tags = State.state.allElements.getEventSourceById(feature.properties.id);
|
||||
infobox = new FeatureInfoBox(tags, layer);
|
||||
|
@ -175,11 +175,11 @@ export default class ShowDataLayer {
|
|||
|
||||
|
||||
infobox.AttachTo(id)
|
||||
infobox.Activate();
|
||||
infobox.Activate();
|
||||
});
|
||||
const self = this;
|
||||
State.state.selectedElement.addCallbackAndRunD(selected => {
|
||||
if ( self._leafletMap.data === undefined) {
|
||||
if (self._leafletMap.data === undefined) {
|
||||
return;
|
||||
}
|
||||
if (leafletLayer.getPopup().isOpen()) {
|
||||
|
@ -187,8 +187,10 @@ export default class ShowDataLayer {
|
|||
}
|
||||
if (selected.properties.id === feature.properties.id) {
|
||||
// A small sanity check to prevent infinite loops:
|
||||
// If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
|
||||
if (selected.geometry.type === feature.geometry.type) {
|
||||
if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
|
||||
|
||||
&& feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
|
||||
) {
|
||||
leafletLayer.openPopup()
|
||||
}
|
||||
|
||||
|
|
|
@ -24,8 +24,6 @@ import Histogram from "./BigComponents/Histogram";
|
|||
import Loc from "../Models/Loc";
|
||||
import {Utils} from "../Utils";
|
||||
import BaseLayer from "../Models/BaseLayer";
|
||||
import DeleteWizard from "./Popup/DeleteWizard";
|
||||
import Constants from "../Models/Constants";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -38,8 +36,6 @@ export interface SpecialVisualization {
|
|||
export default class SpecialVisualizations {
|
||||
|
||||
|
||||
public static specialVisualisationsByName: Map<string, SpecialVisualization> = SpecialVisualizations.byName();
|
||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||
static constructMiniMap: (options?: {
|
||||
background?: UIEventSource<BaseLayer>,
|
||||
location?: UIEventSource<Loc>,
|
||||
|
@ -380,57 +376,10 @@ export default class SpecialVisualizations {
|
|||
[state.layoutToUse])
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
funcName: "delete",
|
||||
docs: `Offers a dialog to (soft) delete the point. The dialog is built to be user friendly and to prevent mistakes. If deletion is not possible, the dialog will hide itself.
|
||||
|
||||
#### Hard deletion if enough experience
|
||||
|
||||
A feature can only be deleted by mapcomplete if:
|
||||
|
||||
- It is a node
|
||||
- No ways or relations use the node
|
||||
- The logged-in user has enough experience (at least ${Constants.userJourney.deletePointsOfOthersUnlock} changesets) OR the user is the only one to have edited the point previously
|
||||
- The user did not select one of the 'non-delete-options' (see below)
|
||||
|
||||
In all other cases, a 'soft deletion' is used.
|
||||
|
||||
#### Soft deletion
|
||||
|
||||
A 'soft deletion' is when the point isn't deleted from OSM but retagged so that it'll won't how up in the mapcomplete theme anymore.
|
||||
This makes it look like it was deleted, without doing damage. A fixme will be added to the point.
|
||||
|
||||
Note that a soft deletion is _only_ possible if these tags are provided by the theme creator, as they'll be different for every theme
|
||||
|
||||
#### No-delete options
|
||||
|
||||
In some cases, the contributor might want to delete something for the wrong reason (e.g. someone who wants to have a path removed "because the path is on their private property").
|
||||
However, the path exists in reality and should thus be on OSM - otherwise the next contributor will pass by and notice "hey, there is a path missing here! Let me redraw it in OSM!)
|
||||
|
||||
The correct approach is to retag the feature in such a way that it is semantically correct *and* that it doesn't show up on the theme anymore.
|
||||
A no-delete option is offered as 'reason to delete it', but secretly retags.
|
||||
|
||||
`,
|
||||
args: [],
|
||||
constr: (state, tagSource, args) => {
|
||||
return new VariableUiElement(tagSource.map(tags => tags.id).map(id =>
|
||||
new DeleteWizard(id)))
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
private static byName(): Map<string, SpecialVisualization> {
|
||||
const result = new Map<string, SpecialVisualization>();
|
||||
|
||||
for (const specialVisualization of SpecialVisualizations.specialVisualizations) {
|
||||
result.set(specialVisualization.funcName, specialVisualization)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||
private static GenHelpMessage() {
|
||||
|
||||
const helpTexts =
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue