From 33ef83c4a91b028f9295a91acebcc96b96efe7f0 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 24 Jan 2022 03:09:21 +0100 Subject: [PATCH] Finish import_viewer gui --- Logic/Tags/TagUtils.ts | 5 +- UI/Base/LeftIndex.ts | 25 +++ UI/Base/Toggleable.ts | 2 +- UI/BigComponents/BackToIndex.ts | 2 +- UI/ImportFlow/CreateNotes.ts | 18 +- UI/ImportFlow/ImportHelperGui.ts | 26 +-- ...{ImportInspector.ts => ImportViewerGui.ts} | 166 +++++++++++++----- UI/ProfessionalGui.ts | 11 +- Utils.ts | 7 + css/index-tailwind-output.css | 24 +-- langs/nl.json | 7 +- package.json | 2 +- scripts/build.sh | 2 +- test.ts | 4 - 14 files changed, 198 insertions(+), 103 deletions(-) create mode 100644 UI/Base/LeftIndex.ts rename UI/ImportFlow/{ImportInspector.ts => ImportViewerGui.ts} (50%) diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index 2b41ed988..d5b532119 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -197,10 +197,7 @@ export class TagUtils { } let b = Number(value?.trim()) if (isNaN(b)) { - if (value.endsWith(" UTC")) { - value = value.replace(" UTC", "+00") - } - b = new Date(value).getTime() + b = Utils.ParseDate(value).getTime() if (isNaN(b)) { return false } diff --git a/UI/Base/LeftIndex.ts b/UI/Base/LeftIndex.ts new file mode 100644 index 000000000..6c734ab01 --- /dev/null +++ b/UI/Base/LeftIndex.ts @@ -0,0 +1,25 @@ +import BaseUIElement from "../BaseUIElement"; +import Combine from "./Combine"; +import BackToIndex from "../BigComponents/BackToIndex"; + +export default class LeftIndex extends Combine{ + + + constructor(leftContents: BaseUIElement[], mainContent: BaseUIElement, options?:{ + hideBackButton : false | boolean + } ) { + + let back : BaseUIElement = undefined; + if(options?.hideBackButton ?? true){ + back = new BackToIndex() + } + super([ + new Combine([ + new Combine([back, ...leftContents]).SetClass("sticky top-4"), + ]).SetClass("ml-4 block w-full md:w-2/6 lg:w-1/6"), + mainContent.SetClass("m-8 w-full mb-24") + ]) + this.SetClass("h-full block md:flex") + } + +} \ No newline at end of file diff --git a/UI/Base/Toggleable.ts b/UI/Base/Toggleable.ts index 0d5b941cd..55b78a4b5 100644 --- a/UI/Base/Toggleable.ts +++ b/UI/Base/Toggleable.ts @@ -53,7 +53,7 @@ export default class Toggleable extends Combine { this.isVisible.addCallbackAndRun(isVisible => { if (isVisible) { - contentElement.style.maxHeight = "8gs0vh" + contentElement.style.maxHeight = "50vh" contentElement.style.overflowY = "auto" contentElement.style["-webkit-mask-image"] = "unset" } else { diff --git a/UI/BigComponents/BackToIndex.ts b/UI/BigComponents/BackToIndex.ts index 9edd54c59..648bd90bf 100644 --- a/UI/BigComponents/BackToIndex.ts +++ b/UI/BigComponents/BackToIndex.ts @@ -11,7 +11,7 @@ export default class BackToIndex extends SubtleButton { Svg.back_svg().SetStyle("height: 1.5rem;"), message ?? Translations.t.general.backToMapcomplete, { - url: "./index.html" + url: "index.html" } ) } diff --git a/UI/ImportFlow/CreateNotes.ts b/UI/ImportFlow/CreateNotes.ts index 60379a164..2b19f83dd 100644 --- a/UI/ImportFlow/CreateNotes.ts +++ b/UI/ImportFlow/CreateNotes.ts @@ -6,7 +6,8 @@ import Toggle from "../Input/Toggle"; import Loading from "../Base/Loading"; import {VariableUiElement} from "../Base/VariableUIElement"; import {FixedUiElement} from "../Base/FixedUiElement"; -import Link from "../Base/Link"; +import {SubtleButton} from "../Base/SubtleButton"; +import Svg from "../../Svg"; export class CreateNotes extends Combine { @@ -24,7 +25,7 @@ export class CreateNotes extends Combine { const tags: string [] = [] for (const key in f.properties) { - if(f.properties[key] === ""){ + if (f.properties[key] === "") { continue } tags.push(key + "=" + f.properties[key].replace(/=/, "\\=").replace(/;/g, "\\;").replace(/\n/g, "\\n")) @@ -56,7 +57,13 @@ export class CreateNotes extends Combine { "Hang on while we are importing...", new Toggle( new Loading(new VariableUiElement(currentNote.map(count => new FixedUiElement("Imported " + count + " out of " + v.features.length + " notes")))), - new FixedUiElement("All done!"), + new Combine([ + new FixedUiElement("All done!").SetClass("thanks"), + new SubtleButton(Svg.note_svg(), "Inspect the progress of your notes in the 'import_viewer'", { + url: "import_viewer.html" + }) + ] + ), currentNote.map(count => count < v.features.length) ), new VariableUiElement(failed.map(failed => { @@ -69,11 +76,6 @@ export class CreateNotes extends Combine { ...failed ]).SetClass("flex flex-col") - })), - new VariableUiElement(createdNotes.map(notes => { - const links = notes.map(n => - new Link(new FixedUiElement("https://openstreetmap.org/note/" + n), "https://openstreetmap.org/note/" + n, true)); - return new Combine(links).SetClass("flex flex-col"); })) ]) this.SetClass("flex flex-col"); diff --git a/UI/ImportFlow/ImportHelperGui.ts b/UI/ImportFlow/ImportHelperGui.ts index 5fbd7f2b4..732da1235 100644 --- a/UI/ImportFlow/ImportHelperGui.ts +++ b/UI/ImportFlow/ImportHelperGui.ts @@ -1,7 +1,6 @@ import Combine from "../Base/Combine"; import Toggle from "../Input/Toggle"; import LanguagePicker from "../LanguagePicker"; -import BackToIndex from "../BigComponents/BackToIndex"; import UserRelatedState from "../../Logic/State/UserRelatedState"; import BaseUIElement from "../BaseUIElement"; import MinimapImplementation from "../Base/MinimapImplementation"; @@ -21,11 +20,11 @@ import {CompareToAlreadyExistingNotes} from "./CompareToAlreadyExistingNotes"; import Introdution from "./Introdution"; import LoginToImport from "./LoginToImport"; import {MapPreview} from "./MapPreview"; +import LeftIndex from "../Base/LeftIndex"; +import {SubtleButton} from "../Base/SubtleButton"; -export default class ImportHelperGui extends Combine { +export default class ImportHelperGui extends LeftIndex { constructor() { - const t = Translations.t.importHelper; - const state = new UserRelatedState(undefined) // We disable the userbadge, as various 'showData'-layers will give a read-only view in this case @@ -61,24 +60,17 @@ export default class ImportHelperGui extends Combine { , true) const leftContents: BaseUIElement[] = [ - new BackToIndex().SetClass("block pl-4"), + new SubtleButton(undefined,"Inspect your preview imports", { + url:"import_viewer.html" + }), toc, new Toggle(new FixedUiElement("Testmode - won't actually import notes").SetClass("alert"), undefined, state.featureSwitchIsTesting), LanguagePicker.CreateLanguagePicker(Translations.t.importHelper.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), ].map(el => el?.SetClass("pl-4")) - const leftBar = new Combine([ - new Combine(leftContents).SetClass("sticky top-4 m-4"), - ]).SetClass("block w-full md:w-2/6 lg:w-1/6") - - - - - super([ - new Combine([ - leftBar, - flow.SetClass("m-8 w-full mb-24") - ]).SetClass("h-full block md:flex")]) + super( + leftContents, + flow) } diff --git a/UI/ImportFlow/ImportInspector.ts b/UI/ImportFlow/ImportViewerGui.ts similarity index 50% rename from UI/ImportFlow/ImportInspector.ts rename to UI/ImportFlow/ImportViewerGui.ts index 59823f492..3758efa3b 100644 --- a/UI/ImportFlow/ImportInspector.ts +++ b/UI/ImportFlow/ImportViewerGui.ts @@ -8,8 +8,6 @@ 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"; @@ -17,11 +15,18 @@ import ValidatedTextField from "../Input/ValidatedTextField"; import {SubtleButton} from "../Base/SubtleButton"; import Svg from "../../Svg"; import Toggle from "../Input/Toggle"; +import Table from "../Base/Table"; +import LeftIndex from "../Base/LeftIndex"; +import Toggleable, {Accordeon} from "../Base/Toggleable"; +import TableOfContents from "../Base/TableOfContents"; +import LoginButton from "../Popup/LoginButton"; +import BackToIndex from "../BigComponents/BackToIndex"; interface NoteProperties { "id": number, "url": string, - "date_created": string + "date_created": string, + closed_at?: string, "status": "open" | "closed", "comments": { date: string, @@ -31,9 +36,15 @@ interface NoteProperties { }[] } +interface NoteState { + props: NoteProperties, + theme: string, + intro: string, + dateStr: string, + status: "imported" | "already_mapped" | "invalid" | "closed" | "not_found" | "open" +} + class MassAction extends Combine { - - constructor(state: UserRelatedState, props: NoteProperties[]) { const textField = ValidatedTextField.InputForType("text") @@ -54,6 +65,16 @@ class MassAction extends Combine { } }, shown: "Add comment to every open note and close all notes" + }, + { + value: { + predicate: p => p.status === "open", + action: async p => { + const txt = textField.GetValue().data + state.osmConnection.addCommentToNode(p.id, txt) + } + }, + shown: "Add comment to every open note" } ]) @@ -96,7 +117,7 @@ class MassAction extends Combine { actions.GetValue().map(v => v !== undefined && textField.GetValue()?.data?.length > 15, [textField.GetValue()]) ), new Toggle( - new FixedUiElement ( "Testmode enable").SetClass("alert"), undefined, + new FixedUiElement("Testmode enable").SetClass("alert"), undefined, state.featureSwitchIsTesting ) ]); @@ -104,6 +125,36 @@ class MassAction extends Combine { } + +class BatchView extends Toggleable { + constructor(state: UserRelatedState, noteStates: NoteState[]) { + const {theme, intro, dateStr} = noteStates[0] + console.log("Creating a batchview for ", noteStates) + super( + new Title(theme + ": " + intro, 2), + new Combine([ + new FixedUiElement(dateStr), + new FixedUiElement("Click to expand/collapse table"), + + + new Table( + ["id", "status", "last comment"], + noteStates.map(ns => { + const link = new Link( + "" + ns.props.id, + "https://openstreetmap.org/note/" + ns.props.id, true + ) + const last_comment = ns.props.comments[ns.props.comments.length - 1].text + return [link, ns.status, last_comment] + }) + ).SetClass("zebra-table link-underline"), + + + new Title("Mass apply an action"), + new MassAction(state, noteStates.map(ns => ns.props)).SetClass("block")]).SetClass("flex flex-col")) + } +} + class ImportInspector extends VariableUiElement { constructor(userDetails: UserDetails, state: UserRelatedState) { @@ -122,61 +173,86 @@ class ImportInspector extends VariableUiElement { 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 props: NoteProperties[] = notes["success"].features.map(f => f.properties) + const perBatch: NoteState[][] = Array.from(ImportInspector.SplitNotesIntoBatches(props).values()); + const els: Toggleable[] = perBatch.map(noteStates => new BatchView(state, noteStates)) - 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) + const accordeon = new Accordeon(els) + const content = new Combine([ + new Title(Translations.t.importInspector.title, 1), + new SubtleButton(undefined, "Create a new batch of imports",{url:'import_helper.html'}), + accordeon]) + return new LeftIndex( + [new TableOfContents(content, {noTopLevel: true, maxDepth: 1}).SetClass("subtle")], + content + ) })); } + /** + * Creates distinct batches of note, where 'date', 'intro' and 'theme' are identical + */ + private static SplitNotesIntoBatches(props: NoteProperties[]): Map { + 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 date = Utils.ParseDate(prop.date_created) + const dateStr = date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate() + const key = theme + lines[0] + dateStr + if (!perBatch.has(key)) { + perBatch.set(key, []) + } + let status: "open" | "closed" | "imported" | "invalid" | "already_mapped" | "not_found" = "open" + if (prop.closed_at !== undefined) { + const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase() + if (lastComment.indexOf("does not exist") >= 0) { + status = "not_found" + } else if (lastComment.indexOf("already mapped") >= 0) { + status = "already_mapped" + } else if (lastComment.indexOf("invalid") >= 0 || lastComment.indexOf("incorrecto") >= 0) { + status = "invalid" + } else if (lastComment.indexOf("imported") >= 0) { + status = "imported" + } else { + status = "closed" + } + } + + perBatch.get(key).push({ + props: prop, + intro: lines[0], + theme, + dateStr, + status + }) + } + return perBatch; + } } -export default class ImportInspectorGui extends Combine { +class ImportViewerGui 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 Combine([new LoginButton("Login to inspect your import flows", state), + new BackToIndex() + ]) } return new ImportInspector(ud, state); })) ]); } +} - -} \ No newline at end of file +new ImportViewerGui().AttachTo("main") \ No newline at end of file diff --git a/UI/ProfessionalGui.ts b/UI/ProfessionalGui.ts index d9a83cd7e..45a744e54 100644 --- a/UI/ProfessionalGui.ts +++ b/UI/ProfessionalGui.ts @@ -8,6 +8,7 @@ import BaseUIElement from "./BaseUIElement"; import LanguagePicker from "./LanguagePicker"; import TableOfContents from "./Base/TableOfContents"; import BackToIndex from "./BigComponents/BackToIndex"; +import LeftIndex from "./Base/LeftIndex"; class Snippet extends Toggleable { constructor(translations, ...extraContent: BaseUIElement[]) { @@ -39,7 +40,7 @@ class SnippetContent extends Combine { } } -class ProfessionalGui { +class ProfessionalGui extends LeftIndex{ constructor() { @@ -93,7 +94,6 @@ class ProfessionalGui { const leftContents: BaseUIElement[] = [ - new BackToIndex().SetClass("block"), new TableOfContents(content, { noTopLevel: true, maxDepth: 2 @@ -102,10 +102,7 @@ class ProfessionalGui { LanguagePicker.CreateLanguagePicker(Translations.t.professional.title.SupportedLanguages())?.SetClass("mt-4 self-end flex-col"), ].map(el => el?.SetClass("pl-4")) - const leftBar = new Combine([ - new Combine(leftContents).SetClass("sticky top-4 m-4") - ]).SetClass("block w-full md:w-2/6 lg:w-1/6") - new Combine([leftBar, content]).SetClass("block md:flex").AttachTo("main") + super(leftContents, content) } @@ -113,4 +110,4 @@ class ProfessionalGui { } new FixedUiElement("").AttachTo("decoration-desktop") -new ProfessionalGui() \ No newline at end of file +new ProfessionalGui().AttachTo("main") \ No newline at end of file diff --git a/Utils.ts b/Utils.ts index 25f898575..64a9c66e9 100644 --- a/Utils.ts +++ b/Utils.ts @@ -625,6 +625,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } return JSON.parse(JSON.stringify(x)); } + + public static ParseDate(str: string): Date{ + if (str.endsWith(" UTC")) { + str = str.replace(" UTC", "+00") + } + return new Date(str) + } private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 9aa741e1c..9451522e5 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -752,14 +752,14 @@ video { bottom: 0px; } -.top-4 { - top: 1rem; -} - .right-1\/3 { right: 33.333333%; } +.top-4 { + top: 1rem; +} + .top-0 { top: 0px; } @@ -872,6 +872,14 @@ video { margin-top: 0.25rem; } +.ml-4 { + margin-left: 1rem; +} + +.mb-24 { + margin-bottom: 6rem; +} + .mr-4 { margin-right: 1rem; } @@ -912,10 +920,6 @@ video { margin-left: 0.25rem; } -.mb-24 { - margin-bottom: 6rem; -} - .mr-0 { margin-right: 0px; } @@ -1182,10 +1186,6 @@ video { flex-grow: 1; } -.table-auto { - table-layout: auto; -} - .border-collapse { border-collapse: collapse; } diff --git a/langs/nl.json b/langs/nl.json index 1dfaa6eb8..58aed08aa 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -319,7 +319,10 @@ "layerName": "Hier is misschien een {title}", "description": "Deze laag toont kaart-nota's die wijzen op een {title}", "popupTitle": "Mogelijkse {title}", - "importButton": "import_button({layerId}, _tags, Hier is een {title}, voeg toe...,./assets/svg/addSmall.svg,,,id)", - "importHandled": "
Dit punt is afgehandeld. Bedankt om mee te helpen!
" + "importButton": "import_button({layerId}, _tags, Ik heb hier een {title} gevonden - voeg deze toe aan de kaart...,./assets/svg/addSmall.svg,,,id)", + "importHandled": "
Dit punt is afgehandeld. Bedankt om mee te helpen!
", + + "notFound": "Ik kon geen {title} vinden hier - verwijder deze van de kaart", + "alreadyMapped": "Er staat hier reeds een {title} op de kaart; dit punt is een duplicaat. Verwijder deze van de kaart" } } diff --git a/package.json b/package.json index 1818e3965..2f843f034 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "prepare-deploy": "./scripts/build.sh", "gittag": "ts-node scripts/printVersion.ts | bash", "lint": "tslint --project . -c tslint.json '**.ts' ", - "clean": "rm -rf .cache/ && (find *.html | grep -v \"\\(404\\|index\\|land\\|test\\|preferences\\|customGenerator\\|professional\\|automaton\\|import_helper\\|theme\\).html\" | xargs rm) && (ls | grep \"^index_[a-zA-Z_]\\+\\.ts$\" | xargs rm) && (ls | grep \".*.webmanifest$\" | xargs rm)", + "clean": "rm -rf .cache/ && (find *.html | grep -v \"\\(404\\|index\\|land\\|test\\|preferences\\|customGenerator\\|professional\\|automaton\\|import_helper\\|import_viewer\\|theme\\).html\" | xargs rm) && (ls | grep \"^index_[a-zA-Z_]\\+\\.ts$\" | xargs rm) && (ls | grep \".*.webmanifest$\" | xargs rm)", "generate:dependency-graph": "node_modules/.bin/depcruise --exclude \"^node_modules\" --output-type dot Logic/State/MapState.ts > dependencies.dot && dot dependencies.dot -T svg -o dependencies.svg && rm dependencies.dot", "bicycle_rental": "ts-node ./scripts/extractBikeRental.ts" }, diff --git a/scripts/build.sh b/scripts/build.sh index b4714b267..99c8678ee 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -37,7 +37,7 @@ fi echo -e "\n\n Building non-theme pages" echo -e " ==========================\n\n" -parcel build --public-url "./" $SRC_MAPS "index.html" "404.html" "professional.html" "automaton.html" "import_helper.html" "land.html" "customGenerator.html" "theme.html" vendor +parcel build --public-url "./" $SRC_MAPS "index.html" "404.html" "professional.html" "automaton.html" "import_helper.html" "import_viewer.html" "land.html" "customGenerator.html" "theme.html" vendor echo -e "\n\n Building theme pages" echo -e " ======================\n\n" diff --git a/test.ts b/test.ts index 6ff12256b..e69de29bb 100644 --- a/test.ts +++ b/test.ts @@ -1,4 +0,0 @@ -import ImportInspectorGui from "./UI/ImportFlow/ImportInspector"; - -new ImportInspectorGui().AttachTo("maindiv") -