Refactoring: port 'TagApplyButton' to svelte

This commit is contained in:
Pieter Vander Vennet 2025-06-19 01:51:09 +02:00
parent 2bc2a6cddf
commit d1f7ae2462
8 changed files with 109 additions and 99 deletions

View file

@ -79,7 +79,7 @@
"source": { "source": {
"geoJson": "https://maproulette.org/api/v2/challenge/view/39519" "geoJson": "https://maproulette.org/api/v2/challenge/view/39519"
}, },
"isShown": "mr_taskStatus=Created",
"calculatedTags": [ "calculatedTags": [
"_closest_osm_poi=closest(feat)('atm')?.properties?.id", "_closest_osm_poi=closest(feat)('atm')?.properties?.id",
"_closest_osm_poi_distance=Math.round(distanceTo(feat)(feat.properties._closest_osm_poi))", "_closest_osm_poi_distance=Math.round(distanceTo(feat)(feat.properties._closest_osm_poi))",
@ -161,7 +161,7 @@
"it": "Aggiungi tutti i tag suggeriti allo sportello bancomat più vicino" "it": "Aggiungi tutti i tag suggeriti allo sportello bancomat più vicino"
}, },
"image": "./assets/svg/addSmall.svg", "image": "./assets/svg/addSmall.svg",
"maproulette_id": "mr_taskId" "maproulette_id": "id"
} }
} }
}, },

View file

@ -171,6 +171,7 @@
}, },
"apply_button": { "apply_button": {
"appliedOnAnotherObject": "The object {id} will receive {tags}", "appliedOnAnotherObject": "The object {id} will receive {tags}",
"applying": "Applying changes",
"isApplied": "The changes are applied" "isApplied": "The changes are applied"
}, },
"attribution": { "attribution": {

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging { export class ThemeMetaTagging {
public static readonly themeName = "usersettings" public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
feat.properties._description Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
?.at(1) Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
Utils.AddLazyProperty( feat.properties['__current_backgroun'] = 'initial_value'
feat.properties, }
"_d", }
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}

View file

@ -1,7 +1,6 @@
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource"
import { Tag } from "../../../Logic/Tags/Tag" import { Tag } from "../../../Logic/Tags/Tag"
import TagApplyButton from "../TagApplyButton"
import { PointImportFlowArguments } from "./PointImportFlowState" import { PointImportFlowArguments } from "./PointImportFlowState"
import { Translation } from "../../i18n/Translation" import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations" import Translations from "../../i18n/Translations"
@ -11,6 +10,7 @@ import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJso
import conflation_json from "../../../../assets/layers/conflation/conflation.json" import conflation_json from "../../../../assets/layers/conflation/conflation.json"
import { SpecialVisualizationState } from "../../SpecialVisualization" import { SpecialVisualizationState } from "../../SpecialVisualization"
import { OsmTags } from "../../../Models/OsmFeature" import { OsmTags } from "../../../Models/OsmFeature"
import TagApplyViz from "../../SpecialVisualisations/TagApplyViz"
export interface ImportFlowArguments { export interface ImportFlowArguments {
readonly text: string readonly text: string
@ -96,9 +96,9 @@ ${Utils.special_visualizations_importRequirementDocs}
return new ImmutableStore(tags) return new ImmutableStore(tags)
} }
newTags = TagApplyButton.generateTagsToApply(items, originalFeatureTags) newTags = TagApplyViz.generateTagsToApply(items, originalFeatureTags)
} else { } else {
newTags = TagApplyButton.generateTagsToApply(tags, originalFeatureTags) newTags = TagApplyViz.generateTagsToApply(tags, originalFeatureTags)
} }
return newTags return newTags
} }

View file

