From c8a6eb4c7272fb9660a3e68046e52a10d2ddda5d Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Sun, 25 Oct 2020 13:37:35 -0700 Subject: [PATCH] Show confirm close alert for unsaved changes --- ui/config-page.ts | 199 ++++++++++++++++++---------------- ui/files-page.ts | 67 ++++++------ ui/overlay.ts | 19 +++- ui/permissions-page.ts | 67 ++++++------ ui/presets-page.ts | 117 +++++++++++--------- ui/provisions-page.ts | 116 +++++++++++--------- ui/put-form-component.ts | 7 ++ ui/ui-config-component.ts | 1 + ui/users-page.ts | 151 ++++++++++++++------------ ui/virtual-parameters-page.ts | 116 +++++++++++--------- 10 files changed, 472 insertions(+), 388 deletions(-) diff --git a/ui/config-page.ts b/ui/config-page.ts index 241b776..725ea91 100644 --- a/ui/config-page.ts +++ b/ui/config-page.ts @@ -147,37 +147,42 @@ function renderTable(confsResponse, searchString): Children { { title: "Edit config value", onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - base: conf, - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, false) - .then((errors) => { - const ErrorList = errors ? Object.values(errors) : []; - if (ErrorList.length) { - for (const err of ErrorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + base: conf, + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, false) + .then((errors) => { + const ErrorList = errors ? Object.values(errors) : []; + if (ErrorList.length) { + for (const err of ErrorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); }, - formData - ) - ); - }; - overlay.open(cb); + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, getIcon("edit") @@ -244,38 +249,43 @@ export const component: ClosureComponent = (): Component => { { title: "Create new config", onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, true) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, true) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); }, - formData - ) - ); - }; - overlay.open(cb); + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New config" @@ -307,39 +317,44 @@ export const component: ClosureComponent = (): Component => { "button", { onclick: () => { - const cb = (): Children => { - return m( - uiConfigComponent, - Object.assign( - { - onUpdate: (errs: string[]) => { - const errors = errs ? Object.values(errs) : []; - if (errors.length) { - for (const err of errors) - notifications.push("error", err); - } else { - notifications.push( - "success", - `${sub.name.replace( - /^[a-z]/, - sub.name[0].toUpperCase() - )} config updated` - ); - overlay.close(cb); - } - store.fulfill(0, Date.now()); - }, - onError: (err) => { - notifications.push("error", err.message); - store.fulfill(0, Date.now()); + let cb: () => Children = null; + const comp = m( + uiConfigComponent, + Object.assign( + { + onUpdate: (errs: string[]) => { + const errors = errs ? Object.values(errs) : []; + if (errors.length) { + for (const err of errors) + notifications.push("error", err); + } else { + notifications.push( + "success", + `${sub.name.replace( + /^[a-z]/, + sub.name[0].toUpperCase() + )} config updated` + ); overlay.close(cb); - }, + } + store.fulfill(0, Date.now()); }, - attrs - ) - ); - }; - overlay.open(cb); + onError: (err) => { + notifications.push("error", err.message); + store.fulfill(0, Date.now()); + overlay.close(cb); + }, + }, + attrs + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, `Edit ${sub.name}` diff --git a/ui/files-page.ts b/ui/files-page.ts index 248f301..229c35c 100644 --- a/ui/files-page.ts +++ b/ui/files-page.ts @@ -221,38 +221,43 @@ export const component: ClosureComponent = (): Component => { { title: "Create new file", onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); }, - formData - ) - ); - }; - overlay.open(cb); + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New" diff --git a/ui/overlay.ts b/ui/overlay.ts index 519ba0c..3b57405 100644 --- a/ui/overlay.ts +++ b/ui/overlay.ts @@ -20,16 +20,24 @@ import m, { Children } from "mithril"; type OverlayCallback = () => Children; +type CloseCallback = () => boolean; let overlayCallback: OverlayCallback = null; +let closeCallback: CloseCallback = null; -export function open(callback: OverlayCallback): void { +export function open( + callback: OverlayCallback, + closeCb: CloseCallback = null +): void { overlayCallback = callback; + closeCallback = closeCb; } -export function close(callback: OverlayCallback): boolean { +export function close(callback: OverlayCallback, force = true): boolean { if (callback === overlayCallback) { + if (!force && closeCallback && !closeCallback()) return false; overlayCallback = null; + closeCallback = null; return true; } @@ -43,7 +51,7 @@ export function render(): Children { { tabindex: 0, onclick: () => { - close(overlayCallback); + close(overlayCallback, false); }, style: "opacity: 0", oncreate: (vnode) => { @@ -75,9 +83,10 @@ export function render(): Children { } document.addEventListener("keydown", (e) => { - if (overlayCallback && e.keyCode === 27 && close(overlayCallback)) m.redraw(); + if (overlayCallback && e.keyCode === 27 && close(overlayCallback, false)) + m.redraw(); }); window.addEventListener("popstate", () => { - if (close(overlayCallback)) m.redraw(); + if (close(overlayCallback, false)) m.redraw(); }); diff --git a/ui/permissions-page.ts b/ui/permissions-page.ts index 1a3c70a..e4ac703 100644 --- a/ui/permissions-page.ts +++ b/ui/permissions-page.ts @@ -285,38 +285,43 @@ export const component: ClosureComponent = (): Component => { { title: "Create new permission", onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); }, - formData - ) - ); - }; - overlay.open(cb); + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New" diff --git a/ui/presets-page.ts b/ui/presets-page.ts index 52c7cd9..71d55ca 100644 --- a/ui/presets-page.ts +++ b/ui/presets-page.ts @@ -287,7 +287,38 @@ export const component: ClosureComponent = (): Component => { "a", { onclick: () => { - const cb = (): Children => { + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + base: preset, + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, false) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); + }, + }, + formData + ) + ); + cb = (): Children => { if (!preset.provision) { return m( "div", @@ -295,15 +326,37 @@ export const component: ClosureComponent = (): Component => { "This UI only supports presets with a single 'provision' configuraiton. If this preset was originally created from the old UI (genieacs-gui), you must edit it there." ); } + return comp; + }; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); + }, + }, + "Show" + ), + ]; + }; - return m( + if (window.authorizer.hasAccess("presets", 3)) { + attrs["actionsCallback"] = (selected: Set): Children => { + return [ + m( + "button.primary", + { + title: "Create new preset", + onclick: () => { + let cb: () => Children = null; + const comp = m( putFormComponent, Object.assign( { - base: preset, actionHandler: (action, object) => { return new Promise((resolve) => { - putActionHandler(action, object, false) + putActionHandler(action, object, true) .then((errors) => { const errorList = errors ? Object.values(errors) @@ -326,55 +379,13 @@ export const component: ClosureComponent = (): Component => { formData ) ); - }; - overlay.open(cb); - }, - }, - "Show" - ), - ]; - }; - - if (window.authorizer.hasAccess("presets", 3)) { - attrs["actionsCallback"] = (selected: Set): Children => { - return [ - m( - "button.primary", - { - title: "Create new preset", - onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, true) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, - }, - formData - ) - ); - }; - overlay.open(cb); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New" diff --git a/ui/provisions-page.ts b/ui/provisions-page.ts index c5688ae..30b7b5d 100644 --- a/ui/provisions-page.ts +++ b/ui/provisions-page.ts @@ -207,15 +207,67 @@ export const component: ClosureComponent = (): Component => { "a", { onclick: () => { - const cb = (): Children => { - return m( + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + base: provision, + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, false) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); + }, + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); + }, + }, + "Show" + ), + ]; + }; + + if (window.authorizer.hasAccess("provisions", 3)) { + attrs["actionsCallback"] = (selected: Set): Children => { + return [ + m( + "button.primary", + { + title: "Create new provision", + onclick: () => { + let cb: () => Children = null; + const comp = m( putFormComponent, Object.assign( { - base: provision, actionHandler: (action, object) => { return new Promise((resolve) => { - putActionHandler(action, object, false) + putActionHandler(action, object, true) .then((errors) => { const errorList = errors ? Object.values(errors) @@ -238,55 +290,13 @@ export const component: ClosureComponent = (): Component => { formData ) ); - }; - overlay.open(cb); - }, - }, - "Show" - ), - ]; - }; - - if (window.authorizer.hasAccess("provisions", 3)) { - attrs["actionsCallback"] = (selected: Set): Children => { - return [ - m( - "button.primary", - { - title: "Create new provision", - onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, true) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, - }, - formData - ) - ); - }; - overlay.open(cb); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New" diff --git a/ui/put-form-component.ts b/ui/put-form-component.ts index 569d257..514b973 100644 --- a/ui/put-form-component.ts +++ b/ui/put-form-component.ts @@ -56,6 +56,7 @@ function createField(current, attr, focus): Children { : null, onchange: (e) => { current.object[attr.id] = e.target.value; + current.modified = true; e.redraw = false; }, }, @@ -80,6 +81,7 @@ function createField(current, attr, focus): Children { if (e.target.checked) currentSelected.add(op); else currentSelected.delete(op); current.object[attr.id] = Array.from(currentSelected); + current.modified = true; e.redraw = false; }, }; @@ -98,6 +100,7 @@ function createField(current, attr, focus): Children { }, onChange: (value) => { current.object[attr.id] = value; + current.modified = true; }, }; return m(codeEditorComponent, attrs); @@ -112,6 +115,7 @@ function createField(current, attr, focus): Children { : null, onchange: (e) => { current.object[attr.id] = e.target.files; + current.modified = true; e.redraw = false; }, }); @@ -132,6 +136,7 @@ function createField(current, attr, focus): Children { : null, oninput: (e) => { current.object[attr.id] = e.target.value; + current.modified = true; e.redraw = false; }, onkeypress: (e) => { @@ -158,6 +163,7 @@ function createField(current, attr, focus): Children { : null, oninput: (e) => { current.object[attr.id] = e.target.value; + current.modified = true; e.redraw = false; }, }); @@ -174,6 +180,7 @@ const component: ClosureComponent = (): Component => { vnode.state["current"] = { isNew: !base["_id"], object: Object.assign({}, base), + modified: false, }; } diff --git a/ui/ui-config-component.ts b/ui/ui-config-component.ts index 5d7f4d9..caba6b0 100644 --- a/ui/ui-config-component.ts +++ b/ui/ui-config-component.ts @@ -120,6 +120,7 @@ const component: ClosureComponent = (): Component => { }, onChange: (value) => { vnode.state["updatedYaml"] = value; + vnode.state["modified"] = true; }, }; diff --git a/ui/users-page.ts b/ui/users-page.ts index 587f18e..15ade7f 100644 --- a/ui/users-page.ts +++ b/ui/users-page.ts @@ -237,46 +237,46 @@ export const component: ClosureComponent = (): Component => { "a", { onclick: () => { - const cb = (): Children => { - const children = [ - m( - putFormComponent, - Object.assign( - { - base: { - _id: user._id, - roles: user.roles.split(","), - }, - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, false) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + base: { + _id: user._id, + roles: user.roles.split(","), + }, + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, false) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); }); - }, - }, - { - resource: "users", - attributes: attributes, - } - ) - ), - ]; + }); + }, + }, + { + resource: "users", + attributes: attributes, + } + ) + ); + cb = () => { + const children: Children = [comp]; if (canWrite) { children.push(m("hr")); const _attrs = { @@ -292,7 +292,13 @@ export const component: ClosureComponent = (): Component => { return children; }; - overlay.open(cb); + + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "Show" @@ -317,38 +323,43 @@ export const component: ClosureComponent = (): Component => { { title: "Create new user", onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, true) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, true) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); }, - formData - ) - ); - }; - overlay.open(cb); + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New" diff --git a/ui/virtual-parameters-page.ts b/ui/virtual-parameters-page.ts index 326cc4c..94e3dfb 100644 --- a/ui/virtual-parameters-page.ts +++ b/ui/virtual-parameters-page.ts @@ -207,15 +207,67 @@ export const component: ClosureComponent = (): Component => { "a", { onclick: () => { - const cb = (): Children => { - return m( + let cb: () => Children = null; + const comp = m( + putFormComponent, + Object.assign( + { + base: virtualParameter, + actionHandler: (action, object) => { + return new Promise((resolve) => { + putActionHandler(action, object, false) + .then((errors) => { + const errorList = errors + ? Object.values(errors) + : []; + if (errorList.length) { + for (const err of errorList) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch((err) => { + notifications.push("error", err.message); + resolve(); + }); + }); + }, + }, + formData + ) + ); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); + }, + }, + "Show" + ), + ]; + }; + + if (window.authorizer.hasAccess("virtualParameters", 3)) { + attrs["actionsCallback"] = (selected: Set): Children => { + return [ + m( + "button.primary", + { + title: "Create new virtual parameter", + onclick: () => { + let cb: () => Children = null; + const comp = m( putFormComponent, Object.assign( { - base: virtualParameter, actionHandler: (action, object) => { return new Promise((resolve) => { - putActionHandler(action, object, false) + putActionHandler(action, object, true) .then((errors) => { const errorList = errors ? Object.values(errors) @@ -238,55 +290,13 @@ export const component: ClosureComponent = (): Component => { formData ) ); - }; - overlay.open(cb); - }, - }, - "Show" - ), - ]; - }; - - if (window.authorizer.hasAccess("virtualParameters", 3)) { - attrs["actionsCallback"] = (selected: Set): Children => { - return [ - m( - "button.primary", - { - title: "Create new virtual parameter", - onclick: () => { - const cb = (): Children => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise((resolve) => { - putActionHandler(action, object, true) - .then((errors) => { - const errorList = errors - ? Object.values(errors) - : []; - if (errorList.length) { - for (const err of errorList) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch((err) => { - notifications.push("error", err.message); - resolve(); - }); - }); - }, - }, - formData - ) - ); - }; - overlay.open(cb); + cb = () => comp; + overlay.open( + cb, + () => + !comp.state["current"]["modified"] || + confirm("You have unsaved changes. Close anyway?") + ); }, }, "New"