diff --git a/Models/ThemeConfig/DeleteConfig.ts b/Models/ThemeConfig/DeleteConfig.ts index 39d7359dd..9a9d0f4a4 100644 --- a/Models/ThemeConfig/DeleteConfig.ts +++ b/Models/ThemeConfig/DeleteConfig.ts @@ -3,8 +3,14 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter" import { DeleteConfigJson } from "./Json/DeleteConfigJson" import Translations from "../../UI/i18n/Translations" 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 { + public static readonly deleteReasonKey = "_delete_reason" private static readonly defaultDeleteReasons: { changesetMessage: string explanation: Translation @@ -32,7 +38,7 @@ export default class DeleteConfig { changesetMessage: string }[] - public readonly nonDeleteMappings?: { if: TagsFilter; then: TypedTranslation }[] + private readonly nonDeleteMappings?: { if: TagConfigJson; then: Translation }[] public readonly softDeletionTags?: TagsFilter public readonly neededChangesets?: number @@ -61,8 +67,9 @@ export default class DeleteConfig { this.nonDeleteMappings = (json.nonDeleteMappings ?? []).map((nonDelete, i) => { const ctx = `${context}.extraDeleteReasons[${i}]` + TagUtils.Tag(nonDelete.if, ctx + ".if") // for validation only return { - if: TagUtils.Tag(nonDelete.if, ctx + ".if"), + if: nonDelete.if, then: Translations.T(nonDelete.then, ctx + ".then"), } }) @@ -88,4 +95,29 @@ export default class DeleteConfig { } this.neededChangesets = json.neededChangesets } + + public constructTagRendering(): TagRenderingConfig { + const t = Translations.t.delete + + const mappings: {if: TagConfigJson, then: Record} []= [] + 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) + } } diff --git a/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index d388766b5..c44df3235 100644 --- a/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -10,7 +10,7 @@ export interface MappingConfigJson { * Shown if the 'if is fulfilled * Type: rendered */ - then: string | any + then: string | Record /** * An extra icon supporting the choice * Type: icon diff --git a/UI/Popup/DeleteFlow/DeleteFlowState.ts b/UI/Popup/DeleteFlow/DeleteFlowState.ts new file mode 100644 index 000000000..cebaced16 --- /dev/null +++ b/UI/Popup/DeleteFlow/DeleteFlowState.ts @@ -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 = new UIEventSource(undefined) + public readonly canBeDeletedReason: UIEventSource = new UIEventSource(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(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 = new UIEventSource(null) + const hasWays: UIEventSource = new UIEventSource(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) + } + }) + } + +} diff --git a/UI/Popup/DeleteFlow/DeleteWizard.svelte b/UI/Popup/DeleteFlow/DeleteWizard.svelte new file mode 100644 index 000000000..75ae58fc2 --- /dev/null +++ b/UI/Popup/DeleteFlow/DeleteWizard.svelte @@ -0,0 +1,155 @@ + + +{#if $canBeDeleted === false && !hasSoftDeletion} +
+ + + +
+{:else} + + {#if currentState === "start"} + + {:else if currentState === "confirm"} + + + + + + {currentState = "start"}}/> + +
+ {#if selectedTags !== undefined} + {#if canBeDeleted && isHardDelete} + + + {:else} + + + {/if} + {/if} +
+
+ + {:else if currentState === "applying"} + + {:else} + + +
+ + +
+ + {/if} + + +
+ +{/if} diff --git a/UI/Popup/DeleteWizard.ts b/UI/Popup/DeleteWizard.ts deleted file mode 100644 index beab1237c..000000000 --- a/UI/Popup/DeleteWizard.ts +++ /dev/null @@ -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, - 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(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 = 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 - ): 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, - 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>, - 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(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 = new UIEventSource(null) - const hasWays: UIEventSource = new UIEventSource(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, - }) - } - }) - } -} diff --git a/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/UI/Popup/TagRendering/TagRenderingQuestion.svelte index 0bf235798..a3467abad 100644 --- a/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -50,7 +50,7 @@ } feedback.setData(undefined) } - let selectedTags: TagsFilter = undefined; + export let selectedTags: TagsFilter = undefined; let mappings: Mapping[] = config?.mappings; @@ -206,9 +206,11 @@
- + + +
{#if $showTags === "yes" || $showTags === "always" || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging} @@ -224,6 +226,7 @@ {/if} + {/if} diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index d8abf38a7..a53792509 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -64,7 +64,6 @@ import {SaveButton} from "./Popup/SaveButton" import Lazy from "./Base/Lazy" import {CheckBox} from "./Input/Checkboxes" import Slider from "./Input/Slider" -import DeleteWizard from "./Popup/DeleteWizard" import {OsmId, OsmTags, WayId} from "../Models/OsmFeature" import MoveWizard from "./Popup/MoveWizard" import SplitRoadWizard from "./Popup/SplitRoadWizard" @@ -74,6 +73,8 @@ import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svel import {PointImportButtonViz} from "./Popup/ImportButtons/PointImportButtonViz"; import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"; import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"; +import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"; +import {UIElement} from "./UIElement"; class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -581,19 +582,13 @@ export default class SpecialVisualizations { feature: Feature, layer: LayerConfig ): BaseUIElement { - return new VariableUiElement( - tagSource - .map((tags) => tags.id) - .map( - (id) => - new DeleteWizard( - id, - >tagSource, - state, - layer.deletion // Reading the configuration from the layerconfig is a bit cheating and should be factored out - ) - ) - ) + return new SvelteUIElement(DeleteWizard, { + tags: tagSource, + deleteConfig: layer.deletion, + state, + feature, + layer + }) }, }, new ShareLinkViz(), diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 36f89ff9e..7ac3e10f0 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -1055,10 +1055,6 @@ video { height: 24rem; } -.max-h-2 { - max-height: 0.5rem; -} - .max-h-12 { max-height: 3rem; } @@ -1075,14 +1071,6 @@ video { max-height: 100vh; } -.max-h-4 { - max-height: 1rem; -} - -.max-h-6 { - max-height: 1.5rem; -} - .w-full { width: 100%; } @@ -1392,10 +1380,6 @@ video { word-break: normal; } -.break-words { - overflow-wrap: break-word; -} - .break-all { word-break: break-all; } @@ -1529,11 +1513,6 @@ video { 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 { --tw-bg-opacity: 1; 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)); } +.bg-red-600 { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity)); +} + .p-8 { padding: 2rem; } @@ -1596,14 +1580,18 @@ video { padding-left: 1rem; } -.pl-2 { - padding-left: 0.5rem; +.pl-1 { + padding-left: 0.25rem; } .pr-2 { padding-right: 0.5rem; } +.pl-2 { + padding-left: 0.5rem; +} + .pt-0\.5 { padding-top: 0.125rem; } @@ -1636,10 +1624,6 @@ video { padding-right: 0px; } -.pl-1 { - padding-left: 0.25rem; -} - .pr-1 { padding-right: 0.25rem; } diff --git a/test.ts b/test.ts index 0a0c3c73a..5d697e8a7 100644 --- a/test.ts +++ b/test.ts @@ -7,9 +7,9 @@ import {VariableUiElement} from "./UI/Base/VariableUIElement" import SvelteUIElement from "./UI/Base/SvelteUIElement" import {SvgToPdf} from "./Utils/svgToPdf" import {Utils} from "./Utils" -import {PointImportFlowState} from "./UI/Popup/ImportButtons/PointImportFlowState"; -import PointImportFlow from "./UI/Popup/ImportButtons/PointImportFlow.svelte"; -import {Feature, Point} from "geojson"; +import DeleteWizard from "./UI/Popup/DeleteFlow/DeleteWizard.svelte"; +import DeleteConfig from "./Models/ThemeConfig/DeleteConfig"; +import {UIEventSource} from "./Logic/UIEventSource"; function testspecial() { const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) @@ -33,6 +33,31 @@ async function testPdf() { } +function testDelete() { + const layout = new LayoutConfig(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")) /*/ testspecial()