@ -58,7 +58,7 @@
}, 50) }, 50)
} }
const _htmlElement = new UIEventSource<HTMLElement>(undefined) let _htmlElement = new UIEventSource<HTMLElement>(undefined)
$: _htmlElement.setData(htmlElem) $: _htmlElement.setData(htmlElem)
function setHighlighting() { function setHighlighting() {

View file

@ -2,7 +2,6 @@ import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisua
import Maproulette from "../../Logic/Maproulette" import Maproulette from "../../Logic/Maproulette"
import SvelteUIElement from "../Base/SvelteUIElement" import SvelteUIElement from "../Base/SvelteUIElement"
import MaprouletteSetStatus from "../MapRoulette/MaprouletteSetStatus.svelte" import MaprouletteSetStatus from "../MapRoulette/MaprouletteSetStatus.svelte"
import TagApplyButton from "../Popup/TagApplyButton"
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"
@ -16,11 +15,12 @@ import LinkedDataLoader from "../../Logic/Web/LinkedDataLoader"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import ComparisonTool from "../Comparison/ComparisonTool.svelte" import ComparisonTool from "../Comparison/ComparisonTool.svelte"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import TagApplyViz from "./TagApplyViz"
export class DataImportSpecialVisualisations { export class DataImportSpecialVisualisations {
public static initList(): (SpecialVisualization & { group })[] { public static initList(): (SpecialVisualization & { group })[] {
return [ return [
new TagApplyButton(), new TagApplyViz(),
new PointImportButtonViz(), new PointImportButtonViz(),
new WayImportButtonViz(), new WayImportButtonViz(),
new ConflateImportButtonViz(), new ConflateImportButtonViz(),

View file

@ -0,0 +1,65 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Tag } from "../../Logic/Tags/Tag"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import LoginToggle from "../Base/LoginToggle.svelte"
import Tr from "../Base/Tr.svelte"
import Loading from "../Base/Loading.svelte"
import TagApplyViz from "./TagApplyViz"
import Icon from "../Map/Icon.svelte"
import TagExplanation from "../Popup/TagExplanation.svelte"
import { And } from "../../Logic/Tags/And"
import Translations from "../i18n/Translations"
/**
* Works closely together with 'TagApplyViz
*/
export let msg: string
export let image: string | undefined
export let tags: Store<Record<string, string>>
export let tagsToApply: Store<Tag[]>
export let targetIdKey: string
export let onApply: () => Promise<void>
export let state: SpecialVisualizationState
const t = Translations.t.general.apply_button
// THis button might be shown on MapRoulette-items, which might already have been applied
// This will default to 'false' for non-maproulette challenges
let isMaprouletteAndApplied = tags?.data?.["mr_taskStatus"] !== undefined &&
tags?.data?.["mr_taskStatus"] !== "Created"
let currentState: UIEventSource<"init" | "applying" | "applied"> = new UIEventSource(
isMaprouletteAndApplied ? "applied" : "init")
async function apply() {
currentState.set("applying")
await onApply()
currentState.set("applied")
}
</script>
<LoginToggle {state} ignoreLoading>
{#if $currentState === "init"}
<button on:click={() => apply()}>
<Icon icon={image} />
<div class="flex flex-col">
<div>{msg}</div>
{#if targetIdKey}
<Tr cls="subtle break-all"
t={t.appliedOnAnotherObject.Subs({ id: $tags[targetIdKey], tags: new And($tagsToApply).asHumanString(false, false, {}) })} />
{:else}
<TagExplanation tagsFilter={new And($tagsToApply)} />
{/if}
</div>
</button>
{:else if $currentState === "applying"}
<Loading>
<Tr t={t.applying} />
</Loading>
{:else if $currentState === "applied"}
<Tr t={t.isApplied} cls="thanks" />
{/if}
</LoginToggle>

View file

@ -1,23 +1,16 @@
import { AutoAction } from "./AutoApplyButtonVis" import { AutoAction } from "../Popup/AutoApplyButtonVis"
import Translations from "../i18n/Translations" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { VariableUiElement } from "../Base/VariableUIElement" import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton" import { Tag } from "../../Logic/Tags/Tag"
import Combine from "../Base/Combine" import { Feature } from "geojson"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson"
import Maproulette from "../../Logic/Maproulette" import Maproulette from "../../Logic/Maproulette"
import SvelteUIElement from "../Base/SvelteUIElement" import SvelteUIElement from "../Base/SvelteUIElement"
import Icon from "../Map/Icon.svelte" import TagApplyButton from "./TagApplyButton.svelte"
export default class TagApplyButton implements AutoAction, SpecialVisualization { export default class TagApplyViz implements AutoAction, SpecialVisualization {
public readonly funcName = "tag_apply" public readonly funcName = "tag_apply"
needsUrls = [] needsUrls = []
group = "data_import" group = "data_import"
@ -47,7 +40,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
{ {
name: "maproulette_id", name: "maproulette_id",
defaultValue: undefined, defaultValue: undefined,
doc: "If specified, this maproulette-challenge will be closed when the tags are applied. This should be the ID of the task, _not_ the task_id.", doc: "If specified, this maproulette-challenge will be closed when the tags are applied. This should be the `id` of the individual task, _not_ the task_id (which corresponds with the challenge).",
}, },
] ]
public readonly example = public readonly example =
@ -55,7 +48,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
public static generateTagsToApply( public static generateTagsToApply(
spec: string, spec: string,
tagSource: Store<Record<string, string>> tagSource: Store<Record<string, string>>,
): Store<Tag[]> { ): Store<Tag[]> {
// Check whether we need to look up a single value // Check whether we need to look up a single value
@ -73,7 +66,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
tgsSpec.push([key, properties[key]]) tgsSpec.push([key, properties[key]])
} }
} else { } else {
tgsSpec = TagApplyButton.parseTagSpec(spec) tgsSpec = TagApplyViz.parseTagSpec(spec)
} }
return tagSource.map((tags) => { return tagSource.map((tags) => {
@ -135,9 +128,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
_: Feature, _: Feature,
state: SpecialVisualizationState, state: SpecialVisualizationState,
tags: UIEventSource<any>, tags: UIEventSource<any>,
args: string[] args: string[],
): Promise<void> { ): Promise<void> {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const tagsToApply = TagApplyViz.generateTagsToApply(args[0], tags)
const targetIdKey = args[3] const targetIdKey = args[3]
const targetId = tags.data[targetIdKey] ?? tags.data.id const targetId = tags.data[targetIdKey] ?? tags.data.id
@ -148,7 +141,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
{ {
theme: state.theme.id, theme: state.theme.id,
changeType: "answer", changeType: "answer",
} },
) )
await state.changes.applyAction(changeAction) await state.changes.applyAction(changeAction)
try { try {
@ -159,6 +152,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
const maproulette_id_key = args[4] const maproulette_id_key = args[4]
if (maproulette_id_key) { if (maproulette_id_key) {
const maproulette_id = tags.data[maproulette_id_key] const maproulette_id = tags.data[maproulette_id_key]
console.log("Looking for maproulette feature with id", maproulette_id,"based on key", maproulette_id_key)
const maproulette_feature = state.indexedFeatures.featuresById.data.get(maproulette_id) const maproulette_feature = state.indexedFeatures.featuresById.data.get(maproulette_id)
const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId) const maproulette_task_id = Number(maproulette_feature.properties.mr_taskId)
await Maproulette.singleton.closeTask( await Maproulette.singleton.closeTask(
@ -167,7 +161,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
state, state,
{ {
comment: "Tags are copied onto " + targetId + " with MapComplete", comment: "Tags are copied onto " + targetId + " with MapComplete",
} },
) )
maproulette_feature.properties["mr_taskStatus"] = "Fixed" maproulette_feature.properties["mr_taskStatus"] = "Fixed"
state.featureProperties.getStore(maproulette_id).ping() state.featureProperties.getStore(maproulette_id).ping()
@ -177,46 +171,24 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
public constr( public constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[], args: string[], feature: Feature,
feature: Feature ): SvelteUIElement {
): BaseUIElement {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const tagsToApply: Store<Tag[]> = TagApplyViz.generateTagsToApply(args[0], tags)
const msg = args[1] const msg = args[1]
let image = args[2]?.trim() let image = args[2]?.trim()
if (image === "" || image === "undefined") { if (image === "" || image === "undefined") {
image = undefined image = undefined
} }
const targetIdKey = args[3] const targetIdKey = args[3]
const t = Translations.t.general.apply_button
const tagsExplanation = new VariableUiElement( const onApply = async () => {
tagsToApply.map((tagsToApply) => {
const tagsStr = tagsToApply.map((t) => t.asHumanString(false, true)).join("&")
let el: BaseUIElement = new FixedUiElement(tagsStr)
if (targetIdKey !== undefined) {
const targetId = tags.data[targetIdKey] ?? tags.data.id
el = t.appliedOnAnotherObject.Subs({ tags: tagsStr, id: targetId })
}
return el
})
).SetClass("subtle break-all")
const applied = new UIEventSource(
tags?.data?.["mr_taskStatus"] !== undefined &&
tags?.data?.["mr_taskStatus"] !== "Created"
) // This will default to 'false' for non-maproulette challenges
const applyButton = new SubtleButton(
new SvelteUIElement(Icon, { icon: image }),
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
).onClick(async () => {
applied.setData(true)
await this.applyActionOn(feature, state, tags, args) await this.applyActionOn(feature, state, tags, args)
}) }
return new Toggle(
new Toggle(t.isApplied.SetClass("thanks"), applyButton, applied), return new SvelteUIElement(TagApplyButton, {
undefined, state, tags, tagsToApply, msg, image, targetIdKey, onApply
state.osmConnection.isLoggedIn })
)
} }
} }