forked from MapComplete/MapComplete
Refactoring: use svelte for delete flow as well
This commit is contained in:
parent
15a481f864
commit
effcb8ca23
9 changed files with 429 additions and 520 deletions
|
@ -3,8 +3,14 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
||||||
import { DeleteConfigJson } from "./Json/DeleteConfigJson"
|
import { DeleteConfigJson } from "./Json/DeleteConfigJson"
|
||||||
import Translations from "../../UI/i18n/Translations"
|
import Translations from "../../UI/i18n/Translations"
|
||||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||||
|
import TagRenderingEditable from "../../UI/Popup/TagRendering/TagRenderingEditable.svelte";
|
||||||
|
import TagRenderingConfig from "./TagRenderingConfig";
|
||||||
|
import {QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
|
||||||
|
import {TagConfigJson} from "./Json/TagConfigJson";
|
||||||
|
import {del} from "idb-keyval";
|
||||||
|
|
||||||
export default class DeleteConfig {
|
export default class DeleteConfig {
|
||||||
|
public static readonly deleteReasonKey = "_delete_reason"
|
||||||
private static readonly defaultDeleteReasons: {
|
private static readonly defaultDeleteReasons: {
|
||||||
changesetMessage: string
|
changesetMessage: string
|
||||||
explanation: Translation
|
explanation: Translation
|
||||||
|
@ -32,7 +38,7 @@ export default class DeleteConfig {
|
||||||
changesetMessage: string
|
changesetMessage: string
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
public readonly nonDeleteMappings?: { if: TagsFilter; then: TypedTranslation<object> }[]
|
private readonly nonDeleteMappings?: { if: TagConfigJson; then: Translation }[]
|
||||||
|
|
||||||
public readonly softDeletionTags?: TagsFilter
|
public readonly softDeletionTags?: TagsFilter
|
||||||
public readonly neededChangesets?: number
|
public readonly neededChangesets?: number
|
||||||
|
@ -61,8 +67,9 @@ export default class DeleteConfig {
|
||||||
|
|
||||||
this.nonDeleteMappings = (json.nonDeleteMappings ?? []).map((nonDelete, i) => {
|
this.nonDeleteMappings = (json.nonDeleteMappings ?? []).map((nonDelete, i) => {
|
||||||
const ctx = `${context}.extraDeleteReasons[${i}]`
|
const ctx = `${context}.extraDeleteReasons[${i}]`
|
||||||
|
TagUtils.Tag(nonDelete.if, ctx + ".if") // for validation only
|
||||||
return {
|
return {
|
||||||
if: TagUtils.Tag(nonDelete.if, ctx + ".if"),
|
if: nonDelete.if,
|
||||||
then: Translations.T(nonDelete.then, ctx + ".then"),
|
then: Translations.T(nonDelete.then, ctx + ".then"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -88,4 +95,29 @@ export default class DeleteConfig {
|
||||||
}
|
}
|
||||||
this.neededChangesets = json.neededChangesets
|
this.neededChangesets = json.neededChangesets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public constructTagRendering(): TagRenderingConfig {
|
||||||
|
const t = Translations.t.delete
|
||||||
|
|
||||||
|
const mappings: {if: TagConfigJson, then: Record<string, string>} []= []
|
||||||
|
for (const nonDeleteMapping of this.nonDeleteMappings) {
|
||||||
|
mappings.push({
|
||||||
|
if: nonDeleteMapping.if,
|
||||||
|
then: nonDeleteMapping.then.translations
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deleteReason of this.deleteReasons) {
|
||||||
|
mappings.push({
|
||||||
|
if: DeleteConfig.deleteReasonKey+ "="+ deleteReason.changesetMessage,
|
||||||
|
then: deleteReason.explanation.translations
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: QuestionableTagRenderingConfigJson = {
|
||||||
|
question: t.whyDelete.translations,
|
||||||
|
mappings
|
||||||
|
}
|
||||||
|
return new TagRenderingConfig(config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export interface MappingConfigJson {
|
||||||
* Shown if the 'if is fulfilled
|
* Shown if the 'if is fulfilled
|
||||||
* Type: rendered
|
* Type: rendered
|
||||||
*/
|
*/
|
||||||
then: string | any
|
then: string | Record<string, string>
|
||||||
/**
|
/**
|
||||||
* An extra icon supporting the choice
|
* An extra icon supporting the choice
|
||||||
* Type: icon
|
* Type: icon
|
||||||
|
|
184
UI/Popup/DeleteFlow/DeleteFlowState.ts
Normal file
184
UI/Popup/DeleteFlow/DeleteFlowState.ts
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import {Translation} from "../../i18n/Translation";
|
||||||
|
import OsmObjectDownloader from "../../../Logic/Osm/OsmObjectDownloader";
|
||||||
|
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||||
|
import {OsmId} from "../../../Models/OsmFeature";
|
||||||
|
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||||
|
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||||
|
import Translations from "../../i18n/Translations";
|
||||||
|
import Constants from "../../../Models/Constants";
|
||||||
|
|
||||||
|
|
||||||
|
export class DeleteFlowState {
|
||||||
|
public readonly canBeDeleted: UIEventSource<boolean | undefined> = new UIEventSource<boolean | undefined>(undefined)
|
||||||
|
public readonly canBeDeletedReason: UIEventSource<Translation | undefined> = new UIEventSource<Translation>(undefined)
|
||||||
|
private readonly objectDownloader: OsmObjectDownloader
|
||||||
|
private readonly _id: OsmId
|
||||||
|
private readonly _allowDeletionAtChangesetCount: number
|
||||||
|
private readonly _osmConnection: OsmConnection
|
||||||
|
private readonly state: SpecialVisualizationState
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: OsmId,
|
||||||
|
state: SpecialVisualizationState,
|
||||||
|
allowDeletionAtChangesetCount?: number
|
||||||
|
) {
|
||||||
|
this.state = state
|
||||||
|
this.objectDownloader = state.osmObjectDownloader
|
||||||
|
this._id = id
|
||||||
|
this._osmConnection = state.osmConnection
|
||||||
|
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE
|
||||||
|
|
||||||
|
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 {
|
||||||
|
console.log("Checking deleteability (internet?", useTheInternet, ")")
|
||||||
|
const t = Translations.t.delete
|
||||||
|
const id = this._id
|
||||||
|
const self = this
|
||||||
|
if (!id.startsWith("node")) {
|
||||||
|
this.canBeDeleted.setData(false)
|
||||||
|
this.canBeDeletedReason.setData(t.isntAPoint)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the currently logged in user have enough experience to delete this point?
|
||||||
|
const deletingPointsOfOtherAllowed = this._osmConnection.userDetails.map((ud) => {
|
||||||
|
if (ud === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (!ud.loggedIn) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
ud.csCount >=
|
||||||
|
Math.min(
|
||||||
|
Constants.userJourney.deletePointsOfOthersUnlock,
|
||||||
|
this._allowDeletionAtChangesetCount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const previousEditors = new UIEventSource<number[]>(undefined)
|
||||||
|
const allByMyself = previousEditors.map(
|
||||||
|
(previous) => {
|
||||||
|
if (previous === null || previous === undefined) {
|
||||||
|
// Not yet downloaded
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const userId = self._osmConnection.userDetails.data.uid
|
||||||
|
return !previous.some((editor) => editor !== userId)
|
||||||
|
},
|
||||||
|
[self._osmConnection.userDetails]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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!
|
||||||
|
|
||||||
|
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
|
||||||
|
const hist = this.objectDownloader
|
||||||
|
.DownloadHistory(id)
|
||||||
|
.map((versions) =>
|
||||||
|
versions.map((version) =>
|
||||||
|
Number(version.tags["_last_edit:contributor:uid"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
hist.addCallbackAndRunD((hist) => previousEditors.setData(hist))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allByMyself.data === true) {
|
||||||
|
// Yay! We can download!
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (allByMyself.data === false) {
|
||||||
|
// Nope, downloading not allowed...
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
[allByMyself]
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
this.canBeDeleted.setData(false)
|
||||||
|
this.canBeDeletedReason.setData(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
|
||||||
|
this.objectDownloader.DownloadReferencingRelations(id).then((rels) => {
|
||||||
|
hasRelations.setData(rels.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.objectDownloader.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
|
||||||
|
}
|
||||||
|
if (hasWays.data === false && hasRelationsData === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
[hasWays]
|
||||||
|
)
|
||||||
|
|
||||||
|
hasWaysOrRelations.addCallbackAndRun((waysOrRelations) => {
|
||||||
|
if (waysOrRelations == null) {
|
||||||
|
// Not yet loaded - we still wait a little bit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (waysOrRelations) {
|
||||||
|
// not deleteable by mapcomplete
|
||||||
|
this.canBeDeleted.setData(false)
|
||||||
|
this.canBeDeletedReason.setData(t.partOfOthers)
|
||||||
|
} else {
|
||||||
|
// alright, this point can be safely deleted!
|
||||||
|
this.canBeDeleted.setData(true)
|
||||||
|
this.canBeDeletedReason.setData(allByMyself.data ? t.onlyEditedByLoggedInUser : t.safeDelete)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
155
UI/Popup/DeleteFlow/DeleteWizard.svelte
Normal file
155
UI/Popup/DeleteFlow/DeleteWizard.svelte
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||||
|
import type {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||||
|
import Translations from "../../i18n/Translations";
|
||||||
|
import Tr from "../../Base/Tr.svelte";
|
||||||
|
import {InformationCircleIcon, TrashIcon} from "@babeard/svelte-heroicons/mini";
|
||||||
|
import type {OsmId, OsmTags} from "../../../Models/OsmFeature";
|
||||||
|
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig";
|
||||||
|
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte";
|
||||||
|
import type {Feature} from "geojson";
|
||||||
|
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||||
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||||
|
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||||
|
import {XCircleIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||||
|
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||||
|
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||||
|
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction";
|
||||||
|
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||||
|
import Loading from "../../Base/Loading.svelte";
|
||||||
|
import {DeleteFlowState} from "./DeleteFlowState";
|
||||||
|
|
||||||
|
export let state: SpecialVisualizationState
|
||||||
|
export let deleteConfig: DeleteConfig
|
||||||
|
|
||||||
|
export let tags: UIEventSource<OsmTags>
|
||||||
|
|
||||||
|
let featureId: OsmId = <OsmId> tags.data.id
|
||||||
|
|
||||||
|
export let feature: Feature
|
||||||
|
export let layer: LayerConfig
|
||||||
|
|
||||||
|
const deleteAbility = new DeleteFlowState(
|
||||||
|
featureId,
|
||||||
|
state,
|
||||||
|
deleteConfig.neededChangesets
|
||||||
|
)
|
||||||
|
|
||||||
|
const canBeDeleted: UIEventSource<boolean | undefined> = deleteAbility.canBeDeleted
|
||||||
|
const canBeDeletedReason = deleteAbility.canBeDeletedReason
|
||||||
|
|
||||||
|
const hasSoftDeletion = deleteConfig.softDeletionTags !== undefined
|
||||||
|
let currentState: "start" | "confirm" | "applying" | "deleted" = ("start")
|
||||||
|
$: {
|
||||||
|
console.log("Current state is", currentState, $canBeDeleted, canBeDeletedReason)
|
||||||
|
deleteAbility.CheckDeleteability(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = Translations.t.delete
|
||||||
|
|
||||||
|
let selectedTags: TagsFilter
|
||||||
|
let changedProperties = undefined
|
||||||
|
$: changedProperties = TagUtils.changeAsProperties(selectedTags?.asChange(tags?.data ?? {}) ?? [])
|
||||||
|
let isHardDelete = undefined
|
||||||
|
$: isHardDelete = changedProperties[DeleteConfig.deleteReasonKey] !== undefined
|
||||||
|
|
||||||
|
|
||||||
|
async function onDelete() {
|
||||||
|
currentState = "applying"
|
||||||
|
let actionToTake: OsmChangeAction
|
||||||
|
const changedProperties = TagUtils.changeAsProperties(selectedTags.asChange(tags?.data ?? {}))
|
||||||
|
const deleteReason = changedProperties[DeleteConfig.deleteReasonKey]
|
||||||
|
console.log("Deleting! Hard?:", canBeDeleted.data, deleteReason)
|
||||||
|
if (deleteReason) {
|
||||||
|
// This is a proper, hard deletion
|
||||||
|
actionToTake = new DeleteAction(
|
||||||
|
featureId,
|
||||||
|
deleteConfig.softDeletionTags,
|
||||||
|
{
|
||||||
|
theme: state?.layout?.id ?? "unknown",
|
||||||
|
specialMotivation: deleteReason,
|
||||||
|
},
|
||||||
|
canBeDeleted.data
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
|
||||||
|
actionToTake = new ChangeTagAction(featureId,
|
||||||
|
selectedTags,
|
||||||
|
tags.data,
|
||||||
|
{
|
||||||
|
theme: state?.layout?.id ?? "unkown",
|
||||||
|
changeType: "special-delete",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await state.changes?.applyAction(actionToTake)
|
||||||
|
tags.data["_deleted"] = "yes"
|
||||||
|
tags.ping()
|
||||||
|
currentState = "deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $canBeDeleted === false && !hasSoftDeletion}
|
||||||
|
<div class="flex low-interaction">
|
||||||
|
<InformationCircleIcon class="w-6 h-6"/>
|
||||||
|
<Tr t={$canBeDeletedReason}/>
|
||||||
|
<Tr class="subtle" t={t.useSomethingElse}/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<LoginToggle ignoreLoading={true} {state}>
|
||||||
|
{#if currentState === "start"}
|
||||||
|
<button class="flex" on:click={() => {currentState = "confirm"}}>
|
||||||
|
<TrashIcon class="w-6 h-6"/>
|
||||||
|
<Tr t={t.delete}/>
|
||||||
|
</button>
|
||||||
|
{:else if currentState === "confirm"}
|
||||||
|
|
||||||
|
<TagRenderingQuestion
|
||||||
|
bind:selectedTags={selectedTags}
|
||||||
|
{tags} config={deleteConfig.constructTagRendering()}
|
||||||
|
{state} selectedElement={feature}
|
||||||
|
{layer}>
|
||||||
|
|
||||||
|
<button slot="save-button" on:click={onDelete}
|
||||||
|
class={(selectedTags === undefined ? "disabled" : "")+ " flex primary bg-red-600"}>
|
||||||
|
<TrashIcon
|
||||||
|
class={"w-6 h-6 rounded-full p-1 ml-1 mr-2 "+(selectedTags !== undefined ? "bg-red-600" : "")}/>
|
||||||
|
<Tr t={t.delete}/>
|
||||||
|
</button>
|
||||||
|
<button slot="cancel" on:click={() => currentState = "start"}>
|
||||||
|
<Tr t={t.cancel}/>
|
||||||
|
</button>
|
||||||
|
<XCircleIcon slot="upper-right" class="w-8 h-8 cursor-pointer"
|
||||||
|
on:click={() => {currentState = "start"}}/>
|
||||||
|
|
||||||
|
<div slot="under-buttons">
|
||||||
|
{#if selectedTags !== undefined}
|
||||||
|
{#if canBeDeleted && isHardDelete}
|
||||||
|
<!-- This is a hard delete - explain that this is a hard delete...-->
|
||||||
|
<Tr t={t.explanations.hardDelete}/>
|
||||||
|
{:else}
|
||||||
|
<!-- This is a soft deletion: we explain _why_ the deletion is soft -->
|
||||||
|
<Tr t={t.explanations.softDelete.Subs({reason: $canBeDeletedReason})}/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</TagRenderingQuestion>
|
||||||
|
|
||||||
|
{:else if currentState === "applying"}
|
||||||
|
<Loading/>
|
||||||
|
{:else}
|
||||||
|
<!-- currentState === 'deleted' -->
|
||||||
|
|
||||||
|
<div class="flex low-interaction">
|
||||||
|
<TrashIcon class="w-6 h-6"/>
|
||||||
|
<Tr t={t.isDeleted}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
|
</LoginToggle>
|
||||||
|
|
||||||
|
{/if}
|
|
@ -1,469 +0,0 @@
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
|
||||||
import Toggle from "../Input/Toggle"
|
|
||||||
import Translations from "../i18n/Translations"
|
|
||||||
import Svg from "../../Svg"
|
|
||||||
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"
|
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
|
||||||
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"
|
|
||||||
import Constants from "../../Models/Constants"
|
|
||||||
import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"
|
|
||||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
|
||||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"
|
|
||||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
|
||||||
import { InputElement } from "../Input/InputElement"
|
|
||||||
import { RadioButton } from "../Input/RadioButton"
|
|
||||||
import { FixedInputElement } from "../Input/FixedInputElement"
|
|
||||||
import Title from "../Base/Title"
|
|
||||||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
|
||||||
import { OsmId, OsmTags } from "../../Models/OsmFeature"
|
|
||||||
import { LoginToggle } from "./LoginButton"
|
|
||||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
|
||||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
|
||||||
import TagHint from "./TagHint.svelte"
|
|
||||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
|
||||||
|
|
||||||
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)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
id: OsmId,
|
|
||||||
tagsSource: UIEventSource<OsmTags>,
|
|
||||||
state: SpecialVisualizationState,
|
|
||||||
options: DeleteConfig
|
|
||||||
) {
|
|
||||||
const deleteAbility = new DeleteabilityChecker(
|
|
||||||
id,
|
|
||||||
state.osmConnection,
|
|
||||||
state.osmObjectDownloader,
|
|
||||||
options.neededChangesets
|
|
||||||
)
|
|
||||||
|
|
||||||
const isDeleted = new UIEventSource(false)
|
|
||||||
const allowSoftDeletion = !!options.softDeletionTags
|
|
||||||
|
|
||||||
const confirm = new UIEventSource<boolean>(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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?.layout?.id ?? "unkown",
|
|
||||||
changeType: "special-delete",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
actionToTake = new DeleteAction(
|
|
||||||
id,
|
|
||||||
options.softDeletionTags,
|
|
||||||
{
|
|
||||||
theme: state?.layout?.id ?? "unkown",
|
|
||||||
specialMotivation: selected["deleteReason"],
|
|
||||||
},
|
|
||||||
deleteAbility.canBeDeleted.data.canBeDeleted
|
|
||||||
)
|
|
||||||
}
|
|
||||||
state.changes?.applyAction(actionToTake)
|
|
||||||
isDeleted.setData(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = Translations.t.delete
|
|
||||||
const cancelButton = t.cancel
|
|
||||||
.SetClass("block btn btn-secondary")
|
|
||||||
.onClick(() => confirm.setData(false))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The button which is shown first. Opening it will trigger the check for deletions
|
|
||||||
*/
|
|
||||||
const deleteButton = new SubtleButton(
|
|
||||||
Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"),
|
|
||||||
t.delete
|
|
||||||
).onClick(() => {
|
|
||||||
deleteAbility.CheckDeleteability(true)
|
|
||||||
confirm.setData(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isShown: Store<boolean> = tagsSource.map((tgs) => tgs.id.indexOf("-") < 0)
|
|
||||||
|
|
||||||
const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state)
|
|
||||||
const deleteDialog = new Combine([
|
|
||||||
new Title(
|
|
||||||
new SubstitutedTranslation(t.whyDelete, tagsSource, state),
|
|
||||||
3
|
|
||||||
),
|
|
||||||
deleteOptionPicker,
|
|
||||||
new Combine([
|
|
||||||
DeleteWizard.constructExplanation(
|
|
||||||
deleteOptionPicker.GetValue(),
|
|
||||||
deleteAbility,
|
|
||||||
tagsSource,
|
|
||||||
state
|
|
||||||
),
|
|
||||||
new Combine([
|
|
||||||
cancelButton,
|
|
||||||
DeleteWizard.constructConfirmButton(deleteOptionPicker.GetValue()).onClick(() =>
|
|
||||||
doDelete(deleteOptionPicker.GetValue().data)
|
|
||||||
),
|
|
||||||
]).SetClass("flex justify-end flex-wrap-reverse"),
|
|
||||||
]).SetClass("flex mt-2 justify-between"),
|
|
||||||
]).SetClass("question")
|
|
||||||
|
|
||||||
const deleteFlow = new Toggle(
|
|
||||||
new Toggle(
|
|
||||||
new Toggle(
|
|
||||||
deleteDialog,
|
|
||||||
new SubtleButton(Svg.envelope_svg(), t.readMessages),
|
|
||||||
state.osmConnection.userDetails.map(
|
|
||||||
(ud) =>
|
|
||||||
ud.csCount >
|
|
||||||
Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
|
|
||||||
ud.unreadMessages == 0
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
deleteButton,
|
|
||||||
confirm
|
|
||||||
),
|
|
||||||
new VariableUiElement(
|
|
||||||
deleteAbility.canBeDeleted.map((cbd) =>
|
|
||||||
new Combine([
|
|
||||||
Svg.delete_not_allowed_svg()
|
|
||||||
.SetStyle("height: 2rem; width: auto")
|
|
||||||
.SetClass("mr-2"),
|
|
||||||
new Combine([
|
|
||||||
t.cannotBeDeleted,
|
|
||||||
cbd.reason.SetClass("subtle"),
|
|
||||||
t.useSomethingElse.SetClass("subtle"),
|
|
||||||
]).SetClass("flex flex-col"),
|
|
||||||
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
deleteAbility.canBeDeleted.map((cbd) => allowSoftDeletion || cbd.canBeDeleted !== false)
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
]).SetClass("flex m-2 rounded-full"),
|
|
||||||
new LoginToggle(deleteFlow, undefined, state),
|
|
||||||
isDeleted
|
|
||||||
),
|
|
||||||
undefined,
|
|
||||||
isShown
|
|
||||||
)
|
|
||||||
|
|
||||||
const self = this
|
|
||||||
confirm.addCallbackAndRunD((dialogIsOpened) => {
|
|
||||||
if (dialogIsOpened) {
|
|
||||||
self.ScrollIntoView()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private static constructConfirmButton(
|
|
||||||
deleteReasons: UIEventSource<any | undefined>
|
|
||||||
): BaseUIElement {
|
|
||||||
const t = Translations.t.delete
|
|
||||||
const btn = new Combine([
|
|
||||||
Svg.delete_icon_svg().SetClass("w-6 h-6 mr-3 block"),
|
|
||||||
t.delete,
|
|
||||||
]).SetClass("flex btn bg-red-500")
|
|
||||||
|
|
||||||
const btnNonActive = new Combine([
|
|
||||||
Svg.delete_icon_svg().SetClass("w-6 h-6 mr-3 block"),
|
|
||||||
t.delete,
|
|
||||||
]).SetClass("flex btn btn-disabled bg-red-200")
|
|
||||||
|
|
||||||
return new Toggle(
|
|
||||||
btn,
|
|
||||||
btnNonActive,
|
|
||||||
deleteReasons.map((reason) => reason !== undefined)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static constructExplanation(
|
|
||||||
selectedOption: UIEventSource<{ deleteReason: string } | { retagTo: TagsFilter }>,
|
|
||||||
deleteAction: DeleteabilityChecker,
|
|
||||||
currentTags: UIEventSource<object>,
|
|
||||||
state?: { osmConnection?: OsmConnection }
|
|
||||||
) {
|
|
||||||
const t = Translations.t.delete
|
|
||||||
return new VariableUiElement(
|
|
||||||
selectedOption.map(
|
|
||||||
(selectedOption) => {
|
|
||||||
if (selectedOption === undefined) {
|
|
||||||
return t.explanations.selectReason.SetClass("subtle")
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
new SvelteUIElement(TagHint, {
|
|
||||||
osmConnection: state.osmConnection,
|
|
||||||
tags: retag,
|
|
||||||
currentProperties: currentTags.data
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteReason = selectedOption["deleteReason"]
|
|
||||||
if (deleteReason !== undefined) {
|
|
||||||
return new VariableUiElement(
|
|
||||||
deleteAction.canBeDeleted.map(({ canBeDeleted, reason }) => {
|
|
||||||
if (canBeDeleted) {
|
|
||||||
// This is a hard delete for which we give an explanation
|
|
||||||
return t.explanations.hardDelete
|
|
||||||
}
|
|
||||||
// This is a soft deletion: we explain _why_ the deletion is soft
|
|
||||||
return t.explanations.softDelete.Subs({ reason: reason })
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[deleteAction.canBeDeleted]
|
|
||||||
)
|
|
||||||
).SetClass("block")
|
|
||||||
}
|
|
||||||
|
|
||||||
private static constructMultipleChoice(
|
|
||||||
config: DeleteConfig,
|
|
||||||
tagsSource: UIEventSource<Record<string, string>>,
|
|
||||||
state: SpecialVisualizationState
|
|
||||||
): InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> {
|
|
||||||
const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[] = []
|
|
||||||
|
|
||||||
for (const nonDeleteOption of config.nonDeleteMappings) {
|
|
||||||
elements.push(
|
|
||||||
new FixedInputElement(
|
|
||||||
new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state),
|
|
||||||
{
|
|
||||||
retagTo: nonDeleteOption.if,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const extraDeleteReason of config.deleteReasons ?? []) {
|
|
||||||
elements.push(
|
|
||||||
new FixedInputElement(
|
|
||||||
new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state),
|
|
||||||
{
|
|
||||||
deleteReason: extraDeleteReason.changesetMessage,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RadioButton(elements, { selectFirstAsDefault: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteabilityChecker {
|
|
||||||
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>
|
|
||||||
private readonly objectDownloader: OsmObjectDownloader
|
|
||||||
private readonly _id: OsmId
|
|
||||||
private readonly _allowDeletionAtChangesetCount: number
|
|
||||||
private readonly _osmConnection: OsmConnection
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
id: OsmId,
|
|
||||||
osmConnection: OsmConnection,
|
|
||||||
objectDownloader: OsmObjectDownloader,
|
|
||||||
allowDeletionAtChangesetCount?: number
|
|
||||||
) {
|
|
||||||
this.objectDownloader = objectDownloader
|
|
||||||
this._id = id
|
|
||||||
this._osmConnection = osmConnection
|
|
||||||
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
|
|
||||||
const self = this
|
|
||||||
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?
|
|
||||||
const deletingPointsOfOtherAllowed = this._osmConnection.userDetails.map((ud) => {
|
|
||||||
if (ud === undefined) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (!ud.loggedIn) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
ud.csCount >=
|
|
||||||
Math.min(
|
|
||||||
Constants.userJourney.deletePointsOfOthersUnlock,
|
|
||||||
this._allowDeletionAtChangesetCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const previousEditors = new UIEventSource<number[]>(undefined)
|
|
||||||
const allByMyself = previousEditors.map(
|
|
||||||
(previous) => {
|
|
||||||
if (previous === null || previous === undefined) {
|
|
||||||
// Not yet downloaded
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const userId = self._osmConnection.userDetails.data.uid
|
|
||||||
return !previous.some((editor) => editor !== userId)
|
|
||||||
},
|
|
||||||
[self._osmConnection.userDetails]
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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!
|
|
||||||
|
|
||||||
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
|
|
||||||
const hist = this.objectDownloader
|
|
||||||
.DownloadHistory(id)
|
|
||||||
.map((versions) =>
|
|
||||||
versions.map((version) =>
|
|
||||||
Number(version.tags["_last_edit:contributor:uid"])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
hist.addCallbackAndRunD((hist) => previousEditors.setData(hist))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allByMyself.data === true) {
|
|
||||||
// Yay! We can download!
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (allByMyself.data === false) {
|
|
||||||
// Nope, downloading not allowed...
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
|
|
||||||
return undefined
|
|
||||||
},
|
|
||||||
[allByMyself]
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
this.objectDownloader.DownloadReferencingRelations(id).then((rels) => {
|
|
||||||
hasRelations.setData(rels.length > 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.objectDownloader.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
|
|
||||||
}
|
|
||||||
if (hasWays.data === false && hasRelationsData === false) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
[hasWays]
|
|
||||||
)
|
|
||||||
|
|
||||||
hasWaysOrRelations.addCallbackAndRun((waysOrRelations) => {
|
|
||||||
if (waysOrRelations == null) {
|
|
||||||
// Not yet loaded - we still wait a little bit
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (waysOrRelations) {
|
|
||||||
// not deleteable by mapcomplete
|
|
||||||
state.setData({
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -50,7 +50,7 @@
|
||||||
}
|
}
|
||||||
feedback.setData(undefined)
|
feedback.setData(undefined)
|
||||||
}
|
}
|
||||||
let selectedTags: TagsFilter = undefined;
|
export let selectedTags: TagsFilter = undefined;
|
||||||
|
|
||||||
|
|
||||||
let mappings: Mapping[] = config?.mappings;
|
let mappings: Mapping[] = config?.mappings;
|
||||||
|
@ -206,9 +206,11 @@
|
||||||
<div class="flex justify-end flex-wrap-reverse sm:flex-nowrap items-stretch">
|
<div class="flex justify-end flex-wrap-reverse sm:flex-nowrap items-stretch">
|
||||||
<!-- TagRenderingQuestion-buttons -->
|
<!-- TagRenderingQuestion-buttons -->
|
||||||
<slot name="cancel"></slot>
|
<slot name="cancel"></slot>
|
||||||
<button on:click={onSave} class={(selectedTags === undefined ? "disabled" : "button-shadow")+" primary"}>
|
<slot name="save-button" {selectedTags}>
|
||||||
<Tr t={Translations.t.general.save}></Tr>
|
<button on:click={onSave} class={(selectedTags === undefined ? "disabled" : "button-shadow")+" primary"}>
|
||||||
</button>
|
<Tr t={Translations.t.general.save}/>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
{#if $showTags === "yes" || $showTags === "always" || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
{#if $showTags === "yes" || $showTags === "always" || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||||
<span class="flex justify-between flex-wrap">
|
<span class="flex justify-between flex-wrap">
|
||||||
|
@ -224,6 +226,7 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
<slot name="under-buttons"/>
|
||||||
</LoginToggle>
|
</LoginToggle>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -64,7 +64,6 @@ import {SaveButton} from "./Popup/SaveButton"
|
||||||
import Lazy from "./Base/Lazy"
|
import Lazy from "./Base/Lazy"
|
||||||
import {CheckBox} from "./Input/Checkboxes"
|
import {CheckBox} from "./Input/Checkboxes"
|
||||||
import Slider from "./Input/Slider"
|
import Slider from "./Input/Slider"
|
||||||
import DeleteWizard from "./Popup/DeleteWizard"
|
|
||||||
import {OsmId, OsmTags, WayId} from "../Models/OsmFeature"
|
import {OsmId, OsmTags, WayId} from "../Models/OsmFeature"
|
||||||
import MoveWizard from "./Popup/MoveWizard"
|
import MoveWizard from "./Popup/MoveWizard"
|
||||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||||
|
@ -74,6 +73,8 @@ import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svel
|
||||||
import {PointImportButtonViz} from "./Popup/ImportButtons/PointImportButtonViz";
|
import {PointImportButtonViz} from "./Popup/ImportButtons/PointImportButtonViz";
|
||||||
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz";
|
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz";
|
||||||
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz";
|
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz";
|
||||||
|
import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte";
|
||||||
|
import {UIElement} from "./UIElement";
|
||||||
|
|
||||||
class NearbyImageVis implements SpecialVisualization {
|
class NearbyImageVis implements SpecialVisualization {
|
||||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||||
|
@ -581,19 +582,13 @@ export default class SpecialVisualizations {
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new VariableUiElement(
|
return new SvelteUIElement(DeleteWizard, {
|
||||||
tagSource
|
tags: tagSource,
|
||||||
.map((tags) => tags.id)
|
deleteConfig: layer.deletion,
|
||||||
.map(
|
state,
|
||||||
(id) =>
|
feature,
|
||||||
new DeleteWizard(
|
layer
|
||||||
<OsmId>id,
|
})
|
||||||
<UIEventSource<OsmTags>>tagSource,
|
|
||||||
state,
|
|
||||||
layer.deletion // Reading the configuration from the layerconfig is a bit cheating and should be factored out
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new ShareLinkViz(),
|
new ShareLinkViz(),
|
||||||
|
|
|
@ -1055,10 +1055,6 @@ video {
|
||||||
height: 24rem;
|
height: 24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-h-2 {
|
|
||||||
max-height: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-12 {
|
.max-h-12 {
|
||||||
max-height: 3rem;
|
max-height: 3rem;
|
||||||
}
|
}
|
||||||
|
@ -1075,14 +1071,6 @@ video {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-h-4 {
|
|
||||||
max-height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-6 {
|
|
||||||
max-height: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -1392,10 +1380,6 @@ video {
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.break-words {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.break-all {
|
.break-all {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
@ -1529,11 +1513,6 @@ video {
|
||||||
background-color: rgb(224 231 255 / var(--tw-bg-opacity));
|
background-color: rgb(224 231 255 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-gray-300 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-red-500 {
|
.bg-red-500 {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||||
|
@ -1544,6 +1523,11 @@ video {
|
||||||
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
|
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-red-600 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.p-8 {
|
.p-8 {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
@ -1596,14 +1580,18 @@ video {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-2 {
|
.pl-1 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pr-2 {
|
.pr-2 {
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pl-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pt-0\.5 {
|
.pt-0\.5 {
|
||||||
padding-top: 0.125rem;
|
padding-top: 0.125rem;
|
||||||
}
|
}
|
||||||
|
@ -1636,10 +1624,6 @@ video {
|
||||||
padding-right: 0px;
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pl-1 {
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pr-1 {
|
.pr-1 {
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
31
test.ts
31
test.ts
|
@ -7,9 +7,9 @@ import {VariableUiElement} from "./UI/Base/VariableUIElement"
|
||||||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||||
import {SvgToPdf} from "./Utils/svgToPdf"
|
import {SvgToPdf} from "./Utils/svgToPdf"
|
||||||
import {Utils} from "./Utils"
|
import {Utils} from "./Utils"
|
||||||
import {PointImportFlowState} from "./UI/Popup/ImportButtons/PointImportFlowState";
|
import DeleteWizard from "./UI/Popup/DeleteFlow/DeleteWizard.svelte";
|
||||||
import PointImportFlow from "./UI/Popup/ImportButtons/PointImportFlow.svelte";
|
import DeleteConfig from "./Models/ThemeConfig/DeleteConfig";
|
||||||
import {Feature, Point} from "geojson";
|
import {UIEventSource} from "./Logic/UIEventSource";
|
||||||
|
|
||||||
function testspecial() {
|
function testspecial() {
|
||||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||||
|
@ -33,6 +33,31 @@ async function testPdf() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function testDelete() {
|
||||||
|
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||||
|
const state = new ThemeViewState(layout)
|
||||||
|
const tags = new UIEventSource({"amenity": "public_bookcase"})
|
||||||
|
new SvelteUIElement(DeleteWizard, {
|
||||||
|
state,
|
||||||
|
tags,
|
||||||
|
layer: layout.layers.find(l => l.id === "public_bookcase"),
|
||||||
|
featureId: "node/10944136609",
|
||||||
|
deleteConfig: new DeleteConfig({
|
||||||
|
nonDeleteMappings: [
|
||||||
|
{
|
||||||
|
if: {"and": ["disused:amenity=public_bookcase", "amenity="]},
|
||||||
|
then: {
|
||||||
|
en: "The bookcase still exists but is not maintained anymore"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, "test")
|
||||||
|
}).AttachTo("maindiv")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
testDelete()
|
||||||
|
|
||||||
// testPdf().then((_) => console.log("All done"))
|
// testPdf().then((_) => console.log("All done"))
|
||||||
/*/
|
/*/
|
||||||
testspecial()
|
testspecial()
|
||||||
|
|
Loading…
Add table
Reference in a new issue