2021-07-01 02:26:45 +02:00
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle"
import Translations from "../i18n/Translations"
import Svg from "../../Svg"
2021-07-15 20:47:28 +02:00
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"
2022-06-05 02:24:14 +02:00
import { Store , UIEventSource } from "../../Logic/UIEventSource"
2021-07-01 02:26:45 +02:00
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton"
import { Translation } from "../i18n/Translation"
import BaseUIElement from "../BaseUIElement"
2021-07-03 14:35:44 +02:00
import Constants from "../../Models/Constants"
2021-08-07 23:11:34 +02:00
import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"
2021-10-04 03:12:42 +02:00
import { OsmObject } from "../../Logic/Osm/OsmObject"
2022-01-19 20:34:04 +01:00
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
2022-05-01 04:17:40 +02:00
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"
2021-07-01 02:26:45 +02:00
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
2022-01-19 20:34:04 +01:00
* @param state : the state of the application
2021-07-01 02:26:45 +02:00
* @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
* /
2022-05-01 04:17:40 +02:00
constructor ( id : string , state : FeaturePipelineState , options : DeleteConfig ) {
2022-01-19 20:34:04 +01:00
const deleteAbility = new DeleteabilityChecker ( id , state , options . neededChangesets )
const tagsSource = state . allElements . getEventSourceById ( id )
2021-07-01 02:26:45 +02:00
2021-10-04 03:12:42 +02:00
const isDeleted = new UIEventSource ( false )
2021-07-03 14:35:44 +02:00
const allowSoftDeletion = ! ! options . softDeletionTags
2021-07-01 02:26:45 +02:00
const confirm = new UIEventSource < boolean > ( false )
2022-05-01 04:17:40 +02:00
/ * *
* This function is the actual delete function
* /
function doDelete ( selected : { deleteReason : string } | { retagTo : TagsFilter } ) {
let actionToTake : OsmChangeAction
if ( selected [ "retagTo" ] !== undefined ) {
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
actionToTake = new ChangeTagAction ( id , selected [ "retagTo" ] , tagsSource . data , {
theme : state?.layoutToUse?.id ? ? "unkown" ,
changeType : "special-delete" ,
} )
} else {
actionToTake = new DeleteAction (
id ,
options . softDeletionTags ,
{
theme : state?.layoutToUse?.id ? ? "unkown" ,
specialMotivation : selected [ "deleteReason" ] ,
} ,
deleteAbility . canBeDeleted . data . canBeDeleted
)
2021-07-01 02:26:45 +02:00
}
2022-05-01 04:17:40 +02:00
state . changes ? . applyAction ( actionToTake )
2021-10-04 03:12:42 +02:00
isDeleted . setData ( true )
2021-07-01 02:26:45 +02:00
}
const t = Translations . t . delete
2022-05-01 04:17:40 +02:00
const cancelButton = t . cancel
. SetClass ( "block btn btn-secondary" )
. onClick ( ( ) = > confirm . setData ( false ) )
2021-07-01 02:26:45 +02:00
/ * *
* The button which is shown first . Opening it will trigger the check for deletions
* /
2021-07-03 14:35:44 +02:00
const deleteButton = new SubtleButton (
2022-05-01 04:17:40 +02:00
Svg . delete_icon_svg ( ) . SetStyle ( "width: 1.5rem; height: 1.5rem;" ) ,
t . delete
) . onClick ( ( ) = > {
deleteAbility . CheckDeleteability ( true )
confirm . setData ( true )
} )
2022-06-05 02:24:14 +02:00
const isShown : Store < boolean > = tagsSource . map ( ( tgs ) = > tgs . id . indexOf ( "-" ) < 0 )
2022-05-01 04:17:40 +02:00
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 )
2022-09-08 21:40:48 +02:00
) ,
2022-05-01 04:17:40 +02:00
] ) . SetClass ( "flex justify-end flex-wrap-reverse" ) ,
] ) . SetClass ( "flex mt-2 justify-between" ) ,
] ) . SetClass ( "question" )
2021-07-01 02:26:45 +02:00
super (
2021-07-03 14:35:44 +02:00
new Toggle (
2021-09-09 00:05:51 +02:00
new Combine ( [
Svg . delete_icon_svg ( ) . SetClass (
"h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full"
) ,
2022-05-01 04:17:40 +02:00
t . isDeleted ,
] ) . SetClass ( "flex m-2 rounded-full" ) ,
2021-07-01 02:26:45 +02:00
new Toggle (
new Toggle (
2021-07-03 14:35:44 +02:00
new Toggle (
2021-09-09 00:05:51 +02:00
new Toggle (
2022-05-01 04:17:40 +02:00
deleteDialog ,
new SubtleButton ( Svg . envelope_ui ( ) , t . readMessages ) ,
2022-01-19 20:34:04 +01:00
state . osmConnection . userDetails . map (
( ud ) = >
ud . csCount >
Constants . userJourney
. addNewPointWithUnreadMessagesUnlock ||
ud . unreadMessages == 0
2022-09-08 21:40:48 +02:00
)
2021-09-09 00:05:51 +02:00
) ,
deleteButton ,
confirm
) ,
2021-10-14 18:29:39 +02:00
new VariableUiElement (
deleteAbility . canBeDeleted . map ( ( cbd ) = >
new Combine ( [
Svg . delete_not_allowed_svg ( )
. SetStyle ( "height: 2rem; width: auto" )
. SetClass ( "mr-2" ) ,
new Combine ( [
2022-05-01 04:17:40 +02:00
t . cannotBeDeleted ,
cbd . reason . SetClass ( "subtle" ) ,
t . useSomethingElse . SetClass ( "subtle" ) ,
] ) . SetClass ( "flex flex-col" ) ,
2021-10-14 18:29:39 +02:00
] ) . SetClass ( "flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200" )
2022-09-08 21:40:48 +02:00
)
2021-10-04 03:12:42 +02:00
) ,
2022-09-08 21:40:48 +02:00
2021-10-04 03:12:42 +02:00
deleteAbility . canBeDeleted . map (
( cbd ) = > allowSoftDeletion || cbd . canBeDeleted !== false
2022-09-08 21:40:48 +02:00
)
) ,
2021-09-09 00:05:51 +02:00
2022-05-01 04:17:40 +02:00
t . loginToDelete . onClick ( state . osmConnection . AttemptLogin ) ,
2022-01-19 20:34:04 +01:00
state . osmConnection . isLoggedIn
2021-09-09 00:05:51 +02:00
) ,
2021-10-04 03:12:42 +02:00
isDeleted
) ,
2021-07-03 14:35:44 +02:00
undefined ,
isShown
)
2021-07-01 02:26:45 +02:00
}
2022-05-01 04:17:40 +02:00
private static constructConfirmButton (
deleteReasons : UIEventSource < any | undefined >
) : BaseUIElement {
2021-07-01 02:26:45 +02:00
const t = Translations . t . delete
const btn = new Combine ( [
Svg . delete_icon_ui ( ) . SetClass ( "w-6 h-6 mr-3 block" ) ,
2022-05-01 04:17:40 +02:00
t . delete ,
2021-07-01 02:26:45 +02:00
] ) . SetClass ( "flex btn bg-red-500" )
const btnNonActive = new Combine ( [
Svg . delete_icon_ui ( ) . SetClass ( "w-6 h-6 mr-3 block" ) ,
2022-05-01 04:17:40 +02:00
t . delete ,
2021-07-01 02:26:45 +02:00
] ) . SetClass ( "flex btn btn-disabled bg-red-200" )
return new Toggle (
btn ,
btnNonActive ,
deleteReasons . map ( ( reason ) = > reason !== undefined )
)
}
2022-05-01 04:17:40 +02:00
private static constructExplanation (
selectedOption : UIEventSource < { deleteReason : string } | { retagTo : TagsFilter } > ,
deleteAction : DeleteabilityChecker ,
currentTags : UIEventSource < object > ,
state ? : { osmConnection? : OsmConnection }
2022-09-08 21:40:48 +02:00
) {
2021-07-01 02:26:45 +02:00
const t = Translations . t . delete
2022-05-01 04:17:40 +02:00
return new VariableUiElement (
selectedOption . map (
( selectedOption ) = > {
if ( selectedOption === undefined ) {
return t . explanations . selectReason . SetClass ( "subtle" )
2022-09-08 21:40:48 +02:00
}
2021-07-01 02:26:45 +02:00
2022-05-01 04:17:40 +02:00
const retag : TagsFilter | undefined = selectedOption [ "retagTo" ]
if ( retag !== undefined ) {
// This is a retagging, not a deletion of any kind
return new Combine ( [
t . explanations . retagNoOtherThemes ,
TagRenderingQuestion . CreateTagExplanation (
new UIEventSource < TagsFilter > ( retag ) ,
currentTags ,
state
) . SetClass ( "subtle" ) ,
] )
2021-07-01 02:26:45 +02:00
}
2022-05-01 04:17:40 +02:00
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 } )
} )
2022-09-08 21:40:48 +02:00
)
2022-05-01 04:17:40 +02:00
}
2021-07-01 02:26:45 +02:00
} ,
[ deleteAction . canBeDeleted ]
2022-09-08 21:40:48 +02:00
)
2021-07-01 02:26:45 +02:00
) . SetClass ( "block" )
}
2022-08-20 12:46:33 +02:00
private static constructMultipleChoice (
config : DeleteConfig ,
tagsSource : UIEventSource < Record < string , string > > ,
state : FeaturePipelineState
2022-05-01 04:17:40 +02:00
) : InputElement < { deleteReason : string } | { retagTo : TagsFilter } > {
const elements : InputElement < { deleteReason : string } | { retagTo : TagsFilter } > [ ] = [ ]
2021-07-01 02:26:45 +02:00
2022-05-01 04:17:40 +02:00
for ( const nonDeleteOption of config . nonDeleteMappings ) {
elements . push (
new FixedInputElement (
new SubstitutedTranslation ( nonDeleteOption . then , tagsSource , state ) ,
{
retagTo : nonDeleteOption.if ,
}
2022-09-08 21:40:48 +02:00
)
2022-05-01 04:17:40 +02:00
)
2021-07-03 14:35:44 +02:00
}
2021-07-01 02:26:45 +02:00
2022-05-01 04:17:40 +02:00
for ( const extraDeleteReason of config . extraDeleteReasons ? ? [ ] ) {
elements . push (
new FixedInputElement (
new SubstitutedTranslation ( extraDeleteReason . explanation , tagsSource , state ) ,
{
deleteReason : extraDeleteReason.changesetMessage ,
}
2022-09-08 21:40:48 +02:00
)
2022-05-01 04:17:40 +02:00
)
}
2021-07-01 02:26:45 +02:00
2022-05-01 04:17:40 +02:00
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 ,
}
2022-09-08 21:40:48 +02:00
)
2022-05-01 04:17:40 +02:00
)
}
2021-07-01 02:26:45 +02:00
2022-05-01 04:17:40 +02:00
return new RadioButton ( elements , { selectFirstAsDefault : false } )
2021-07-01 02:26:45 +02:00
}
2021-10-04 03:12:42 +02:00
}
class DeleteabilityChecker {
public readonly canBeDeleted : UIEventSource < { canBeDeleted? : boolean ; reason : Translation } >
private readonly _id : string
private readonly _allowDeletionAtChangesetCount : number
2022-01-19 20:34:04 +01:00
private readonly _state : {
osmConnection : OsmConnection
}
2021-10-04 03:12:42 +02:00
constructor (
id : string ,
2022-01-26 21:40:38 +01:00
state : { osmConnection : OsmConnection } ,
2021-10-04 03:12:42 +02:00
allowDeletionAtChangesetCount? : number
) {
this . _id = id
2022-01-19 20:34:04 +01:00
this . _state = state
2021-10-04 03:12:42 +02:00
this . _allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ? ? Number . MAX_VALUE
this . canBeDeleted = new UIEventSource < { canBeDeleted? : boolean ; reason : Translation } > ( {
canBeDeleted : undefined ,
reason : Translations.t.delete.loading ,
} )
this . CheckDeleteability ( false )
}
/ * *
* Checks if the currently logged in user can delete the current point .
* State is written into this . _canBeDeleted
* @constructor
* @private
* /
public CheckDeleteability ( useTheInternet : boolean ) : void {
const t = Translations . t . delete
const id = this . _id
const state = this . canBeDeleted
2022-01-26 21:40:38 +01:00
const self = this
2021-10-04 03:12:42 +02:00
if ( ! id . startsWith ( "node" ) ) {
this . canBeDeleted . setData ( {
canBeDeleted : false ,
reason : t.isntAPoint ,
} )
return
}
// Does the currently logged in user have enough experience to delete this point?
2022-01-19 20:34:04 +01:00
const deletingPointsOfOtherAllowed = this . _state . osmConnection . userDetails . map ( ( ud ) = > {
2021-10-04 03:12:42 +02:00
if ( ud === undefined ) {
return undefined
}
if ( ! ud . loggedIn ) {
return false
}
return (
ud . csCount >=
Math . min (
Constants . userJourney . deletePointsOfOthersUnlock ,
this . _allowDeletionAtChangesetCount
2022-09-08 21:40:48 +02:00
)
)
2021-10-04 03:12:42 +02:00
} )
const previousEditors = new UIEventSource < number [ ] > ( undefined )
const allByMyself = previousEditors . map (
( previous ) = > {
if ( previous === null || previous === undefined ) {
// Not yet downloaded
return null
}
2022-01-19 20:34:04 +01:00
const userId = self . _state . osmConnection . userDetails . data . uid
2021-10-04 03:12:42 +02:00
return ! previous . some ( ( editor ) = > editor !== userId )
2022-01-19 20:34:04 +01:00
} ,
[ self . _state . osmConnection . userDetails ]
)
2021-10-04 03:12:42 +02:00
// User allowed OR only edited by self?
const deletetionAllowed = deletingPointsOfOtherAllowed . map (
( isAllowed ) = > {
if ( isAllowed === undefined ) {
// No logged in user => definitively not allowed to delete!
return false
}
if ( isAllowed === true ) {
return true
}
// 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!
2022-09-08 21:40:48 +02:00
2021-10-04 03:12:42 +02:00
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
2022-09-02 13:43:14 +02:00
const hist = OsmObject . DownloadHistory ( id ) . map ( ( versions ) = >
versions . map ( ( version ) = >
Number ( version . tags [ "_last_edit:contributor:uid" ] )
2022-09-08 21:40:48 +02:00
)
)
2022-06-05 02:24:14 +02:00
hist . addCallbackAndRunD ( ( hist ) = > previousEditors . setData ( hist ) )
2022-09-08 21:40:48 +02:00
}
2021-10-04 03:12:42 +02:00
if ( allByMyself . data === true ) {
// Yay! We can download!
return true
2022-09-08 21:40:48 +02:00
}
2021-10-04 03:12:42 +02:00
if ( allByMyself . data === false ) {
// Nope, downloading not allowed...
return false
}
2021-07-01 02:26:45 +02:00
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
2022-05-01 04:17:40 +02:00
return undefined
2022-09-08 21:40:48 +02:00
} ,
2021-10-04 03:12:42 +02:00
[ allByMyself ]
2022-09-08 21:40:48 +02:00
)
2021-10-04 03:12:42 +02:00
const hasRelations : UIEventSource < boolean > = new UIEventSource < boolean > ( null )
const hasWays : UIEventSource < boolean > = new UIEventSource < boolean > ( null )
deletetionAllowed . addCallbackAndRunD ( ( deletetionAllowed ) = > {
if ( deletetionAllowed === false ) {
// Nope, we are not allowed to delete
state . setData ( {
canBeDeleted : false ,
reason : t.notEnoughExperience ,
} )
return true // unregister this caller!
}
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 ) . then ( ( rels ) = > {
hasRelations . setData ( rels . length > 0 )
} )
OsmObject . DownloadReferencingWays ( id ) . then ( ( ways ) = > {
hasWays . setData ( ways . length > 0 )
} )
return true // unregister to only run once
} )
const hasWaysOrRelations = hasRelations . map (
( hasRelationsData ) = > {
if ( hasRelationsData === true ) {
return true
}
if ( hasWays . data === true ) {
return true
}
if ( hasWays . data === null || hasRelationsData === null ) {
return null
2022-09-08 21:40:48 +02:00
}
2021-10-04 03:12:42 +02:00
if ( hasWays . data === false && hasRelationsData === false ) {
return false
}
return null
2022-09-08 21:40:48 +02:00
} ,
[ hasWays ]
2021-10-04 03:12:42 +02:00
)
hasWaysOrRelations . addCallbackAndRun ( ( waysOrRelations ) = > {
if ( waysOrRelations == null ) {
// Not yet loaded - we still wait a little bit
2022-09-08 21:40:48 +02:00
return
}
2021-10-04 03:12:42 +02:00
if ( waysOrRelations ) {
// not deleteble by mapcomplete
state . setData ( {
canBeDeleted : false ,
reason : t.partOfOthers ,
2022-09-08 21:40:48 +02:00
} )
} else {
2021-10-04 03:12:42 +02:00
// alright, this point can be safely deleted!
state . setData ( {
canBeDeleted : true ,
reason : allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete ,
2022-09-08 21:40:48 +02:00
} )
}
} )
2021-10-04 03:12:42 +02:00
}
2021-07-01 02:26:45 +02:00
}