From 433b624616c4c76263b9820a32f2e76b659af86e Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 13 Jul 2023 16:52:16 -0700 Subject: [PATCH 01/71] Fix Ecephys access and block Behavior validation --- pyflask/manageNeuroconv/manage_neuroconv.py | 2 +- src/renderer/src/validation/validation.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyflask/manageNeuroconv/manage_neuroconv.py b/pyflask/manageNeuroconv/manage_neuroconv.py index 79bd70a37..a82baa340 100644 --- a/pyflask/manageNeuroconv/manage_neuroconv.py +++ b/pyflask/manageNeuroconv/manage_neuroconv.py @@ -297,7 +297,7 @@ def update_conversion_progress(**kwargs): # Update the first recording interface with Ecephys table data recording_interface = get_first_recording_interface(converter) - ecephys_metadata = info["metadata"]["Ecephys"] + ecephys_metadata = info["metadata"].get("Ecephys", {}) if recording_interface and is_supported_recording_interface(recording_interface, info["metadata"]): electrode_column_results = ecephys_metadata["ElectrodeColumns"] diff --git a/src/renderer/src/validation/validation.json b/src/renderer/src/validation/validation.json index 9e3a422dd..21092c1e1 100644 --- a/src/renderer/src/validation/validation.json +++ b/src/renderer/src/validation/validation.json @@ -35,6 +35,8 @@ "ElectrodeColumns": { } }, + "Behavior": false, + "Subject": { "description": false, "genotype": false, From ff987a93e6e51335e6355c47a0e59b74a3ee9dba Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 3 Aug 2023 13:24:46 -0700 Subject: [PATCH 02/71] Open a popup when the user jumps to a new page using the sidebar --- src/renderer/src/stories/Dashboard.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index c878e9d51..4ce2aecff 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -31,6 +31,7 @@ import "../../../../node_modules/@sweetalert2/theme-bulma/bulma.css"; import "../../assets/css/guided.css"; import isElectron from "../electron/check.js"; import { isStorybook, reloadPageToHome } from "../dependencies/globals.js"; +import Swal from "sweetalert2"; // import "https://jsuites.net/v4/jsuites.js" // import "https://bossanova.uk/jspreadsheet/v4/jexcel.js" @@ -86,7 +87,20 @@ export class Dashboard extends LitElement { this.sidebar.onClick = (_, value) => this.setAttribute("activePage", value.info.id); this.subSidebar = new NavigationSidebar(); - this.subSidebar.onClick = (id) => this.setAttribute("activePage", id); + this.subSidebar.onClick = async (id) => { + + const result = await Swal.fire({ + title: "You may have unsaved data on this page. Would you like to continue?", + text: "You won't be able to revert this!", + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + confirmButtonText: 'Keep Editing', + cancelButtonText: 'Jump to Selected Page' + }); + + if (!result.isConfirmed) this.setAttribute("activePage", id); + } this.pages = props.pages ?? {}; this.name = props.name; From da55922d36cd165f24e3e17a1998e0a78d214c07 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:27:20 +0000 Subject: [PATCH 03/71] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/Dashboard.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index 4ce2aecff..50cc6d973 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -88,19 +88,18 @@ export class Dashboard extends LitElement { this.subSidebar = new NavigationSidebar(); this.subSidebar.onClick = async (id) => { - const result = await Swal.fire({ title: "You may have unsaved data on this page. Would you like to continue?", text: "You won't be able to revert this!", - icon: 'warning', + icon: "warning", showCancelButton: true, - confirmButtonColor: '#3085d6', - confirmButtonText: 'Keep Editing', - cancelButtonText: 'Jump to Selected Page' + confirmButtonColor: "#3085d6", + confirmButtonText: "Keep Editing", + cancelButtonText: "Jump to Selected Page", }); if (!result.isConfirmed) this.setAttribute("activePage", id); - } + }; this.pages = props.pages ?? {}; this.name = props.name; From 5751907f6fd03b31dbef51eb58bad542184be8fc Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 4 Aug 2023 11:37:46 -0700 Subject: [PATCH 04/71] Track actual changes that need to be saved --- src/renderer/src/stories/BasicTable.js | 2 ++ src/renderer/src/stories/Dashboard.js | 20 +++++++++++-------- src/renderer/src/stories/JSONSchemaForm.js | 11 ++++++++++ src/renderer/src/stories/SimpleTable.js | 5 +++++ src/renderer/src/stories/Table.js | 16 ++++++++++++++- src/renderer/src/stories/pages/FormPage.js | 1 + src/renderer/src/stories/pages/Page.js | 11 +++++++++- .../pages/guided-mode/data/GuidedMetadata.js | 3 +++ .../guided-mode/data/GuidedPathExpansion.js | 6 +++++- .../guided-mode/data/GuidedSourceData.js | 1 + .../options/GuidedConversionOptions.js | 1 + .../pages/guided-mode/options/GuidedUpload.js | 1 + .../guided-mode/setup/GuidedNewDatasetInfo.js | 1 + .../pages/guided-mode/setup/GuidedSubjects.js | 4 +++- .../src/stories/pages/tutorial/Tutorial.js | 4 +--- src/renderer/src/stories/table/Cell.ts | 10 +++++++++- src/renderer/src/stories/table/cells/base.ts | 6 ++++++ 17 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/stories/BasicTable.js b/src/renderer/src/stories/BasicTable.js index 47f19082e..0478a4d5a 100644 --- a/src/renderer/src/stories/BasicTable.js +++ b/src/renderer/src/stories/BasicTable.js @@ -330,6 +330,8 @@ export class BasicTable extends LitElement { key in this.schema.properties ? (latest[key] = value) : "" ); // Only include data from schema }); + + this.onUpdate(null, null, value) // Update the whole table } // Render Code diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index 50cc6d973..2c03322e9 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -88,17 +88,20 @@ export class Dashboard extends LitElement { this.subSidebar = new NavigationSidebar(); this.subSidebar.onClick = async (id) => { - const result = await Swal.fire({ - title: "You may have unsaved data on this page. Would you like to continue?", - text: "You won't be able to revert this!", + + const result = this.#active.unsavedUpdates ? await Swal.fire({ + title: "You have unsaved data on this page.", + text: "Would you like to save your changes?", icon: "warning", showCancelButton: true, confirmButtonColor: "#3085d6", - confirmButtonText: "Keep Editing", - cancelButtonText: "Jump to Selected Page", - }); + confirmButtonText: "Save and Continue", + cancelButtonText: "Ignore Changes", + }) : undefined; - if (!result.isConfirmed) this.setAttribute("activePage", id); + if (result && result.isConfirmed) this.#active.save() + + this.setAttribute("activePage", id); }; this.pages = props.pages ?? {}; @@ -141,7 +144,8 @@ export class Dashboard extends LitElement { else if (key === "renderNameInSidebar") this.sidebar.renderName = latest === "true" || latest === true; else if (key === "pages") this.#updated(latest); else if (key.toLowerCase() === "activepage") { - if (this.#active && this.#active.info.parent && this.#active.info.section) this.#active.save(); // Always properly saves the page + + // if (this.#active && this.#active.info.parent && this.#active.info.section) this.#active.save(); // Always properly saves the page while (latest && !this.pagesById[latest]) latest = latest.split("/").slice(0, -1).join("/"); // Trim off last character until you find a page diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index b90306482..6c99fbccb 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -245,10 +245,12 @@ export class JSONSchemaForm extends LitElement { this.conditionalRequirements = props.conditionalRequirements ?? []; // NOTE: We assume properties only belong to one conditional requirement group this.validateEmptyValues = props.validateEmptyValues ?? true; + if (props.onInvalid) this.onInvalid = props.onInvalid; if (props.validateOnChange) this.validateOnChange = props.validateOnChange; if (props.onThrow) this.onThrow = props.onThrow; if (props.onLoaded) this.onLoaded = props.onLoaded; + if (props.onUpdate) this.onUpdate = props.onUpdate if (props.renderTable) this.renderTable = props.renderTable; if (props.onStatusChange) this.onStatusChange = props.onStatusChange; @@ -283,6 +285,9 @@ export class JSONSchemaForm extends LitElement { // Track resolved values for the form #update(fullPath, value) { + + this.onUpdate(fullPath, value) + const path = [...fullPath]; const name = path.pop(); const resultParent = path.reduce((acc, key) => acc[key], this.results); @@ -535,6 +540,10 @@ export class JSONSchemaForm extends LitElement { onStatusChange: () => this.#checkStatus(), // Check status on all elements validateEmptyCells: this.validateEmptyValues, deferLoading: this.deferLoading, + onUpdate: (row, col, value) => { + console.log(fullPath, row, col, value) + this.onUpdate([...fullPath. row, col], value) + }, onLoaded: () => { this.#nLoaded++; this.#checkAllLoaded(); @@ -603,6 +612,7 @@ ${info.default ? JSON.stringify(info.default, null, 2) : "No default value"} {}; onLoaded = () => {}; + onUpdate = () => {}; #deleteExtraneousResults = (results, schema) => { for (let name in results) { @@ -860,6 +870,7 @@ ${info.default ? JSON.stringify(info.default, null, 2) : "No default value"} this.onUpdate([...fullPath, ...internalPath], value), required: required[name], // Scoped to the sub-schema ignore: this.ignore, diff --git a/src/renderer/src/stories/SimpleTable.js b/src/renderer/src/stories/SimpleTable.js index 30def2ca2..89e904dfb 100644 --- a/src/renderer/src/stories/SimpleTable.js +++ b/src/renderer/src/stories/SimpleTable.js @@ -21,6 +21,7 @@ const isVisible = function (ele, container) { export class SimpleTable extends LitElement { validateOnChange; + onUpdate = () => {} static get styles() { return css` @@ -168,6 +169,7 @@ export class SimpleTable extends LitElement { validateEmptyCells, onStatusChange, onLoaded, + onUpdate, onThrow, deferLoading, maxHeight, @@ -185,6 +187,7 @@ export class SimpleTable extends LitElement { if (onStatusChange) this.onStatusChange = onStatusChange; if (onLoaded) this.onLoaded = onLoaded; if (onThrow) this.onThrow = onThrow; + if (onUpdate) this.onUpdate = onUpdate this.onmousedown = (ev) => { this.#clearSelected(); @@ -631,6 +634,8 @@ export class SimpleTable extends LitElement { if (value == undefined || value === "") delete target[rowName][header]; else target[rowName][header] = value; } + + if (cell.interacted) this.onUpdate(rowName, header, value) }; #createCell = (value, info) => { diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index 179408fec..1998ecf55 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -56,7 +56,7 @@ const styleSymbol = Symbol("table-styles"); export class Table extends LitElement { validateOnChange; - constructor({ schema, data, template, keyColumn, validateOnChange, validateEmptyCells, onStatusChange } = {}) { + constructor({ schema, data, template, keyColumn, validateOnChange, onUpdate, validateEmptyCells, onStatusChange } = {}) { super(); this.schema = schema ?? {}; this.data = data ?? []; @@ -64,6 +64,7 @@ export class Table extends LitElement { this.template = template ?? {}; this.validateEmptyCells = validateEmptyCells ?? true; + if (onUpdate) this.onUpdate = onUpdate if (validateOnChange) this.validateOnChange = validateOnChange; if (onStatusChange) this.onStatusChange = onStatusChange; @@ -134,8 +135,12 @@ export class Table extends LitElement { status; onStatusChange = () => {}; + onUpdate = () => {} updated() { + + let updateComplete = false + const div = (this.shadowRoot ?? this).querySelector("div"); const entries = { ...this.schema.properties }; @@ -298,6 +303,9 @@ export class Table extends LitElement { const unresolved = (this.unresolved = {}); + let validated = 0 + const initialCellsToUpdate = data.reduce((acc, v) => acc + v.length, 0) + table.addHook("afterValidate", (isValid, value, row, prop) => { const header = typeof prop === "number" ? colHeaders[prop] : prop; let rowName = this.keyColumn ? rowHeaders[row] : row; @@ -333,6 +341,10 @@ export class Table extends LitElement { else target[rowName][header] = value; } + validated++ + + if (initialCellsToUpdate < validated) this.onUpdate(rowName, header, value) + if (typeof isValid === "function") isValid(); // } }); @@ -364,6 +376,8 @@ export class Table extends LitElement { // Trigger validation on all cells data.forEach((row, i) => this.#setRow(i, row)); + + updateComplete = true } #setRow(row, data) { diff --git a/src/renderer/src/stories/pages/FormPage.js b/src/renderer/src/stories/pages/FormPage.js index 8a21d0f46..f298db657 100644 --- a/src/renderer/src/stories/pages/FormPage.js +++ b/src/renderer/src/stories/pages/FormPage.js @@ -64,6 +64,7 @@ export class GuidedFormPage extends Page { const form = (this.form = new JSONSchemaForm({ ...this.info.formOptions, results, + onUpdate: () => this.unsavedUpdates = true, validateOnChange, onThrow, })); diff --git a/src/renderer/src/stories/pages/Page.js b/src/renderer/src/stories/pages/Page.js index 407d2390b..35e15f58e 100644 --- a/src/renderer/src/stories/pages/Page.js +++ b/src/renderer/src/stories/pages/Page.js @@ -62,7 +62,10 @@ export class Page extends LitElement { onTransition = () => {}; // User-defined function updatePages = () => {}; // User-defined function - save = (overrides) => save(this, overrides); + save = (overrides) => { + save(this, overrides); + this.unsavedUpdates = false + } load = (datasetNameToResume = new URLSearchParams(window.location.search).get("project")) => (this.info.globalState = get(datasetNameToResume)); @@ -165,6 +168,12 @@ export class Page extends LitElement { this.updatePages(); }; + unsavedUpdates = false // Track unsaved updates + + updated() { + this.unsavedUpdates = false + } + render() { return html``; } diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js index 36759e09b..8492c91e0 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -101,6 +101,9 @@ export class GuidedMetadataPage extends ManagedPage { this.#nLoaded++; this.#checkAllLoaded(); }, + + onUpdate: () => this.unsavedUpdates = true, + validateOnChange, onlyRequired: false, onStatusChange: (state) => this.manager.updateState(`sub-${subject}/ses-${session}`, state), diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js index 9ab15cabb..be0d1fff1 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -152,7 +152,11 @@ export class GuidedPathExpansionPage extends Page { this.optional.requestUpdate(); - const form = (this.form = new JSONSchemaForm({ ...structureGlobalState, onThrow })); + const form = (this.form = new JSONSchemaForm({ + ...structureGlobalState, + onThrow , + onUpdate: () => this.unsavedUpdates = true, + })); const pathExpansionInfoBox = new InfoBox({ header: "How do I use a Python format string for path expansion?", diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js index c960041c6..1f0c9a652 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -96,6 +96,7 @@ export class GuidedSourceDataPage extends ManagedPage { results: info.source_data, ignore: ["verbose", "es_key", "exclude_shanks"], // onlyRequired: true, + onUpdate: () => this.unsavedUpdates = true, onStatusChange: (state) => this.manager.updateState(instanceId, state), onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js index f8bb85e00..b5f413f4f 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js @@ -48,6 +48,7 @@ export class GuidedConversionOptionsPage extends Page { dialogOptions: { properties: ["openDirectory", "createDirectory"], }, + onUpdate: () => this.unsavedUpdates = true, onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index cd4422cdc..b2372f8c9 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -64,6 +64,7 @@ export class GuidedUploadPage extends Page { // { name: 'NWB File', extensions: ['nwb'] } // ] }, + onUpdate: () => this.unsavedUpdates = true, onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index 2d36f6391..b47749061 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -91,6 +91,7 @@ export class GuidedNewDatasetPage extends Page { properties: ["createDirectory"], }, validateOnChange, + onUpdate: () => this.unsavedUpdates = true, onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js index 9404c6ff9..7d19b2f05 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -5,6 +5,7 @@ import { validateOnChange } from "../../../../validation/index.js"; import { Table } from "../../../Table.js"; import { updateResultsFromSubjects } from "./utils"; +import { SimpleTable } from "../../../SimpleTable.js"; export class GuidedSubjectsPage extends Page { constructor(...args) { @@ -65,12 +66,13 @@ export class GuidedSubjectsPage extends Page { subjects[subject].sessions = sessions; } - this.table = new Table({ + this.table = new SimpleTable({ schema: subjectSchema, data: subjects, template: this.info.globalState.project.Subject, keyColumn: "subject_id", validateEmptyCells: false, + onUpdate: () => this.unsavedUpdates = true, validateOnChange: (key, parent, v) => { if (key === "sessions") return true; else { diff --git a/src/renderer/src/stories/pages/tutorial/Tutorial.js b/src/renderer/src/stories/pages/tutorial/Tutorial.js index 7845838f8..2b7d5dab9 100644 --- a/src/renderer/src/stories/pages/tutorial/Tutorial.js +++ b/src/renderer/src/stories/pages/tutorial/Tutorial.js @@ -34,9 +34,7 @@ export class TutorialPage extends Page { properties: ["createDirectory"], }, results: state, - validateOnChange: () => { - global.save(); - }, + onUpdate: () => global.save() }); form.style.width = "100%"; diff --git a/src/renderer/src/stories/table/Cell.ts b/src/renderer/src/stories/table/Cell.ts index 24fc87c93..d02766419 100644 --- a/src/renderer/src/stories/table/Cell.ts +++ b/src/renderer/src/stories/table/Cell.ts @@ -132,6 +132,7 @@ export class TableCell extends LitElement { }; setInput(value: any) { + this.interacted = true this.input.set(value) // Ensure all operations are undoable } @@ -146,6 +147,8 @@ export class TableCell extends LitElement { #cls: any + interacted = false + // input = new TableCellBase({ }) input: TableCellBase @@ -153,6 +156,8 @@ export class TableCell extends LitElement { render() { let cls = TableCellBase + + this.interacted = false if (this.schema.type === "array") cls = ArrayCell else if (this.schema.format === "date-time") cls = DateTimeCell @@ -160,7 +165,10 @@ export class TableCell extends LitElement { // Only actually rerender if new class type if (cls !== this.#cls) { this.input = new cls({ - onChange: () => this.validate(), + onChange: () => { + if (this.input.interacted) this.interacted = true + this.validate() + }, schema: this.schema }) } diff --git a/src/renderer/src/stories/table/cells/base.ts b/src/renderer/src/stories/table/cells/base.ts index 7d6c9b3a1..3a52c7774 100644 --- a/src/renderer/src/stories/table/cells/base.ts +++ b/src/renderer/src/stories/table/cells/base.ts @@ -49,6 +49,8 @@ export class TableCellBase extends LitElement { schema: any; + interacted = false + constructor({ schema, onOpen, @@ -64,6 +66,7 @@ export class TableCellBase extends LitElement { if (onClose) this.onClose = onClose this.#editable.addEventListener('input', (ev: InputEvent) => { + this.interacted = true if (ev.inputType.includes('history')) this.setText(this.#editable.innerText) // Catch undo / redo} }) @@ -118,6 +121,7 @@ export class TableCellBase extends LitElement { } set(value: any) { + if (document.execCommand) { this.#editable.setAttribute('contenteditable', '') this.#editable.focus(); @@ -158,6 +162,8 @@ export class TableCellBase extends LitElement { render() { + this.interacted = false + this.#editable.id = 'editable' const editor = this.#editor = this.#render('editor') From 225daf489be225a7bfd0a196ba541ab71b8b63e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 18:38:03 +0000 Subject: [PATCH 05/71] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/BasicTable.js | 2 +- src/renderer/src/stories/Dashboard.js | 26 ++++++++--------- src/renderer/src/stories/JSONSchemaForm.js | 9 +++--- src/renderer/src/stories/SimpleTable.js | 6 ++-- src/renderer/src/stories/Table.js | 28 ++++++++++++------- src/renderer/src/stories/pages/FormPage.js | 2 +- src/renderer/src/stories/pages/Page.js | 8 +++--- .../pages/guided-mode/data/GuidedMetadata.js | 2 +- .../guided-mode/data/GuidedPathExpansion.js | 8 +++--- .../guided-mode/data/GuidedSourceData.js | 2 +- .../options/GuidedConversionOptions.js | 2 +- .../pages/guided-mode/options/GuidedUpload.js | 2 +- .../guided-mode/setup/GuidedNewDatasetInfo.js | 2 +- .../pages/guided-mode/setup/GuidedSubjects.js | 2 +- .../src/stories/pages/tutorial/Tutorial.js | 2 +- src/renderer/src/stories/table/Cell.ts | 2 +- 16 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/renderer/src/stories/BasicTable.js b/src/renderer/src/stories/BasicTable.js index 0478a4d5a..1cad4503d 100644 --- a/src/renderer/src/stories/BasicTable.js +++ b/src/renderer/src/stories/BasicTable.js @@ -331,7 +331,7 @@ export class BasicTable extends LitElement { ); // Only include data from schema }); - this.onUpdate(null, null, value) // Update the whole table + this.onUpdate(null, null, value); // Update the whole table } // Render Code diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index 2c03322e9..75d2d1817 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -88,19 +88,20 @@ export class Dashboard extends LitElement { this.subSidebar = new NavigationSidebar(); this.subSidebar.onClick = async (id) => { + const result = this.#active.unsavedUpdates + ? await Swal.fire({ + title: "You have unsaved data on this page.", + text: "Would you like to save your changes?", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + confirmButtonText: "Save and Continue", + cancelButtonText: "Ignore Changes", + }) + : undefined; + + if (result && result.isConfirmed) this.#active.save(); - const result = this.#active.unsavedUpdates ? await Swal.fire({ - title: "You have unsaved data on this page.", - text: "Would you like to save your changes?", - icon: "warning", - showCancelButton: true, - confirmButtonColor: "#3085d6", - confirmButtonText: "Save and Continue", - cancelButtonText: "Ignore Changes", - }) : undefined; - - if (result && result.isConfirmed) this.#active.save() - this.setAttribute("activePage", id); }; @@ -144,7 +145,6 @@ export class Dashboard extends LitElement { else if (key === "renderNameInSidebar") this.sidebar.renderName = latest === "true" || latest === true; else if (key === "pages") this.#updated(latest); else if (key.toLowerCase() === "activepage") { - // if (this.#active && this.#active.info.parent && this.#active.info.section) this.#active.save(); // Always properly saves the page while (latest && !this.pagesById[latest]) latest = latest.split("/").slice(0, -1).join("/"); // Trim off last character until you find a page diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 6c99fbccb..21240d714 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -250,7 +250,7 @@ export class JSONSchemaForm extends LitElement { if (props.validateOnChange) this.validateOnChange = props.validateOnChange; if (props.onThrow) this.onThrow = props.onThrow; if (props.onLoaded) this.onLoaded = props.onLoaded; - if (props.onUpdate) this.onUpdate = props.onUpdate + if (props.onUpdate) this.onUpdate = props.onUpdate; if (props.renderTable) this.renderTable = props.renderTable; if (props.onStatusChange) this.onStatusChange = props.onStatusChange; @@ -285,8 +285,7 @@ export class JSONSchemaForm extends LitElement { // Track resolved values for the form #update(fullPath, value) { - - this.onUpdate(fullPath, value) + this.onUpdate(fullPath, value); const path = [...fullPath]; const name = path.pop(); @@ -541,8 +540,8 @@ export class JSONSchemaForm extends LitElement { validateEmptyCells: this.validateEmptyValues, deferLoading: this.deferLoading, onUpdate: (row, col, value) => { - console.log(fullPath, row, col, value) - this.onUpdate([...fullPath. row, col], value) + console.log(fullPath, row, col, value); + this.onUpdate([...fullPath.row, col], value); }, onLoaded: () => { this.#nLoaded++; diff --git a/src/renderer/src/stories/SimpleTable.js b/src/renderer/src/stories/SimpleTable.js index 89e904dfb..0f09c62b5 100644 --- a/src/renderer/src/stories/SimpleTable.js +++ b/src/renderer/src/stories/SimpleTable.js @@ -21,7 +21,7 @@ const isVisible = function (ele, container) { export class SimpleTable extends LitElement { validateOnChange; - onUpdate = () => {} + onUpdate = () => {}; static get styles() { return css` @@ -187,7 +187,7 @@ export class SimpleTable extends LitElement { if (onStatusChange) this.onStatusChange = onStatusChange; if (onLoaded) this.onLoaded = onLoaded; if (onThrow) this.onThrow = onThrow; - if (onUpdate) this.onUpdate = onUpdate + if (onUpdate) this.onUpdate = onUpdate; this.onmousedown = (ev) => { this.#clearSelected(); @@ -635,7 +635,7 @@ export class SimpleTable extends LitElement { else target[rowName][header] = value; } - if (cell.interacted) this.onUpdate(rowName, header, value) + if (cell.interacted) this.onUpdate(rowName, header, value); }; #createCell = (value, info) => { diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index 1998ecf55..4e0a4c478 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -56,7 +56,16 @@ const styleSymbol = Symbol("table-styles"); export class Table extends LitElement { validateOnChange; - constructor({ schema, data, template, keyColumn, validateOnChange, onUpdate, validateEmptyCells, onStatusChange } = {}) { + constructor({ + schema, + data, + template, + keyColumn, + validateOnChange, + onUpdate, + validateEmptyCells, + onStatusChange, + } = {}) { super(); this.schema = schema ?? {}; this.data = data ?? []; @@ -64,7 +73,7 @@ export class Table extends LitElement { this.template = template ?? {}; this.validateEmptyCells = validateEmptyCells ?? true; - if (onUpdate) this.onUpdate = onUpdate + if (onUpdate) this.onUpdate = onUpdate; if (validateOnChange) this.validateOnChange = validateOnChange; if (onStatusChange) this.onStatusChange = onStatusChange; @@ -135,11 +144,10 @@ export class Table extends LitElement { status; onStatusChange = () => {}; - onUpdate = () => {} + onUpdate = () => {}; updated() { - - let updateComplete = false + let updateComplete = false; const div = (this.shadowRoot ?? this).querySelector("div"); @@ -303,8 +311,8 @@ export class Table extends LitElement { const unresolved = (this.unresolved = {}); - let validated = 0 - const initialCellsToUpdate = data.reduce((acc, v) => acc + v.length, 0) + let validated = 0; + const initialCellsToUpdate = data.reduce((acc, v) => acc + v.length, 0); table.addHook("afterValidate", (isValid, value, row, prop) => { const header = typeof prop === "number" ? colHeaders[prop] : prop; @@ -341,9 +349,9 @@ export class Table extends LitElement { else target[rowName][header] = value; } - validated++ + validated++; - if (initialCellsToUpdate < validated) this.onUpdate(rowName, header, value) + if (initialCellsToUpdate < validated) this.onUpdate(rowName, header, value); if (typeof isValid === "function") isValid(); // } @@ -377,7 +385,7 @@ export class Table extends LitElement { // Trigger validation on all cells data.forEach((row, i) => this.#setRow(i, row)); - updateComplete = true + updateComplete = true; } #setRow(row, data) { diff --git a/src/renderer/src/stories/pages/FormPage.js b/src/renderer/src/stories/pages/FormPage.js index f298db657..dc5281837 100644 --- a/src/renderer/src/stories/pages/FormPage.js +++ b/src/renderer/src/stories/pages/FormPage.js @@ -64,7 +64,7 @@ export class GuidedFormPage extends Page { const form = (this.form = new JSONSchemaForm({ ...this.info.formOptions, results, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), validateOnChange, onThrow, })); diff --git a/src/renderer/src/stories/pages/Page.js b/src/renderer/src/stories/pages/Page.js index 35e15f58e..879343b61 100644 --- a/src/renderer/src/stories/pages/Page.js +++ b/src/renderer/src/stories/pages/Page.js @@ -64,8 +64,8 @@ export class Page extends LitElement { save = (overrides) => { save(this, overrides); - this.unsavedUpdates = false - } + this.unsavedUpdates = false; + }; load = (datasetNameToResume = new URLSearchParams(window.location.search).get("project")) => (this.info.globalState = get(datasetNameToResume)); @@ -168,10 +168,10 @@ export class Page extends LitElement { this.updatePages(); }; - unsavedUpdates = false // Track unsaved updates + unsavedUpdates = false; // Track unsaved updates updated() { - this.unsavedUpdates = false + this.unsavedUpdates = false; } render() { diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js index 8492c91e0..32b52c9d7 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -102,7 +102,7 @@ export class GuidedMetadataPage extends ManagedPage { this.#checkAllLoaded(); }, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), validateOnChange, onlyRequired: false, diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js index be0d1fff1..098e626e1 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -152,10 +152,10 @@ export class GuidedPathExpansionPage extends Page { this.optional.requestUpdate(); - const form = (this.form = new JSONSchemaForm({ - ...structureGlobalState, - onThrow , - onUpdate: () => this.unsavedUpdates = true, + const form = (this.form = new JSONSchemaForm({ + ...structureGlobalState, + onThrow, + onUpdate: () => (this.unsavedUpdates = true), })); const pathExpansionInfoBox = new InfoBox({ diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js index 1f0c9a652..04e76358f 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -96,7 +96,7 @@ export class GuidedSourceDataPage extends ManagedPage { results: info.source_data, ignore: ["verbose", "es_key", "exclude_shanks"], // onlyRequired: true, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), onStatusChange: (state) => this.manager.updateState(instanceId, state), onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js index b5f413f4f..fbd66ec08 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedConversionOptions.js @@ -48,7 +48,7 @@ export class GuidedConversionOptionsPage extends Page { dialogOptions: { properties: ["openDirectory", "createDirectory"], }, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js index b2372f8c9..1b1133c89 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -64,7 +64,7 @@ export class GuidedUploadPage extends Page { // { name: 'NWB File', extensions: ['nwb'] } // ] }, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js index b47749061..831b708fb 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -91,7 +91,7 @@ export class GuidedNewDatasetPage extends Page { properties: ["createDirectory"], }, validateOnChange, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), onThrow, }); diff --git a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js index 7d19b2f05..475cae9a4 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -72,7 +72,7 @@ export class GuidedSubjectsPage extends Page { template: this.info.globalState.project.Subject, keyColumn: "subject_id", validateEmptyCells: false, - onUpdate: () => this.unsavedUpdates = true, + onUpdate: () => (this.unsavedUpdates = true), validateOnChange: (key, parent, v) => { if (key === "sessions") return true; else { diff --git a/src/renderer/src/stories/pages/tutorial/Tutorial.js b/src/renderer/src/stories/pages/tutorial/Tutorial.js index 2b7d5dab9..9cc911642 100644 --- a/src/renderer/src/stories/pages/tutorial/Tutorial.js +++ b/src/renderer/src/stories/pages/tutorial/Tutorial.js @@ -34,7 +34,7 @@ export class TutorialPage extends Page { properties: ["createDirectory"], }, results: state, - onUpdate: () => global.save() + onUpdate: () => global.save(), }); form.style.width = "100%"; diff --git a/src/renderer/src/stories/table/Cell.ts b/src/renderer/src/stories/table/Cell.ts index d02766419..e258e218f 100644 --- a/src/renderer/src/stories/table/Cell.ts +++ b/src/renderer/src/stories/table/Cell.ts @@ -156,7 +156,7 @@ export class TableCell extends LitElement { render() { let cls = TableCellBase - + this.interacted = false if (this.schema.type === "array") cls = ArrayCell From 681219ded39a1b281ae11f7997689d78d7dc4b18 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 4 Aug 2023 16:03:27 -0700 Subject: [PATCH 06/71] Interrim progress that generalizes save progression --- src/renderer/src/progress.js | 68 ++++++++++++---- src/renderer/src/stories/Dashboard.js | 23 ++---- src/renderer/src/stories/JSONSchemaForm.js | 2 +- src/renderer/src/stories/Main.js | 7 +- src/renderer/src/stories/pages/FormPage.js | 20 ++++- src/renderer/src/stories/pages/Page.js | 36 ++++++++- .../pages/getting-started/GettingStarted.js | 2 +- .../stories/pages/guided-mode/GuidedFooter.js | 8 +- .../stories/pages/guided-mode/GuidedHome.js | 2 +- .../pages/guided-mode/data/GuidedMetadata.js | 42 ++++++++-- .../guided-mode/data/GuidedPathExpansion.js | 8 +- .../guided-mode/data/GuidedSourceData.js | 21 +++-- .../pages/guided-mode/data/GuidedStructure.js | 41 +++++----- .../stories/pages/guided-mode/data/utils.js | 9 ++- .../options/GuidedConversionOptions.js | 4 +- .../guided-mode/options/GuidedStubPreview.js | 7 +- .../pages/guided-mode/options/GuidedUpload.js | 4 +- .../guided-mode/setup/GuidedNewDatasetInfo.js | 2 +- .../pages/guided-mode/setup/GuidedSubjects.js | 77 +++++++++++-------- .../src/stories/pages/tutorial/Tutorial.js | 2 +- src/renderer/src/stories/pages/utils.js | 29 +++---- 21 files changed, 268 insertions(+), 146 deletions(-) diff --git a/src/renderer/src/progress.js b/src/renderer/src/progress.js index 6e9930b65..d20d095af 100644 --- a/src/renderer/src/progress.js +++ b/src/renderer/src/progress.js @@ -3,6 +3,7 @@ import Swal from "sweetalert2"; import { guidedProgressFilePath, reloadPageToHome, isStorybook, appDirectory } from "./dependencies/globals.js"; import { fs } from "./electron/index.js"; import { joinPath, runOnLoad } from "./globals.js"; +import { merge } from "./stories/pages/utils.js"; class GlobalAppConfig { path = `${appDirectory}/config.json`; @@ -48,33 +49,70 @@ export const update = (newDatasetName, previousDatasetName) => { } else throw new Error("No previous dataset name provided"); }; +export const getCurrentProjectName = () => { + const params = new URLSearchParams(location.search); + return params.get("project"); +} + +export const updateAppProgress = (pageId, dataOrProjectName = {}, projectName = typeof dataOrProjectName === 'string' ? dataOrProjectName : undefined) => { + + if (projectName) { + const params = new URLSearchParams(location.search); + params.set("project", projectName); + + // Update browser history state + const value = `${location.pathname}?${params}`; + if (history.state) history.state.project = dataOrProjectName; + window.history.pushState(history.state, null, value); + } + + // Is a project name + if (dataOrProjectName === projectName) updateFile(dataOrProjectName, (data) => data["page-before-exit"] = pageId) + + // Is a data object + else dataOrProjectName["page-before-exit"] = pageId + +} + export const save = (page, overrides = {}) => { - const globalState = page.info.globalState; - let guidedProgressFileName = overrides.globalState?.project?.name ?? globalState.project?.name; + + const globalState = merge(overrides, page.info.globalState); // Merge the overrides into the actual global state + + let guidedProgressFileName = globalState.project?.name; //return if guidedProgressFileName is not a string greater than 0 if (typeof guidedProgressFileName !== "string" || guidedProgressFileName.length === 0) return; - const params = new URLSearchParams(location.search); - params.set("project", guidedProgressFileName); + updateFile(guidedProgressFileName, () => { + updateAppProgress(page.info.id, globalState, guidedProgressFileName) // Will automatically set last updated time + return globalState + }) +}; + +//Destination: HOMEDIR/NWB_GUIDE/pipelines +export const updateFile = (projectName, callback) => { + + let data = get(projectName) - // Update browser history state - const value = `${location.pathname}?${params}`; - if (history.state) history.state.project = guidedProgressFileName; - window.history.pushState(history.state, null, value); + if (callback) { + const result = callback(data) + if (result && typeof result === 'object') data = result + } + + data["last-modified"] = new Date(); // Always update the last modified time - //Destination: HOMEDIR/NWB_GUIDE/pipelines - globalState["last-modified"] = new Date(); - globalState["page-before-exit"] = overrides.id ?? page.info.id; + const copy = merge(data, {}) + console.error(copy) - var guidedFilePath = joinPath(guidedProgressFilePath, guidedProgressFileName + ".json"); + var guidedFilePath = joinPath(guidedProgressFilePath, projectName + ".json"); // Save the file through the available mechanisms if (fs) { if (!fs.existsSync(guidedProgressFilePath)) fs.mkdirSync(guidedProgressFilePath, { recursive: true }); //create progress folder if one does not exist - fs.writeFileSync(guidedFilePath, JSON.stringify(globalState, null, 2)); - } else localStorage.setItem(guidedFilePath, JSON.stringify(globalState)); -}; + fs.writeFileSync(guidedFilePath, JSON.stringify(data, null, 2)); + } else localStorage.setItem(guidedFilePath, JSON.stringify(data)); + +} export const getEntries = () => { if (fs && !fs.existsSync(guidedProgressFilePath)) fs.mkdirSync(guidedProgressFilePath, { recursive: true }); //Check if progress folder exists. If not, create it. diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index 2c03322e9..4f54cef57 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -31,7 +31,7 @@ import "../../../../node_modules/@sweetalert2/theme-bulma/bulma.css"; import "../../assets/css/guided.css"; import isElectron from "../electron/check.js"; import { isStorybook, reloadPageToHome } from "../dependencies/globals.js"; -import Swal from "sweetalert2"; +import { getCurrentProjectName, updateAppProgress } from "../progress.js"; // import "https://jsuites.net/v4/jsuites.js" // import "https://bossanova.uk/jspreadsheet/v4/jexcel.js" @@ -89,19 +89,9 @@ export class Dashboard extends LitElement { this.subSidebar = new NavigationSidebar(); this.subSidebar.onClick = async (id) => { - const result = this.#active.unsavedUpdates ? await Swal.fire({ - title: "You have unsaved data on this page.", - text: "Would you like to save your changes?", - icon: "warning", - showCancelButton: true, - confirmButtonColor: "#3085d6", - confirmButtonText: "Save and Continue", - cancelButtonText: "Ignore Changes", - }) : undefined; - - if (result && result.isConfirmed) this.#active.save() + this.#active.to(id) - this.setAttribute("activePage", id); + // this.setAttribute("activePage", id); }; this.pages = props.pages ?? {}; @@ -145,8 +135,11 @@ export class Dashboard extends LitElement { else if (key === "pages") this.#updated(latest); else if (key.toLowerCase() === "activepage") { - // if (this.#active && this.#active.info.parent && this.#active.info.section) this.#active.save(); // Always properly saves the page - + if (this.#active && this.#active.info.parent && this.#active.info.section) { + const currentProject = getCurrentProjectName() + updateAppProgress(latest, currentProject) + } + while (latest && !this.pagesById[latest]) latest = latest.split("/").slice(0, -1).join("/"); // Trim off last character until you find a page this.sidebar.selectItem(latest); // Just highlight the item diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 6c99fbccb..7e7d5fb18 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -976,7 +976,7 @@ ${info.default ? JSON.stringify(info.default, null, 2) : "No default value"} this.onTransition("/"), + onNext: () => this.toRender.page.to("/"), }; // Allow navigating laterally if there is a next page else footer = true; diff --git a/src/renderer/src/stories/pages/FormPage.js b/src/renderer/src/stories/pages/FormPage.js index f298db657..adf37f04b 100644 --- a/src/renderer/src/stories/pages/FormPage.js +++ b/src/renderer/src/stories/pages/FormPage.js @@ -3,6 +3,7 @@ import { JSONSchemaForm } from "../JSONSchemaForm.js"; import { Page } from "./Page.js"; import { validateOnChange } from "../../validation/index.js"; import { onThrow } from "../../errors"; +import { merge } from "./utils.js"; export function schemaToPages(schema, globalStatePath, options, transformationCallback = (info) => info) { return Object.entries(schema.properties) @@ -46,11 +47,21 @@ export class GuidedFormPage extends Page { if (!this.info.formOptions.results) this.info.formOptions.results = {}; } + beforeSave = () => { + + // Merge results before saving + if (this.info.globalStatePath) { + const parent = this.info.globalStatePath.reduce((acc, key) => acc[key] ?? (acc[key] = {}), this.info.globalState) + parent[this.info.key] = this.localState[this.info.key] + } + + } + footer = { onNext: async () => { - this.save(); - await this.form.validate(); - this.onTransition(1); + await this.save(); // Save in case validation fails + await this.form.validate(); // Validate the results of the form + this.to(1); }, }; @@ -59,7 +70,8 @@ export class GuidedFormPage extends Page { const temp = this.info.globalStatePath ? this.info.globalStatePath.reduce((acc, key) => acc[key] ?? (acc[key] = {}), this.info.globalState) : {}; - const results = { [key]: temp[key] ?? (temp[key] = {}) }; + + const results = this.localState = merge({ [key]: temp[key] ?? (temp[key] = {}) }, {}); // Keep a local copy of the results const form = (this.form = new JSONSchemaForm({ ...this.info.formOptions, diff --git a/src/renderer/src/stories/pages/Page.js b/src/renderer/src/stories/pages/Page.js index 35e15f58e..eae6b34a0 100644 --- a/src/renderer/src/stories/pages/Page.js +++ b/src/renderer/src/stories/pages/Page.js @@ -6,6 +6,7 @@ import { merge, randomizeElements, mapSessions } from "./utils.js"; import { ProgressBar } from "../ProgressBar"; import { resolveResults } from "./guided-mode/data/utils.js"; +import Swal from "sweetalert2"; export class Page extends LitElement { // static get styles() { @@ -59,10 +60,39 @@ export class Page extends LitElement { this.#notifications.push(note); }; + + to = async (transition) => { + + // Otherwise note unsaved updates if present + if (this.unsavedUpdates) { + if (transition === 1) await this.save() // Save before a single forward transition + else { + Swal.fire({ + title: "You have unsaved data on this page.", + text: "Would you like to save your changes?", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + confirmButtonText: "Save and Continue", + cancelButtonText: "Ignore Changes", + }).then(async result => { + if (result && result.isConfirmed) await this.save() + this.onTransition(transition) + }) + + return + } + } + + this.onTransition(transition) + } + onTransition = () => {}; // User-defined function updatePages = () => {}; // User-defined function + beforeSave = () => {} // User-defined function - save = (overrides) => { + save = async (overrides, runBeforeSave = true) => { + if (runBeforeSave) await this.beforeSave() save(this, overrides); this.unsavedUpdates = false } @@ -70,7 +100,6 @@ export class Page extends LitElement { load = (datasetNameToResume = new URLSearchParams(window.location.search).get("project")) => (this.info.globalState = get(datasetNameToResume)); - merge = merge; addSession({ subject, session, info }) { if (!this.info.globalState.results[subject]) this.info.globalState.results[subject] = {}; @@ -86,7 +115,7 @@ export class Page extends LitElement { delete this.info.globalState.results[subject][session]; } - mapSessions = (callback) => mapSessions(callback, this.info.globalState); + mapSessions = (callback, data = this.info.globalState) => mapSessions(callback, data); async runConversions(conversionOptions = {}, toRun, options = {}) { let original = toRun; @@ -170,6 +199,7 @@ export class Page extends LitElement { unsavedUpdates = false // Track unsaved updates + // NOTE: Make sure you call this explicitly if a child class overwrites this AND data is updated updated() { this.unsavedUpdates = false } diff --git a/src/renderer/src/stories/pages/getting-started/GettingStarted.js b/src/renderer/src/stories/pages/getting-started/GettingStarted.js index e5f80cf89..ae6359fb2 100644 --- a/src/renderer/src/stories/pages/getting-started/GettingStarted.js +++ b/src/renderer/src/stories/pages/getting-started/GettingStarted.js @@ -65,7 +65,7 @@ export class GettingStartedPage extends Page {