mapcomplete/UI/ImportFlow/ImportViewerGui.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

802 lines
29 KiB
TypeScript
Raw Normal View History

2022-01-22 04:01:13 +01:00
import Combine from "../Base/Combine"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Utils } from "../../Utils"
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 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, { ClickableToggle } from "../Input/Toggle"
2022-01-24 03:09:21 +01:00
import Table from "../Base/Table"
import LeftIndex from "../Base/LeftIndex"
import Toggleable, { Accordeon } from "../Base/Toggleable"
import TableOfContents from "../Base/TableOfContents"
2022-02-15 15:42:09 +01:00
import { LoginToggle } from "../Popup/LoginButton"
2022-01-25 21:55:51 +01:00
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import Lazy from "../Base/Lazy"
import { Button } from "../Base/Button"
2023-02-24 20:17:31 +01:00
import ChartJs from "../Base/ChartJs"
2022-01-22 04:01:13 +01:00
interface NoteProperties {
id: number
url: string
2022-01-24 03:09:21 +01:00
date_created: string
closed_at?: string
2022-01-22 04:01:13 +01:00
status: "open" | "closed"
comments: {
date: string
uid: number
user: string
2022-01-25 21:55:51 +01:00
text: string
html: string
2022-01-22 04:01:13 +01:00
}[]
}
2022-01-24 03:09:21 +01:00
interface NoteState {
props: NoteProperties
theme: string
intro: string
dateStr: string
2022-01-25 21:55:51 +01:00
status:
| "imported"
| "already_mapped"
| "invalid"
| "closed"
| "not_found"
| "open"
| "has_comments"
2022-01-24 03:09:21 +01:00
}
2022-01-22 04:01:13 +01:00
2022-04-23 02:14:31 +02:00
class DownloadStatisticsButton extends SubtleButton {
constructor(states: NoteState[][]) {
super(Svg.statistics_svg(), "Download statistics")
this.onClick(() => {
const st: NoteState[] = [].concat(...states)
2022-04-23 02:14:31 +02:00
const fields = [
"id",
"status",
"theme",
"date_created",
"date_closed",
"days_open",
"intro",
"...comments",
]
const values: string[][] = st.map((note) => {
return [
note.props.id + "",
2022-04-23 02:14:31 +02:00
note.status,
note.theme,
note.props.date_created?.substr(0, note.props.date_created.length - 3),
note.props.closed_at?.substr(0, note.props.closed_at.length - 3) ?? "",
JSON.stringify(note.intro),
...note.props.comments.map(
(c) => JSON.stringify(c.user) + ": " + JSON.stringify(c.text)
2022-09-08 21:40:48 +02:00
),
2022-04-23 02:14:31 +02:00
]
})
2022-04-23 02:14:31 +02:00
Utils.offerContentsAsDownloadableFile(
[fields, ...values].map((c) => c.join(", ")).join("\n"),
"mapcomplete_import_notes_overview.csv",
{
mimetype: "text/csv",
}
)
})
}
}
2022-01-24 03:09:21 +01:00
class MassAction extends Combine {
2022-01-22 04:01:13 +01:00
constructor(state: UserRelatedState, props: NoteProperties[]) {
const textField = ValidatedTextField.ForType("text").ConstructInputElement()
2022-01-22 04:01:13 +01:00
const actions = new DropDown<{
predicate: (p: NoteProperties) => boolean
action: (p: NoteProperties) => Promise<void>
}>("On which notes should an action be performed?", [
{
value: undefined,
shown: <string | BaseUIElement>"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",
2022-01-24 03:09:21 +01:00
},
{
value: {
predicate: (p) => p.status === "open",
action: async (p) => {
const txt = textField.GetValue().data
state.osmConnection.addCommentToNote(p.id, txt)
2022-01-24 03:09:21 +01:00
},
},
shown: "Add comment to every open note",
2022-02-11 02:40:23 +01:00
},
/*
{
2022-11-16 01:10:13 +01:00
// This was a one-off for one of the first imports
2022-02-11 02:40:23 +01:00
value:{
predicate: p => p.status === "open" && p.comments[0].text.split("\n").find(l => l.startsWith("note=")) !== undefined,
action: async p => {
const note = p.comments[0].text.split("\n").find(l => l.startsWith("note=")).substr("note=".length)
state.osmConnection.addCommentToNode(p.id, note)
}
},
shown:"On every open note, read the 'note='-tag and and this note as comment. (This action ignores the textfield)"
},//*/
2022-01-22 04:01:13 +01:00
])
const handledNotesCounter = new UIEventSource<number>(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)
2022-09-08 21:40:48 +02:00
}
2022-01-22 04:01:13 +01:00
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
2022-09-08 21:40:48 +02:00
)
2022-01-22 04:01:13 +01:00
})
2022-09-08 21:40:48 +02:00
)
2022-01-22 04:01:13 +01:00
),
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)
2022-02-15 15:42:09 +01:00
),
2022-09-08 21:40:48 +02:00
2022-02-15 15:42:09 +01:00
new VariableUiElement(
textField
.GetValue()
.map(
(txt) =>
"Type a text of at least 15 characters to apply the action. Currently, there are " +
(txt?.length ?? 0) +
" characters"
2022-09-08 21:40:48 +02:00
)
2022-02-15 15:42:09 +01:00
).SetClass("alert"),
2022-01-22 04:01:13 +01:00
actions
.GetValue()
.map(
(v) => v !== undefined && textField.GetValue()?.data?.length > 15,
[textField.GetValue()]
2022-09-08 21:40:48 +02:00
)
2022-01-22 04:01:13 +01:00
),
new Toggle(
2022-01-24 03:09:21 +01:00
new FixedUiElement("Testmode enable").SetClass("alert"),
undefined,
2022-01-22 04:01:13 +01:00
state.featureSwitchIsTesting
),
])
}
}
2023-02-24 20:17:31 +01:00
class Statistics extends Combine {
2023-03-01 04:39:31 +01:00
private static r() {
return Math.floor(Math.random() * 256)
}
private static randomColour(): string {
return "rgba(" + Statistics.r() + "," + Statistics.r() + "," + Statistics.r() + ")"
}
private static CreatePieByAuthor(closed_by: Record<string, number[]>): ChartJs {
const importers = Object.keys(closed_by)
importers.sort((a, b) => closed_by[b].at(-1) - closed_by[a].at(-1))
return new ChartJs(<any>{
type: "doughnut",
data: {
labels: importers,
datasets: [
{
label: "Closed by",
data: importers.map((k) => closed_by[k].at(-1)),
backgroundColor: importers.map((_) => Statistics.randomColour()),
},
],
},
})
}
private static CreateStatePie(noteStates: NoteState[]) {
const colors = {
imported: "#0aa323",
already_mapped: "#00bbff",
invalid: "#ff0000",
closed: "#000000",
not_found: "#ff6d00",
open: "#626262",
has_comments: "#a8a8a8",
}
const knownStates = Object.keys(colors)
const byState = knownStates.map(
(targetState) => noteStates.filter((ns) => ns.status === targetState).length
)
return new ChartJs(<any>{
type: "doughnut",
data: {
labels: knownStates.map(
(state, i) =>
state + " " + Math.floor((100 * byState[i]) / noteStates.length) + "%"
),
datasets: [
{
label: "Status by",
data: byState,
backgroundColor: knownStates.map((state) => colors[state]),
},
],
},
})
}
2023-02-24 20:17:31 +01:00
constructor(noteStates: NoteState[]) {
if (noteStates.length === 0) {
super([])
return
}
// We assume all notes are created at the same time
let dateOpened = new Date(noteStates[0].dateStr)
for (const noteState of noteStates) {
const openDate = new Date(noteState.dateStr)
if (openDate < dateOpened) {
dateOpened = openDate
}
}
const today = new Date()
const daysBetween = (today.getTime() - dateOpened.getTime()) / (1000 * 60 * 60 * 24)
const ranges = {
dates: [],
is_open: [],
}
const closed_by: Record<string, number[]> = {}
for (const noteState of noteStates) {
const closing_user = noteState.props.comments.at(-1).user
if (closed_by[closing_user] === undefined) {
closed_by[closing_user] = []
}
}
for (let i = -1; i < daysBetween; i++) {
const dt = new Date(dateOpened.getTime() + 24 * 60 * 60 * 1000 * i)
let open_count = 0
for (const closing_user in closed_by) {
closed_by[closing_user].push(0)
}
for (const noteState of noteStates) {
const openDate = new Date(noteState.dateStr)
if (openDate > dt) {
// Not created at this point
continue
}
if (noteState.props.closed_at === undefined) {
open_count++
} else if (
new Date(noteState.props.closed_at.substring(0, 10)).getTime() > dt.getTime()
) {
open_count++
} else {
const closing_user = noteState.props.comments.at(-1).user
const user_count = closed_by[closing_user]
user_count[user_count.length - 1] += 1
}
}
ranges.dates.push(
new Date(dateOpened.getTime() + i * 1000 * 60 * 60 * 24)
.toISOString()
.substring(0, 10)
)
ranges.is_open.push(open_count)
}
const labels = ranges.dates.map((i) => "" + i)
const data = {
labels: labels,
datasets: [
{
label: "Total open",
data: ranges.is_open,
fill: false,
borderColor: "rgb(75, 192, 192)",
tension: 0.1,
},
],
}
for (const closing_user in closed_by) {
if (closed_by[closing_user].at(-1) <= 10) {
continue
}
data.datasets.push({
label: "Closed by " + closing_user,
data: closed_by[closing_user],
fill: false,
2023-03-01 04:39:31 +01:00
borderColor: Statistics.randomColour(),
2023-02-24 20:17:31 +01:00
tension: 0.1,
})
}
2023-03-01 04:39:31 +01:00
2023-02-24 20:17:31 +01:00
super([
new ChartJs({
type: "line",
data,
options: {
2023-03-01 04:39:31 +01:00
scales: <any>{
2023-02-24 20:17:31 +01:00
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
}),
2023-03-01 04:39:31 +01:00
new Combine([
Statistics.CreatePieByAuthor(closed_by),
Statistics.CreateStatePie(noteStates),
])
.SetClass("flex w-full h-32")
.SetStyle("width: 40rem"),
2023-02-24 20:17:31 +01:00
])
this.SetClass("block w-full h-64 border border-red")
}
}
class NoteTable extends Combine {
private static individualActions: [() => BaseUIElement, string][] = [
[Svg.not_found_svg, "This feature does not exist"],
[Svg.addSmall_svg, "imported"],
[Svg.duplicate_svg, "Already mapped"],
]
constructor(noteStates: NoteState[], state?: UserRelatedState) {
const typicalComment = noteStates[0].props.comments[0].html
const table = new Table(
["id", "status", "last comment", "last modified by", "actions"],
noteStates.map((ns) => NoteTable.noteField(ns, state)),
{ sortable: true }
).SetClass("zebra-table link-underline")
super([
new Title("Mass apply an action on " + noteStates.length + " notes below"),
state !== undefined
? new MassAction(
state,
noteStates.map((ns) => ns.props)
).SetClass("block")
: undefined,
table,
new Title("Example note", 4),
new FixedUiElement(typicalComment).SetClass("literal-code link-underline"),
])
this.SetClass("flex flex-col")
}
private static noteField(ns: NoteState, state: UserRelatedState) {
const link = new Link(
"" + ns.props.id,
"https://openstreetmap.org/note/" + ns.props.id,
true
)
let last_comment = ""
const last_comment_props = ns.props.comments[ns.props.comments.length - 1]
const before_last_comment = ns.props.comments[ns.props.comments.length - 2]
if (ns.props.comments.length > 1) {
last_comment = last_comment_props.text
if (last_comment === undefined && before_last_comment?.uid === last_comment_props.uid) {
last_comment = before_last_comment.text
}
}
const statusIcon = BatchView.icons[ns.status]().SetClass("h-4 w-4 shrink-0")
const togglestate = new UIEventSource(false)
const changed = new UIEventSource<string>(undefined)
2022-09-08 21:40:48 +02:00
const lazyButtons = new Lazy(() =>
new Combine(
this.individualActions.map(([img, text]) =>
img()
.onClick(async () => {
if (ns.props.status === "closed") {
await state.osmConnection.reopenNote(ns.props.id)
}
await state.osmConnection.closeNote(ns.props.id, text)
changed.setData(text)
})
.SetClass("h-8 w-8")
2022-09-08 21:40:48 +02:00
)
).SetClass("flex")
2022-09-08 21:40:48 +02:00
)
const appliedButtons = new VariableUiElement(
changed.map((currentState) => (currentState === undefined ? lazyButtons : currentState))
2022-09-08 21:40:48 +02:00
)
const buttons = Toggle.If(
state?.osmConnection?.isLoggedIn,
() =>
new ClickableToggle(
appliedButtons,
new Button("edit...", () => {
console.log("Enabling...")
togglestate.setData(true)
}),
togglestate
2022-09-08 21:40:48 +02:00
)
)
return [
link,
new Combine([statusIcon, ns.status]).SetClass("flex"),
last_comment,
new Link(
last_comment_props.user,
"https://www.openstreetmap.org/user/" + last_comment_props.user,
true
),
buttons,
]
}
}
2022-01-24 03:09:21 +01:00
class BatchView extends Toggleable {
public static icons = {
2022-01-25 21:55:51 +01:00
open: Svg.compass_svg,
has_comments: Svg.speech_bubble_svg,
imported: Svg.addSmall_svg,
already_mapped: Svg.checkmark_svg,
not_found: Svg.not_found_svg,
closed: Svg.close_svg,
invalid: Svg.invalid_svg,
2022-01-25 21:55:51 +01:00
}
constructor(noteStates: NoteState[], state?: UserRelatedState) {
noteStates.sort((a, b) => a.props.id - b.props.id)
2022-01-24 03:09:21 +01:00
const { theme, intro, dateStr } = noteStates[0]
2022-01-25 21:55:51 +01:00
const statusHist = new Map<string, number>()
for (const noteState of noteStates) {
const st = noteState.status
const c = statusHist.get(st) ?? 0
statusHist.set(st, c + 1)
}
const unresolvedTotal =
(statusHist.get("open") ?? 0) + (statusHist.get("has_comments") ?? 0)
const badges: BaseUIElement[] = [
new FixedUiElement(dateStr).SetClass("literal-code rounded-full"),
new FixedUiElement(noteStates.length + " total")
.SetClass("literal-code rounded-full ml-1 border-4 border-gray")
.onClick(() => filterOn.setData(undefined)),
unresolvedTotal === 0
? new Combine([Svg.party_svg().SetClass("h-6 m-1"), "All done!"]).SetClass(
"flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black"
)
: new FixedUiElement(
Math.round(100 - (100 * unresolvedTotal) / noteStates.length) + "%"
).SetClass("literal-code rounded-full ml-1"),
]
const filterOn = new UIEventSource<string>(undefined)
Object.keys(BatchView.icons).forEach((status) => {
const count = statusHist.get(status)
if (count === undefined) {
return undefined
}
const normal = new Combine([
BatchView.icons[status]().SetClass("h-6 m-1"),
count + " " + status,
]).SetClass("flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border border-black")
const selected = new Combine([
BatchView.icons[status]().SetClass("h-6 m-1"),
count + " " + status,
]).SetClass(
"flex ml-1 mb-1 pl-1 pr-3 items-center rounded-full border-4 border-black animate-pulse"
)
const toggle = new ClickableToggle(
selected,
normal,
filterOn.sync(
(f) => f === status,
[],
(selected, previous) => {
if (selected) {
return status
}
if (previous === status) {
return undefined
}
return previous
}
2022-09-08 21:40:48 +02:00
)
).ToggleOnClick()
badges.push(toggle)
2022-01-25 21:55:51 +01:00
})
2022-01-24 03:09:21 +01:00
2023-02-24 20:17:31 +01:00
const fullTable = new Combine([
new NoteTable(noteStates, state),
new Statistics(noteStates),
])
2022-01-25 21:55:51 +01:00
super(
new Combine([
new Title(theme + ": " + intro, 2),
new Combine(badges).SetClass("flex flex-wrap"),
]),
2023-02-24 20:17:31 +01:00
new VariableUiElement(
filterOn.map((filter) => {
if (filter === undefined) {
return fullTable
}
2023-02-24 20:17:31 +01:00
const notes = noteStates.filter((ns) => ns.status === filter)
return new Combine([new NoteTable(notes, state), new Statistics(notes)])
})
),
2022-01-25 21:55:51 +01:00
{
closeOnClick: false,
}
)
2022-01-24 03:09:21 +01:00
}
}
2022-01-22 04:01:13 +01:00
class ImportInspector extends VariableUiElement {
2022-01-25 21:55:51 +01:00
constructor(
userDetails: { uid: number } | { display_name: string; search?: string },
state: UserRelatedState
) {
let url
2022-02-15 15:42:09 +01:00
2022-01-25 21:55:51 +01:00
if (userDetails["uid"] !== undefined) {
url =
"https://api.openstreetmap.org/api/0.6/notes/search.json?user=" +
userDetails["uid"] +
"&closed=730&limit=10000&sort=created_at&q=%23import"
2022-01-25 21:55:51 +01:00
} else {
url =
"https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" +
encodeURIComponent(userDetails["display_name"]) +
2023-02-24 20:17:31 +01:00
"&limit=10000&closed=730&sort=created_at&q="
if (userDetails["search"] !== "") {
url += userDetails["search"]
} else {
url += "#import"
}
2022-01-25 21:55:51 +01:00
}
2022-01-22 04:01:13 +01:00
const notes: UIEventSource<
{ error: string } | { success: { features: { properties: NoteProperties }[] } }
> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url))
2022-09-08 21:40:48 +02:00
super(
2022-01-24 03:09:21 +01:00
notes.map((notes) => {
2022-01-22 04:01:13 +01:00
if (notes === undefined) {
2022-01-25 21:55:51 +01:00
return new Loading("Loading notes which mention '#import'")
2022-09-08 21:40:48 +02:00
}
2022-01-22 04:01:13 +01:00
if (notes["error"] !== undefined) {
return new FixedUiElement("Something went wrong: " + notes["error"]).SetClass(
2022-09-08 21:40:48 +02:00
"alert"
)
}
2022-01-22 04:01:13 +01:00
// We only care about the properties here
2022-11-16 01:10:13 +01:00
let props: NoteProperties[] = notes["success"].features.map((f) => f.properties)
2022-12-08 03:01:08 +01:00
if (userDetails["uid"]) {
props = props.filter((n) => n.comments[0].uid === userDetails["uid"])
2022-11-16 01:10:13 +01:00
}
2023-02-24 20:17:31 +01:00
if (userDetails["display_name"] !== undefined) {
const display_name = <string>userDetails["display_name"]
props = props.filter((n) => n.comments[0].user === display_name)
}
2022-01-24 03:09:21 +01:00
const perBatch: NoteState[][] = Array.from(
ImportInspector.SplitNotesIntoBatches(props).values()
2022-09-08 21:40:48 +02:00
)
2022-01-25 21:55:51 +01:00
const els: Toggleable[] = perBatch.map(
(noteStates) => new BatchView(noteStates, state)
2022-09-08 21:40:48 +02:00
)
2022-01-22 04:01:13 +01:00
const accordeon = new Accordeon(els)
2022-01-25 21:55:51 +01:00
let contents = []
if (state?.osmConnection?.isLoggedIn?.data) {
contents = [
new Title(Translations.t.importInspector.title, 1),
new SubtleButton(undefined, "Create a new batch of imports", {
url: "import_helper.html",
}),
]
}
contents.push(accordeon)
2023-02-24 20:17:31 +01:00
contents.push(
new Combine([
new Title("Statistics for all notes"),
new Statistics([].concat(...perBatch)),
])
)
2022-01-25 21:55:51 +01:00
const content = new Combine(contents)
2022-01-24 03:09:21 +01:00
return new LeftIndex(
2022-04-23 02:14:31 +02:00
[
new TableOfContents(content, { noTopLevel: true, maxDepth: 1 }).SetClass(
"subtle"
),
new DownloadStatisticsButton(perBatch),
2022-04-23 02:14:31 +02:00
],
2022-01-24 03:09:21 +01:00
content
)
2022-01-22 04:01:13 +01:00
})
2022-09-08 21:40:48 +02:00
)
2022-01-22 04:01:13 +01:00
}
2022-01-24 03:09:21 +01:00
/**
* Creates distinct batches of note, where 'date', 'intro' and 'theme' are identical
*/
private static SplitNotesIntoBatches(props: NoteProperties[]): Map<string, NoteState[]> {
const perBatch = new Map<string, NoteState[]>()
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)
2022-01-25 21:55:51 +01:00
const dateStr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()
2022-01-24 03:09:21 +01:00
const key = theme + lines[0] + dateStr
if (!perBatch.has(key)) {
perBatch.set(key, [])
}
2022-01-25 21:55:51 +01:00
let status:
| "open"
| "closed"
| "imported"
| "invalid"
| "already_mapped"
| "not_found"
| "has_comments" = "open"
2023-03-01 04:39:31 +01:00
function has(keywords: string[], comment: string): boolean {
return keywords.some((keyword) => comment.toLowerCase().indexOf(keyword) >= 0)
}
2022-01-24 03:09:21 +01:00
if (prop.closed_at !== undefined) {
const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase()
2023-03-01 04:39:31 +01:00
if (has(["does not exist", "bestaat niet", "geen"], lastComment)) {
2022-01-24 03:09:21 +01:00
status = "not_found"
2023-03-01 04:39:31 +01:00
} else if (
has(
[
"already mapped",
"reeds",
"dubbele note",
"stond er al",
"stonden er al",
"staat er al",
"staan er al",
"stond al",
"stonden al",
"staat al",
"staan al",
],
lastComment
)
) {
2022-01-24 03:09:21 +01:00
status = "already_mapped"
} else if (
lastComment.indexOf("invalid") >= 0 ||
2023-03-01 04:39:31 +01:00
lastComment.indexOf("incorrect") >= 0
2022-01-24 03:09:21 +01:00
) {
status = "invalid"
} else if (
2023-03-01 04:39:31 +01:00
has(
[
"imported",
"erbij",
"toegevoegd",
"added",
"gemapped",
"gemapt",
"mapped",
"done",
"openstreetmap.org/changeset",
],
lastComment
)
) {
2022-01-24 03:09:21 +01:00
status = "imported"
} else {
status = "closed"
}
2022-01-25 21:55:51 +01:00
} else if (prop.comments.length > 1) {
status = "has_comments"
2022-01-24 03:09:21 +01:00
}
perBatch.get(key).push({
props: prop,
intro: lines[0],
theme,
dateStr,
status,
2022-01-24 03:09:21 +01:00
})
}
return perBatch
}
2022-01-22 04:01:13 +01:00
}
2022-02-15 15:42:09 +01:00
class ImportViewerGui extends LoginToggle {
2022-01-22 04:01:13 +01:00
constructor() {
const state = new UserRelatedState(undefined)
2022-01-25 21:55:51 +01:00
const displayNameParam = QueryParameters.GetQueryParameter(
"user",
"",
"The username of the person whom you want to see the notes for"
)
const searchParam = QueryParameters.GetQueryParameter(
"search",
"",
"A text that should be included in the first comment of the note to be shown"
2022-09-08 21:40:48 +02:00
)
2022-02-15 15:42:09 +01:00
super(
2022-01-22 04:01:13 +01:00
new VariableUiElement(
state.osmConnection.userDetails.map(
(ud) => {
2022-01-25 21:55:51 +01:00
const display_name = displayNameParam.data
const search = searchParam.data
2023-02-24 20:17:31 +01:00
if (display_name !== "" || search !== "") {
2022-01-27 20:15:28 +01:00
return new ImportInspector({ display_name, search }, undefined)
2022-01-25 21:55:51 +01:00
}
2022-01-22 04:01:13 +01:00
return new ImportInspector(ud, state)
2022-02-15 15:42:09 +01:00
},
[displayNameParam, searchParam]
2022-09-08 21:40:48 +02:00
)
2022-02-15 15:42:09 +01:00
),
"Login to inspect your import flows",
state
)
2022-01-22 04:01:13 +01:00
}
2022-01-24 03:09:21 +01:00
}
2022-01-22 04:01:13 +01:00
2022-01-24 03:09:21 +01:00
new ImportViewerGui().AttachTo("main")