From 3a158a615581e717819ee9ffbaf704caef0fdf8b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 22 Jan 2022 04:01:13 +0100 Subject: [PATCH] Import inspector --- UI/ImportFlow/ImportInspector.ts | 183 ++++++++++++++++++++++++++++++- langs/en.json | 3 + test.ts | 8 +- 3 files changed, 188 insertions(+), 6 deletions(-) diff --git a/UI/ImportFlow/ImportInspector.ts b/UI/ImportFlow/ImportInspector.ts index 075e2fab22..59823f4925 100644 --- a/UI/ImportFlow/ImportInspector.ts +++ b/UI/ImportFlow/ImportInspector.ts @@ -1 +1,182 @@ -export default class ImportInspector {} \ No newline at end of file +import Combine from "../Base/Combine"; +import UserRelatedState from "../../Logic/State/UserRelatedState"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import {Utils} from "../../Utils"; +import UserDetails from "../../Logic/Osm/OsmConnection"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Title from "../Base/Title"; +import Translations from "../i18n/Translations"; +import Loading from "../Base/Loading"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import Toggleable from "../Base/Toggleable"; +import List from "../Base/List"; +import Link from "../Base/Link"; +import {DropDown} from "../Input/DropDown"; +import BaseUIElement from "../BaseUIElement"; +import ValidatedTextField from "../Input/ValidatedTextField"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; +import Toggle from "../Input/Toggle"; + +interface NoteProperties { + "id": number, + "url": string, + "date_created": string + "status": "open" | "closed", + "comments": { + date: string, + uid: number, + user: string, + text: string + }[] +} + +class MassAction extends Combine { + + + constructor(state: UserRelatedState, props: NoteProperties[]) { + const textField = ValidatedTextField.InputForType("text") + + const actions = new DropDown<{ + predicate: (p: NoteProperties) => boolean, + action: (p: NoteProperties) => Promise + }>("On which notes should an action be performed?", [ + { + value: undefined, + shown: "Pick an option..." + }, + { + value: { + predicate: p => p.status === "open", + action: async p => { + const txt = textField.GetValue().data + state.osmConnection.closeNote(p.id, txt) + } + }, + shown: "Add comment to every open note and close all notes" + } + ]) + + const handledNotesCounter = new UIEventSource(undefined) + const apply = new SubtleButton(Svg.checkmark_svg(), "Apply action") + .onClick(async () => { + const {predicate, action} = actions.GetValue().data + for (let i = 0; i < props.length; i++) { + handledNotesCounter.setData(i) + const prop = props[i] + if (!predicate(prop)) { + continue + } + await action(prop) + } + handledNotesCounter.setData(props.length) + }) + super([ + + actions, + textField.SetClass("w-full border border-black"), + new Toggle( + new Toggle( + apply, + + new Toggle( + new Loading(new VariableUiElement(handledNotesCounter.map(state => { + if (state === props.length) { + return "All done!" + } + return "Handling note " + (state + 1) + " out of " + props.length; + }))), + new Combine([Svg.checkmark_svg().SetClass("h-8"), "All done!"]).SetClass("thanks flex p-4"), + handledNotesCounter.map(s => s < props.length) + ), + handledNotesCounter.map(s => s === undefined) + ) + + , undefined, + actions.GetValue().map(v => v !== undefined && textField.GetValue()?.data?.length > 15, [textField.GetValue()]) + ), + new Toggle( + new FixedUiElement ( "Testmode enable").SetClass("alert"), undefined, + state.featureSwitchIsTesting + ) + ]); + } + +} + +class ImportInspector extends VariableUiElement { + + constructor(userDetails: UserDetails, state: UserRelatedState) { + const t = Translations.t.importInspector; + + + const url = "https://api.openstreetmap.org/api/0.6/notes/search.json?user=" + userDetails.uid + "&limit=10000&sort=created_at&q=%23import" + const notes: UIEventSource<{ error: string } | { success: { features: { properties: NoteProperties }[] } }> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url)) + notes.addCallbackAndRun(n => console.log("Notes are:", n)) + super(notes.map(notes => { + + if (notes === undefined) { + return new Loading("Loading your notes which mention '#import'") + } + if (notes["error"] !== undefined) { + return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass("alert") + } + // We only care about the properties here + const props = notes["success"].features.map(f => f.properties) + + const perBatch = new Map() + const prefix = "https://mapcomplete.osm.be/" + for (const prop of props) { + const lines = prop.comments[0].text.split("\n") + const trigger = lines.findIndex(l => l.startsWith(prefix) && l.endsWith("#import")) + if (trigger < 0) { + continue + } + let theme = lines[trigger].substr(prefix.length) + theme = theme.substr(0, theme.indexOf(".")) + const key = lines[0] + if (!perBatch.has(key)) { + perBatch.set(key, {props: [], theme}) + } + perBatch.get(key).props.push(prop) + } + const els = [] + perBatch.forEach(({props, theme}, intro) => { + els.push(new Combine([ + new Title(theme + ": " + intro + " (" + props.length + " features)", 2), + new Toggleable(new FixedUiElement("Notes"), + new List(props.map(prop => new Link( + "" + prop.id, + "https://openstreetmap.org/note/" + prop.id, true + )))), + new Title("Mass apply an action"), + new MassAction(state, props).SetClass("block") + + + ])) + }) + return new Combine(els) + + })); + } + +} + +export default class ImportInspectorGui extends Combine { + + constructor() { + const state = new UserRelatedState(undefined) + const t = Translations.t.importInspector; + super([ + new Title(t.title, 1), + new VariableUiElement(state.osmConnection.userDetails.map(ud => { + if (ud === undefined || ud.loggedIn === false) { + return undefined + } + return new ImportInspector(ud, state); + })) + ]); + } + + +} \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index f98dfa695f..e449154748 100644 --- a/langs/en.json +++ b/langs/en.json @@ -509,5 +509,8 @@ "inspectDataTitle": "Inspect data of {count} features to import", "inspectDidAutoDected": "Layer was chosen automatically", "inspectLooksCorrect": "These values look correct" + }, + "importInspector": { + "title": "Inspect and manage import notes" } } diff --git a/test.ts b/test.ts index 7b98702cdf..6ff12256b4 100644 --- a/test.ts +++ b/test.ts @@ -1,6 +1,4 @@ -import ValidatedTextField from "./UI/Input/ValidatedTextField"; -import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import ImportInspectorGui from "./UI/ImportFlow/ImportInspector"; + +new ImportInspectorGui().AttachTo("maindiv") -const tf = ValidatedTextField.InputForType("url") -tf.AttachTo("maindiv") -new VariableUiElement(tf.GetValue()).AttachTo("extradiv") \ No newline at end of file