From 1a3c946dee559c2bcec80e766772ad209229406a Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 6 Mar 2024 12:48:01 -0800 Subject: [PATCH 01/10] Complete first draft of preform page with relevant interactions across the GUIDE workflow --- src/renderer/src/pages.js | 7 + src/renderer/src/progress/operations.js | 1 - src/renderer/src/stories/Dashboard.js | 29 ++- src/renderer/src/stories/JSONSchemaInput.js | 18 +- src/renderer/src/stories/Main.js | 21 +- src/renderer/src/stories/NavigationSidebar.js | 2 +- .../pages/guided-mode/data/GuidedMetadata.js | 26 +- .../guided-mode/data/GuidedPathExpansion.js | 232 ++++++++---------- .../guided-mode/data/GuidedSourceData.js | 26 +- .../pages/guided-mode/setup/GuidedSubjects.js | 45 ++-- .../pages/guided-mode/setup/Preform.js | 120 +++++++++ .../stories/pages/settings/SettingsPage.js | 5 +- 12 files changed, 351 insertions(+), 181 deletions(-) create mode 100644 src/renderer/src/stories/pages/guided-mode/setup/Preform.js diff --git a/src/renderer/src/pages.js b/src/renderer/src/pages.js index edf1c3ac2..38d9956d2 100644 --- a/src/renderer/src/pages.js +++ b/src/renderer/src/pages.js @@ -28,6 +28,7 @@ import { UploadsPage } from "./stories/pages/uploads/UploadsPage"; import { SettingsPage } from "./stories/pages/settings/SettingsPage"; import { InspectPage } from "./stories/pages/inspect/InspectPage"; import { PreviewPage } from "./stories/pages/preview/PreviewPage"; +import { GuidedPreform } from "./stories/pages/guided-mode/setup/Preform"; let dashboard = document.querySelector("nwb-dashboard"); if (!dashboard) dashboard = new Dashboard(); @@ -94,6 +95,12 @@ const pages = { section: sections[0], }), + workflow: new GuidedPreform({ + title: "Pipeline Workflow", + label: "Pipeline workflow", + section: sections[0], + }), + structure: new GuidedStructurePage({ title: "Provide Data Formats", label: "Data formats", diff --git a/src/renderer/src/progress/operations.js b/src/renderer/src/progress/operations.js index 10e25e533..6d5990f58 100644 --- a/src/renderer/src/progress/operations.js +++ b/src/renderer/src/progress/operations.js @@ -12,7 +12,6 @@ export const remove = (name) => { } else localStorage.removeItem(progressFilePathToDelete); if (fs) { - console.log(previewSaveFolderPath, conversionSaveFolderPath, name); // delete default preview location fs.rmSync(joinPath(previewSaveFolderPath, name), { recursive: true, force: true }); diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index 6099785f9..443603e0f 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -157,6 +157,8 @@ export class Dashboard extends LitElement { while (latest && !this.pagesById[latest]) latest = latest.split("/").slice(0, -1).join("/"); // Trim off last character until you find a page + // Update sidebar states + this.sidebar.selectItem(latest); // Just highlight the item this.sidebar.initialize = false; this.#activatePage(latest); @@ -214,7 +216,6 @@ export class Dashboard extends LitElement { this.page.set(toPass, false); this.page.checkSyncState().then(() => { - this.page.requestUpdate(); // Re-render page const projectName = info.globalState?.project?.name; @@ -228,6 +229,19 @@ export class Dashboard extends LitElement { }); if (this.#transitionPromise.value) this.#transitionPromise.trigger(page); // This ensures calls to page.to() can be properly awaited until the next page is ready + + const { skipped } = this.subSidebar.sections[info.section].pages[info.id]; + if ( skipped ) { + + // Run skip functions + Object.entries(page.workflow).forEach(([key, state]) => { + if (typeof state.skip === 'function') state.skip() + }); + + // Skip right over the page if configured as such + if (previous.info.previous === this.page) this.back() + else this.next(); + } }); } @@ -260,6 +274,18 @@ export class Dashboard extends LitElement { state.active = false; pageState.active = false; + // Check if page is skipped based on workflow state (if applicable) + if (page.workflow) { + const workflow = page.workflow; + const workflowValues = globalState.project?.workflow ?? {}; + const skipped = Object.entries(workflow).some(([key, state]) => { + if (!workflowValues[key]) return state.skip + }); + + pageState.skipped = skipped + } + + if (page.info.pages) this.#getSections(page.info.pages, globalState); // Show all states if (!("visited" in pageState)) pageState.visited = false; @@ -311,6 +337,7 @@ export class Dashboard extends LitElement { const page = this.getPage(this.pagesById[id]); if (page) { + const { id, label } = page.info; const queries = new URLSearchParams(window.location.search); queries.set("page", id); diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 281c281df..5b3d52027 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -514,15 +514,17 @@ export class JSONSchemaInput extends LitElement { if (!forceValidate) { // Update the actual input element const inputElement = this.getElement(); + if (!inputElement) return false; + if (inputElement.type === "checkbox") inputElement.checked = value; - else if (inputElement.classList.contains("list")) { - const list = inputElement.children[0]; - inputElement.children[0].items = this.#mapToList({ - value, - list, - }); // NOTE: Make sure this is correct - } else if (inputElement instanceof Search) inputElement.shadowRoot.querySelector("input").value = value; - else inputElement.value = value; + else if (inputElement.classList.contains("list")) { + const list = inputElement.children[0]; + inputElement.children[0].items = this.#mapToList({ + value, + list, + }); // NOTE: Make sure this is correct + } else if (inputElement instanceof Search) inputElement.shadowRoot.querySelector("input").value = value; + else inputElement.value = value; } const { path: fullPath } = this; diff --git a/src/renderer/src/stories/Main.js b/src/renderer/src/stories/Main.js index 65b163c3c..1bfdf5e9a 100644 --- a/src/renderer/src/stories/Main.js +++ b/src/renderer/src/stories/Main.js @@ -60,6 +60,24 @@ export class Main extends LitElement { page.onTransition = this.onTransition; page.updatePages = this.updatePages; + // Constrain based on workflow configuration + const workflowConfig = page.workflow ?? ( page.workflow = {} ) + const workflowValues = page.info.globalState?.project?.workflow ?? {}; + + Object.entries(workflowConfig).forEach(([ key, state ]) => { + workflowConfig[key].value = workflowValues[key] + + const value = workflowValues[key] + + if (state.elements) { + const elements = state.elements + if (value) elements.forEach((el) => el.removeAttribute("hidden")) + else elements.forEach((el) => el.setAttribute("hidden", true)) + } + }) + + page.requestUpdate() // Ensure the page is re-rendered with new workflow configurations + if (this.content) this.toRender = toRender.page ? toRender : { page }; else this.#queue.push(page); } @@ -80,6 +98,7 @@ export class Main extends LitElement { } render() { + let { page = "", sections = {} } = this.toRender ?? {}; let footer = page?.footer; // Page-specific footer @@ -97,7 +116,6 @@ export class Main extends LitElement { // Go to home screen if there is no next page if (!info.next) { - console.log("setting", info); footer = Object.assign( { exit: false, @@ -131,7 +149,6 @@ export class Main extends LitElement { if (header === true || !("header" in page) || !("sections" in page.header)) { const sectionNames = Object.keys(sections); - header = page.header && typeof page.header === "object" ? page.header : {}; header.sections = sectionNames; header.selected = sectionNames.indexOf(info.section); diff --git a/src/renderer/src/stories/NavigationSidebar.js b/src/renderer/src/stories/NavigationSidebar.js index 71b00cf34..168c953fb 100644 --- a/src/renderer/src/stories/NavigationSidebar.js +++ b/src/renderer/src/stories/NavigationSidebar.js @@ -129,7 +129,7 @@ export class NavigationSidebar extends LitElement { class=" guided--nav-bar-section-page hidden - ${state.visited ? " completed" : " not-completed"} + ${state.visited && !state.skipped ? " completed" : " not-completed"} ${state.active ? "active" : ""}"f " @click=${() => this.onClick(id)} 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 53ddb4065..5ddad9e60 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -89,17 +89,23 @@ export class GuidedMetadataPage extends ManagedPage { form; + #globalButton = new Button({ + icon: globalIcon, + label: "Edit Global Metadata", + onClick: () => { + this.#globalModal.form.results = structuredClone(this.info.globalState.project); + this.#globalModal.open = true; + }, + }) + + workflow = { + multiple_sessions: { + elements: [ this.#globalButton ], + } + } + header = { - controls: [ - new Button({ - icon: globalIcon, - label: "Edit Global Metadata", - onClick: () => { - this.#globalModal.form.results = structuredClone(this.info.globalState.project); - this.#globalModal.open = true; - }, - }), - ], + controls: [ this.#globalButton ], subtitle: "Edit all metadata for this conversion at the session level", }; 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 30380b8bc..2c9390a69 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -236,6 +236,9 @@ function getFiles(dir) { } export class GuidedPathExpansionPage extends Page { + + #notification + constructor(...args) { super(...args); } @@ -244,121 +247,130 @@ export class GuidedPathExpansionPage extends Page { subtitle: "Automatic source data detection for multiple subjects / sessions", }; - beforeSave = async () => { - const keepExistingData = this.dataManagementForm.resolved.keep_existing_data; - this.localState.keep_existing_data = keepExistingData; - - const globalState = this.info.globalState; - merge({ structure: this.localState }, globalState); // Merge the actual entries into the structure - - const hidden = this.optional.hidden; - globalState.structure.state = !hidden; - - if (hidden) { - // Force single subject/session if not keeping existing data - if (!keepExistingData || !globalState.results) { - const existingMetadata = - globalState.results?.[this.altInfo.subject_id]?.[this.altInfo.session_id]?.metadata; - - const existingSourceData = - globalState.results?.[this.altInfo.subject_id]?.[this.altInfo.session_id]?.source_data; - - const source_data = {}; - for (let key in globalState.interfaces) { - const existing = existingSourceData?.[key]; - if (existing) source_data[key] = existing ?? {}; - } + #initialize = () => this.localState = merge(this.info.globalState.structure, { results: {} }) + + workflow = { + locate_data: { + skip: () => { + + this.#initialize() + const globalState = this.info.globalState; + merge({ structure: this.localState }, globalState); // Merge the actual entries into the structure + + // Force single subject/session if not keeping existing data + if (!globalState.results) { + const existingMetadata = + globalState.results?.[this.altInfo.subject_id]?.[this.altInfo.session_id]?.metadata; + + const existingSourceData = + globalState.results?.[this.altInfo.subject_id]?.[this.altInfo.session_id]?.source_data; + + const source_data = {}; + for (let key in globalState.interfaces) { + const existing = existingSourceData?.[key]; + if (existing) source_data[key] = existing ?? {}; + } - globalState.results = { - [this.altInfo.subject_id]: { - [this.altInfo.session_id]: { - source_data, - metadata: { - NWBFile: { - session_id: this.altInfo.session_id, - ...(existingMetadata?.NWBFile ?? {}), - }, - Subject: { - subject_id: this.altInfo.subject_id, - ...(existingMetadata?.Subject ?? {}), + globalState.results = { + [this.altInfo.subject_id]: { + [this.altInfo.session_id]: { + source_data, + metadata: { + NWBFile: { + session_id: this.altInfo.session_id, + ...(existingMetadata?.NWBFile ?? {}), + }, + Subject: { + subject_id: this.altInfo.subject_id, + ...(existingMetadata?.Subject ?? {}), + }, }, }, }, - }, - }; + }; + } + } } + } - // Otherwise use path expansion to merge into existing subjects - else if (!hidden && hidden !== undefined) { - const structure = globalState.structure.results; - - await this.form.validate(); - - const finalStructure = {}; - for (let key in structure) { - const entry = { ...structure[key] }; - const fstring = entry.format_string_path; - if (!fstring) continue; - if (fstring.split(".").length > 1) entry.file_path = fstring; - else entry.folder_path = fstring; - delete entry.format_string_path; - finalStructure[key] = entry; - } + beforeSave = async () => { + + const globalState = this.info.globalState; + merge({ structure: this.localState }, globalState); // Merge the actual entries into the structure - const results = await run(`locate`, finalStructure, { title: "Locating Data" }).catch((error) => { - this.notify(error.message, "error"); - throw error; - }); + const structure = globalState.structure.results; - const subjects = Object.keys(results); - if (subjects.length === 0) { - const message = "No subjects found with the current configuration. Please try again."; - this.notify(message, "error"); - throw message; - } + await this.form.validate(); + + const finalStructure = {}; + for (let key in structure) { + const entry = { ...structure[key] }; + const fstring = entry.format_string_path; + if (!fstring) continue; + if (fstring.split(".").length > 1) entry.file_path = fstring; + else entry.folder_path = fstring; + delete entry.format_string_path; + finalStructure[key] = entry; + } + + const results = await run(`locate`, finalStructure, { title: "Locating Data" }).catch((error) => { + this.notify(error.message, "error"); + throw error; + }); - // Save an overall results object organized by subject and session - merge({ results }, globalState); + const subjects = Object.keys(results); + if (subjects.length === 0) { + if (this.#notification) this.dismiss(this.#notification); + const message = "No subjects found with the current configuration. Please try again."; + this.#notification = this.notify(message, "error"); + throw message; + } + + // Save an overall results object organized by subject and session + merge({ results }, globalState); - const globalResults = globalState.results; + const globalResults = globalState.results; - if (!keepExistingData) { - for (let sub in globalResults) { - const subRef = results[sub]; - if (!subRef) - delete globalResults[sub]; // Delete removed subjects - else { - for (let ses in globalResults[sub]) { - const sesRef = subRef[ses]; + if (!keepExistingData) { + for (let sub in globalResults) { + const subRef = results[sub]; + if (!subRef) + delete globalResults[sub]; // Delete removed subjects + else { + for (let ses in globalResults[sub]) { + const sesRef = subRef[ses]; - if (!sesRef) - delete globalResults[sub][ses]; // Delete removed sessions - else { - const globalSesRef = globalResults[sub][ses]; + if (!sesRef) + delete globalResults[sub][ses]; // Delete removed sessions + else { + const globalSesRef = globalResults[sub][ses]; - for (let name in globalSesRef.source_data) { - if (!sesRef.source_data[name]) delete globalSesRef.source_data[name]; // Delete removed interfaces - } + for (let name in globalSesRef.source_data) { + if (!sesRef.source_data[name]) delete globalSesRef.source_data[name]; // Delete removed interfaces } } - - if (Object.keys(globalResults[sub]).length === 0) delete globalResults[sub]; // Delete empty subjects } + + if (Object.keys(globalResults[sub]).length === 0) delete globalResults[sub]; // Delete empty subjects } } } + }; footer = { onNext: async () => { + await this.save(); // Save in case the request fails - if (!this.optional.toggled) { - const message = "Please select an option."; - this.notify(message, "error"); - throw new Error(message); - } + await this.form.validate() + + // if (!this.optional.toggled) { + // const message = "Please select an option."; + // this.notify(message, "error"); + // throw new Error(message); + // } return this.to(1); }, @@ -390,21 +402,8 @@ export class GuidedPathExpansionPage extends Page { localState = {}; render() { - this.optional = new OptionalSection({ - header: "Will you locate your files programmatically?", - description: infoBox, - onChange: () => (this.unsavedUpdates = "conversions"), - // altContent: this.altForm, - }); - const structureState = (this.localState = merge(this.info.globalState.structure, { - results: {}, - keep_existing_data: true, - })); - - const state = structureState.state; - - this.optional.state = state; + const structureState = this.#initialize() // Require properties for all sources const generatedSchema = { type: "object", properties: {}, additionalProperties: false }; @@ -427,7 +426,7 @@ export class GuidedPathExpansionPage extends Page { } structureState.schema = generatedSchema; - this.optional.requestUpdate(); + // this.optional.requestUpdate(); const form = (this.form = new JSONSchemaForm({ ...structureState, @@ -515,32 +514,9 @@ export class GuidedPathExpansionPage extends Page { }, })); - this.optional.innerHTML = ""; - - this.optional.style.paddingTop = "10px"; - - this.dataManagementForm = new JSONSchemaForm({ - results: { keep_existing_data: structureState.keep_existing_data }, - onUpdate: () => (this.unsavedUpdates = "conversions"), - schema: { - type: "object", - additionalProperties: false, - properties: { - keep_existing_data: { - type: "boolean", - description: "Maintain data for subjects / sessions that are not located.", - }, - }, - }, - }); - - this.optional.append(form); - form.style.width = "100%"; - this.scrollTop = "300px"; - - return html`${this.dataManagementForm}${this.optional}`; + return html`${infoBox}

${form}`; } } 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 93fb40053..99fe9070a 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -42,21 +42,27 @@ export class GuidedSourceDataPage extends ManagedPage { merge(this.localState, this.info.globalState); }; + #globalButton = new Button({ + icon: globalIcon, + label: "Edit Global Source Data", + onClick: () => { + this.#globalModal.form.results = structuredClone(this.info.globalState.project.SourceData ?? {}); + this.#globalModal.open = true; + }, + }) + header = { - controls: [ - new Button({ - icon: globalIcon, - label: "Edit Global Source Data", - onClick: () => { - this.#globalModal.form.results = structuredClone(this.info.globalState.project.SourceData ?? {}); - this.#globalModal.open = true; - }, - }), - ], + controls: [ this.#globalButton ], subtitle: "Specify the file and folder locations on your local system for each interface, as well as any additional details that might be required.", }; + workflow = { + multiple_sessions: { + elements: [ this.#globalButton ], + } + } + footer = { onNext: async () => { await this.save(); // Save in case the conversion fails 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 44a0dfbb0..ca4e8604b 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -18,18 +18,30 @@ export class GuidedSubjectsPage extends Page { super(...args); } + #addButton = new Button({ + label: "Add Subject", + onClick: () => this.table.table.alter("insert_row_below") + }); + + #globalButton = new Button({ + icon: globalIcon, + label: "Edit Global Metadata", + onClick: () => { + this.#globalModal.form.results = structuredClone(this.info.globalState.project.Subject ?? {}); + this.#globalModal.open = true; + }, + }) + + workflow = { + multiple_sessions: { + elements: [ this.#globalButton, this.#addButton ], + } + } + header = { subtitle: "Enter all metadata known about each experiment subject", - controls: [ - new Button({ - icon: globalIcon, - label: "Edit Global Metadata", - onClick: () => { - this.#globalModal.form.results = structuredClone(this.info.globalState.project.Subject ?? {}); - this.#globalModal.open = true; - }, - }), - ], + controls: [ this.#globalButton ], + }; // Abort save if subject structure is invalid @@ -72,8 +84,6 @@ export class GuidedSubjectsPage extends Page { footer = {}; updated() { - const add = this.query("#addButton"); - add.onclick = () => this.table.table.alter("insert_row_below"); super.updated(); // Call if updating data } @@ -104,6 +114,7 @@ export class GuidedSubjectsPage extends Page { } render() { + const subjects = (this.localState = structuredClone(this.info.globalState.subjects ?? {})); // Ensure all the proper subjects are in the global state @@ -117,6 +128,10 @@ export class GuidedSubjectsPage extends Page { subjects[subject].sessions = sessions; } + const contextMenuConfig = { ignore: [ "row_below" ] }; + + if (!this.workflow.multiple_sessions.value) contextMenuConfig.ignore.push("remove_row"); + this.table = new Table({ schema: { type: "array", @@ -126,9 +141,7 @@ export class GuidedSubjectsPage extends Page { globals: this.info.globalState.project.Subject, keyColumn: "subject_id", validateEmptyCells: ["subject_id", "sessions"], - contextMenu: { - ignore: ["row_below"], - }, + contextMenu: contextMenuConfig, onThrow: (message, type) => this.notify(message, type), onOverride: (name) => { this.notify(`${header(name)} has been overridden with a global value.`, "warning", 3000); @@ -157,7 +170,7 @@ export class GuidedSubjectsPage extends Page { return html`
${this.table}
- Add Subject + ${this.#addButton}
`; } diff --git a/src/renderer/src/stories/pages/guided-mode/setup/Preform.js b/src/renderer/src/stories/pages/guided-mode/setup/Preform.js new file mode 100644 index 000000000..5bc029c56 --- /dev/null +++ b/src/renderer/src/stories/pages/guided-mode/setup/Preform.js @@ -0,0 +1,120 @@ +import { html } from "lit"; +import { JSONSchemaForm } from "../../../JSONSchemaForm.js"; +import { Page } from "../../Page.js"; +import { onThrow } from "../../../../errors"; + +// ------------------------------------------------------------------------------ +// ------------------------ Preform Configuration ------------------------------- +// ------------------------------------------------------------------------------ + +const questions = { + multiple_sessions: { + type: "boolean", + title: "Will this pipeline be run on multiple sessions?", + default: false, + }, + locate_data: { + type: "boolean", + title: "Would you like to locate these sessions programmatically?", + dependencies: [ "multiple_sessions" ], + default: false, + }, +} + +// ------------------------------------------------------------------------------------------- +// ------------------------ Derived from the above information ------------------------------- +// ------------------------------------------------------------------------------------------- + +const dependents = Object.entries(questions).reduce((acc, [ name, info ]) => { + acc[name] = []; + + if (info.dependencies) { + info.dependencies.forEach((dep) => { + if (!acc[dep]) acc[dep] = []; + acc[dep].push(name); + }); + } + return acc; +}, {}); + +const projectWorkflowSchema = { + type: "object", + properties: Object.entries(questions).reduce((acc, [ name, info ]) => { + acc[name] = info + return acc; + }, {}) +} + +// ---------------------------------------------------------------------- +// ------------------------ Preform Class ------------------------------- +// ---------------------------------------------------------------------- + +export class GuidedPreform extends Page { + constructor(...args) { + super(...args); + this.updateForm(); // Register nested pages on creation—not just with data + } + + state = {}; + + header = { + subtitle: "Answer the following questions to simplify your workflow through the GUIDE", + }; + + footer = { + onNext: async () => { + await this.form.validate(); + this.info.globalState.project.workflow = this.state; + this.save(); + return this.to(1); + }, + }; + + updateForm = () => { + + const schema = structuredClone(projectWorkflowSchema) + const projectState = this.info.globalState.project ?? {}; + if (!projectState.workflow) projectState.workflow = {}; + this.state = structuredClone(projectState.workflow) + + this.form = new JSONSchemaForm({ + schema, + results: this.state, + validateOnChange: function (name, parent, path, value) { + + dependents[name].forEach((dependent) => { + const dependencies = questions[dependent].dependencies; + const dependentEl = this.inputs[dependent]; + if (dependencies.every((dep) => parent[dep])) dependentEl.removeAttribute("disabled"); + else { + dependentEl.updateData(false); + dependentEl.setAttribute("disabled", true); + } + }) + + }, + onUpdate: () => (this.unsavedUpdates = true), + onThrow, + groups: [ + { + name: 'Session Workflow', + properties: [ [ "multiple_sessions" ], [ "locate_data" ] ], + } + ] + }); + + return this.form; + }; + + render() { + this.state = {}; // Clear local state on each render + + const form = this.updateForm(); + form.style.width = "100%"; + + return html` ${form} `; + } +} + +customElements.get("nwbguide-guided-preform-page") || + customElements.define("nwbguide-guided-preform-page", GuidedPreform); diff --git a/src/renderer/src/stories/pages/settings/SettingsPage.js b/src/renderer/src/stories/pages/settings/SettingsPage.js index f114e33be..6a210216b 100644 --- a/src/renderer/src/stories/pages/settings/SettingsPage.js +++ b/src/renderer/src/stories/pages/settings/SettingsPage.js @@ -66,10 +66,7 @@ function saveNewPipelineFromYaml(name, sourceData, rootFolder) { return acc; }, {}), - structure: { - keep_existing_data: true, - state: false, - }, + structure: {}, results: { [subjectId]: sessions.reduce((acc, sessionId) => { From 4621642a64b314f874502734d0c3be3cc632a93d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:49:19 +0000 Subject: [PATCH 02/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/progress/operations.js | 1 - src/renderer/src/stories/Dashboard.js | 14 +++----- src/renderer/src/stories/JSONSchemaInput.js | 18 +++++----- src/renderer/src/stories/Main.js | 19 +++++----- .../pages/guided-mode/data/GuidedMetadata.js | 10 +++--- .../guided-mode/data/GuidedPathExpansion.js | 29 ++++++--------- .../guided-mode/data/GuidedSourceData.js | 10 +++--- .../pages/guided-mode/setup/GuidedSubjects.js | 16 ++++----- .../pages/guided-mode/setup/Preform.js | 35 +++++++++---------- 9 files changed, 67 insertions(+), 85 deletions(-) diff --git a/src/renderer/src/progress/operations.js b/src/renderer/src/progress/operations.js index 6d5990f58..83cc9ce66 100644 --- a/src/renderer/src/progress/operations.js +++ b/src/renderer/src/progress/operations.js @@ -12,7 +12,6 @@ export const remove = (name) => { } else localStorage.removeItem(progressFilePathToDelete); if (fs) { - // delete default preview location fs.rmSync(joinPath(previewSaveFolderPath, name), { recursive: true, force: true }); diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index 443603e0f..aa0104fc1 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -216,7 +216,6 @@ export class Dashboard extends LitElement { this.page.set(toPass, false); this.page.checkSyncState().then(() => { - const projectName = info.globalState?.project?.name; this.subSidebar.header = projectName @@ -231,15 +230,14 @@ export class Dashboard extends LitElement { if (this.#transitionPromise.value) this.#transitionPromise.trigger(page); // This ensures calls to page.to() can be properly awaited until the next page is ready const { skipped } = this.subSidebar.sections[info.section].pages[info.id]; - if ( skipped ) { - + if (skipped) { // Run skip functions Object.entries(page.workflow).forEach(([key, state]) => { - if (typeof state.skip === 'function') state.skip() + if (typeof state.skip === "function") state.skip(); }); // Skip right over the page if configured as such - if (previous.info.previous === this.page) this.back() + if (previous.info.previous === this.page) this.back(); else this.next(); } }); @@ -279,13 +277,12 @@ export class Dashboard extends LitElement { const workflow = page.workflow; const workflowValues = globalState.project?.workflow ?? {}; const skipped = Object.entries(workflow).some(([key, state]) => { - if (!workflowValues[key]) return state.skip + if (!workflowValues[key]) return state.skip; }); - pageState.skipped = skipped + pageState.skipped = skipped; } - if (page.info.pages) this.#getSections(page.info.pages, globalState); // Show all states if (!("visited" in pageState)) pageState.visited = false; @@ -337,7 +334,6 @@ export class Dashboard extends LitElement { const page = this.getPage(this.pagesById[id]); if (page) { - const { id, label } = page.info; const queries = new URLSearchParams(window.location.search); queries.set("page", id); diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 5b3d52027..1719dae89 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -515,16 +515,16 @@ export class JSONSchemaInput extends LitElement { // Update the actual input element const inputElement = this.getElement(); if (!inputElement) return false; - + if (inputElement.type === "checkbox") inputElement.checked = value; - else if (inputElement.classList.contains("list")) { - const list = inputElement.children[0]; - inputElement.children[0].items = this.#mapToList({ - value, - list, - }); // NOTE: Make sure this is correct - } else if (inputElement instanceof Search) inputElement.shadowRoot.querySelector("input").value = value; - else inputElement.value = value; + else if (inputElement.classList.contains("list")) { + const list = inputElement.children[0]; + inputElement.children[0].items = this.#mapToList({ + value, + list, + }); // NOTE: Make sure this is correct + } else if (inputElement instanceof Search) inputElement.shadowRoot.querySelector("input").value = value; + else inputElement.value = value; } const { path: fullPath } = this; diff --git a/src/renderer/src/stories/Main.js b/src/renderer/src/stories/Main.js index 1bfdf5e9a..00a780486 100644 --- a/src/renderer/src/stories/Main.js +++ b/src/renderer/src/stories/Main.js @@ -61,22 +61,22 @@ export class Main extends LitElement { page.updatePages = this.updatePages; // Constrain based on workflow configuration - const workflowConfig = page.workflow ?? ( page.workflow = {} ) + const workflowConfig = page.workflow ?? (page.workflow = {}); const workflowValues = page.info.globalState?.project?.workflow ?? {}; - Object.entries(workflowConfig).forEach(([ key, state ]) => { - workflowConfig[key].value = workflowValues[key] + Object.entries(workflowConfig).forEach(([key, state]) => { + workflowConfig[key].value = workflowValues[key]; - const value = workflowValues[key] + const value = workflowValues[key]; if (state.elements) { - const elements = state.elements - if (value) elements.forEach((el) => el.removeAttribute("hidden")) - else elements.forEach((el) => el.setAttribute("hidden", true)) + const elements = state.elements; + if (value) elements.forEach((el) => el.removeAttribute("hidden")); + else elements.forEach((el) => el.setAttribute("hidden", true)); } - }) + }); - page.requestUpdate() // Ensure the page is re-rendered with new workflow configurations + page.requestUpdate(); // Ensure the page is re-rendered with new workflow configurations if (this.content) this.toRender = toRender.page ? toRender : { page }; else this.#queue.push(page); @@ -98,7 +98,6 @@ export class Main extends LitElement { } render() { - let { page = "", sections = {} } = this.toRender ?? {}; let footer = page?.footer; // Page-specific footer 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 5ddad9e60..67881f10f 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -96,16 +96,16 @@ export class GuidedMetadataPage extends ManagedPage { this.#globalModal.form.results = structuredClone(this.info.globalState.project); this.#globalModal.open = true; }, - }) + }); workflow = { multiple_sessions: { - elements: [ this.#globalButton ], - } - } + elements: [this.#globalButton], + }, + }; header = { - controls: [ this.#globalButton ], + controls: [this.#globalButton], subtitle: "Edit all metadata for this conversion at the session level", }; 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 2c9390a69..0c4c616a5 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -236,8 +236,7 @@ function getFiles(dir) { } export class GuidedPathExpansionPage extends Page { - - #notification + #notification; constructor(...args) { super(...args); @@ -247,16 +246,15 @@ export class GuidedPathExpansionPage extends Page { subtitle: "Automatic source data detection for multiple subjects / sessions", }; - #initialize = () => this.localState = merge(this.info.globalState.structure, { results: {} }) - + #initialize = () => (this.localState = merge(this.info.globalState.structure, { results: {} })); + workflow = { locate_data: { skip: () => { - - this.#initialize() + this.#initialize(); const globalState = this.info.globalState; merge({ structure: this.localState }, globalState); // Merge the actual entries into the structure - + // Force single subject/session if not keeping existing data if (!globalState.results) { const existingMetadata = @@ -289,13 +287,11 @@ export class GuidedPathExpansionPage extends Page { }, }; } - - } - } - } + }, + }, + }; beforeSave = async () => { - const globalState = this.info.globalState; merge({ structure: this.localState }, globalState); // Merge the actual entries into the structure @@ -356,15 +352,13 @@ export class GuidedPathExpansionPage extends Page { } } } - }; footer = { onNext: async () => { - await this.save(); // Save in case the request fails - await this.form.validate() + await this.form.validate(); // if (!this.optional.toggled) { // const message = "Please select an option."; @@ -402,8 +396,7 @@ export class GuidedPathExpansionPage extends Page { localState = {}; render() { - - const structureState = this.#initialize() + const structureState = this.#initialize(); // Require properties for all sources const generatedSchema = { type: "object", properties: {}, additionalProperties: false }; @@ -516,7 +509,7 @@ export class GuidedPathExpansionPage extends Page { form.style.width = "100%"; - return html`${infoBox}

${form}`; + return html`${infoBox}

${form}`; } } 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 99fe9070a..fef1c2cfa 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -49,19 +49,19 @@ export class GuidedSourceDataPage extends ManagedPage { this.#globalModal.form.results = structuredClone(this.info.globalState.project.SourceData ?? {}); this.#globalModal.open = true; }, - }) + }); header = { - controls: [ this.#globalButton ], + controls: [this.#globalButton], subtitle: "Specify the file and folder locations on your local system for each interface, as well as any additional details that might be required.", }; workflow = { multiple_sessions: { - elements: [ this.#globalButton ], - } - } + elements: [this.#globalButton], + }, + }; footer = { onNext: async () => { 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 ca4e8604b..c6cb1a34d 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -20,7 +20,7 @@ export class GuidedSubjectsPage extends Page { #addButton = new Button({ label: "Add Subject", - onClick: () => this.table.table.alter("insert_row_below") + onClick: () => this.table.table.alter("insert_row_below"), }); #globalButton = new Button({ @@ -30,18 +30,17 @@ export class GuidedSubjectsPage extends Page { this.#globalModal.form.results = structuredClone(this.info.globalState.project.Subject ?? {}); this.#globalModal.open = true; }, - }) + }); workflow = { multiple_sessions: { - elements: [ this.#globalButton, this.#addButton ], - } - } + elements: [this.#globalButton, this.#addButton], + }, + }; header = { subtitle: "Enter all metadata known about each experiment subject", - controls: [ this.#globalButton ], - + controls: [this.#globalButton], }; // Abort save if subject structure is invalid @@ -114,7 +113,6 @@ export class GuidedSubjectsPage extends Page { } render() { - const subjects = (this.localState = structuredClone(this.info.globalState.subjects ?? {})); // Ensure all the proper subjects are in the global state @@ -128,7 +126,7 @@ export class GuidedSubjectsPage extends Page { subjects[subject].sessions = sessions; } - const contextMenuConfig = { ignore: [ "row_below" ] }; + const contextMenuConfig = { ignore: ["row_below"] }; if (!this.workflow.multiple_sessions.value) contextMenuConfig.ignore.push("remove_row"); diff --git a/src/renderer/src/stories/pages/guided-mode/setup/Preform.js b/src/renderer/src/stories/pages/guided-mode/setup/Preform.js index 5bc029c56..1d5736c87 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/Preform.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/Preform.js @@ -16,16 +16,16 @@ const questions = { locate_data: { type: "boolean", title: "Would you like to locate these sessions programmatically?", - dependencies: [ "multiple_sessions" ], + dependencies: ["multiple_sessions"], default: false, }, -} +}; // ------------------------------------------------------------------------------------------- // ------------------------ Derived from the above information ------------------------------- // ------------------------------------------------------------------------------------------- -const dependents = Object.entries(questions).reduce((acc, [ name, info ]) => { +const dependents = Object.entries(questions).reduce((acc, [name, info]) => { acc[name] = []; if (info.dependencies) { @@ -39,11 +39,11 @@ const dependents = Object.entries(questions).reduce((acc, [ name, info ]) => { const projectWorkflowSchema = { type: "object", - properties: Object.entries(questions).reduce((acc, [ name, info ]) => { - acc[name] = info + properties: Object.entries(questions).reduce((acc, [name, info]) => { + acc[name] = info; return acc; - }, {}) -} + }, {}), +}; // ---------------------------------------------------------------------- // ------------------------ Preform Class ------------------------------- @@ -71,36 +71,33 @@ export class GuidedPreform extends Page { }; updateForm = () => { - - const schema = structuredClone(projectWorkflowSchema) + const schema = structuredClone(projectWorkflowSchema); const projectState = this.info.globalState.project ?? {}; if (!projectState.workflow) projectState.workflow = {}; - this.state = structuredClone(projectState.workflow) + this.state = structuredClone(projectState.workflow); this.form = new JSONSchemaForm({ schema, results: this.state, validateOnChange: function (name, parent, path, value) { - - dependents[name].forEach((dependent) => { + dependents[name].forEach((dependent) => { const dependencies = questions[dependent].dependencies; - const dependentEl = this.inputs[dependent]; + const dependentEl = this.inputs[dependent]; if (dependencies.every((dep) => parent[dep])) dependentEl.removeAttribute("disabled"); else { dependentEl.updateData(false); dependentEl.setAttribute("disabled", true); } - }) - + }); }, onUpdate: () => (this.unsavedUpdates = true), onThrow, groups: [ { - name: 'Session Workflow', - properties: [ [ "multiple_sessions" ], [ "locate_data" ] ], - } - ] + name: "Session Workflow", + properties: [["multiple_sessions"], ["locate_data"]], + }, + ], }); return this.form; From cfcfc3142d48b0297bc97d1abd475c69d08bfa58 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 6 Mar 2024 12:53:16 -0800 Subject: [PATCH 03/10] Do not use global overrides if not editable --- .../pages/guided-mode/data/GuidedMetadata.js | 6 +++++- .../pages/guided-mode/data/GuidedSourceData.js | 5 ++++- .../src/stories/pages/guided-mode/data/utils.js | 13 +++++++++---- .../pages/guided-mode/setup/GuidedSubjects.js | 6 ++++-- 4 files changed, 22 insertions(+), 8 deletions(-) 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 5ddad9e60..36963b444 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -151,6 +151,9 @@ export class GuidedMetadataPage extends ManagedPage { } createForm = ({ subject, session, info }) => { + + const hasMultipleSessions = this.workflow.multiple_sessions.value + // const results = createResults({ subject, info }, this.info.globalState); const { globalState } = this.info; @@ -158,7 +161,7 @@ export class GuidedMetadataPage extends ManagedPage { const results = info.metadata; // Edited form info // Define the appropriate global metadata to fill empty values in the form - const aggregateGlobalMetadata = resolveGlobalOverrides(subject, globalState); + const aggregateGlobalMetadata = resolveGlobalOverrides(subject, globalState, hasMultipleSessions); // Define the correct instance identifier const instanceId = `sub-${subject}/ses-${session}`; @@ -223,6 +226,7 @@ export class GuidedMetadataPage extends ManagedPage { ); } + // Create the form const form = new JSONSchemaForm({ identifier: instanceId, 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 99fe9070a..69c4ebfef 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -157,6 +157,9 @@ export class GuidedSourceDataPage extends ManagedPage { }; createForm = ({ subject, session, info }) => { + + const hasMultipleSessions = this.workflow.multiple_sessions.value + const instanceId = `sub-${subject}/ses-${session}`; const schema = this.info.globalState.schema.source_data; @@ -168,7 +171,7 @@ export class GuidedSourceDataPage extends ManagedPage { results: info.source_data, emptyMessage: "No source data required for this session.", ignore: propsToIgnore, - globals: this.info.globalState.project.SourceData, + globals: hasMultipleSessions ? this.info.globalState.project.SourceData : undefined, onOverride: (name) => { this.notify(`${header(name)} has been overridden with a global value.`, "warning", 3000); }, diff --git a/src/renderer/src/stories/pages/guided-mode/data/utils.js b/src/renderer/src/stories/pages/guided-mode/data/utils.js index 6593aba50..0124ae793 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/utils.js +++ b/src/renderer/src/stories/pages/guided-mode/data/utils.js @@ -24,15 +24,20 @@ export const getInfoFromId = (key) => { return { subject, session }; }; -export function resolveGlobalOverrides(subject, globalState) { +export function resolveGlobalOverrides(subject, globalState, resolveMultiSessionOverrides = true) { const subjectMetadataCopy = { ...globalState.subjects[subject] }; delete subjectMetadataCopy.sessions; // Remove extra key from metadata - const overrides = structuredClone(globalState.project ?? {}); // Copy project-wide metadata + if (resolveMultiSessionOverrides) { + const overrides = structuredClone(globalState.project ?? {}); // Copy project-wide metadata - merge(subjectMetadataCopy, overrides.Subject ?? (overrides.Subject = {})); // Ensure Subject exists + merge(subjectMetadataCopy, overrides.Subject ?? (overrides.Subject = {})); // Ensure Subject exists - return overrides; + return overrides + } + + return { Subject: subjectMetadataCopy } + } const isPatternResult = Symbol("ispatternresult"); 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 ca4e8604b..2b9a22d69 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -115,6 +115,8 @@ export class GuidedSubjectsPage extends Page { render() { + const hasMultipleSessions = this.workflow.multiple_sessions.value + const subjects = (this.localState = structuredClone(this.info.globalState.subjects ?? {})); // Ensure all the proper subjects are in the global state @@ -130,7 +132,7 @@ export class GuidedSubjectsPage extends Page { const contextMenuConfig = { ignore: [ "row_below" ] }; - if (!this.workflow.multiple_sessions.value) contextMenuConfig.ignore.push("remove_row"); + if (!hasMultipleSessions) contextMenuConfig.ignore.push("remove_row"); this.table = new Table({ schema: { @@ -138,7 +140,7 @@ export class GuidedSubjectsPage extends Page { items: getSubjectSchema(), }, data: subjects, - globals: this.info.globalState.project.Subject, + globals: hasMultipleSessions ? this.info.globalState.project.Subject : undefined, keyColumn: "subject_id", validateEmptyCells: ["subject_id", "sessions"], contextMenu: contextMenuConfig, From 3a931ab5ccc8f8ca2e6846b905be66af3f62a1ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 20:54:00 +0000 Subject: [PATCH 04/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../src/stories/pages/guided-mode/data/GuidedMetadata.js | 4 +--- .../src/stories/pages/guided-mode/data/GuidedSourceData.js | 3 +-- src/renderer/src/stories/pages/guided-mode/data/utils.js | 5 ++--- .../src/stories/pages/guided-mode/setup/GuidedSubjects.js | 3 +-- 4 files changed, 5 insertions(+), 10 deletions(-) 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 dce4de307..88f0035bd 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -151,8 +151,7 @@ export class GuidedMetadataPage extends ManagedPage { } createForm = ({ subject, session, info }) => { - - const hasMultipleSessions = this.workflow.multiple_sessions.value + const hasMultipleSessions = this.workflow.multiple_sessions.value; // const results = createResults({ subject, info }, this.info.globalState); @@ -226,7 +225,6 @@ export class GuidedMetadataPage extends ManagedPage { ); } - // Create the form const form = new JSONSchemaForm({ identifier: instanceId, 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 990ca6e33..3d27ee62e 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -157,8 +157,7 @@ export class GuidedSourceDataPage extends ManagedPage { }; createForm = ({ subject, session, info }) => { - - const hasMultipleSessions = this.workflow.multiple_sessions.value + const hasMultipleSessions = this.workflow.multiple_sessions.value; const instanceId = `sub-${subject}/ses-${session}`; diff --git a/src/renderer/src/stories/pages/guided-mode/data/utils.js b/src/renderer/src/stories/pages/guided-mode/data/utils.js index 0124ae793..8dff00a62 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/utils.js +++ b/src/renderer/src/stories/pages/guided-mode/data/utils.js @@ -33,11 +33,10 @@ export function resolveGlobalOverrides(subject, globalState, resolveMultiSession merge(subjectMetadataCopy, overrides.Subject ?? (overrides.Subject = {})); // Ensure Subject exists - return overrides + return overrides; } - return { Subject: subjectMetadataCopy } - + return { Subject: subjectMetadataCopy }; } const isPatternResult = Symbol("ispatternresult"); 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 36f0bfe8e..04cbdfce1 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -113,8 +113,7 @@ export class GuidedSubjectsPage extends Page { } render() { - - const hasMultipleSessions = this.workflow.multiple_sessions.value + const hasMultipleSessions = this.workflow.multiple_sessions.value; const subjects = (this.localState = structuredClone(this.info.globalState.subjects ?? {})); From 14ea585c47ed5cae077a0c44c6bda0c68cf887e3 Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Wed, 6 Mar 2024 13:58:17 -0800 Subject: [PATCH 05/10] Pivot to single-session tests (skips locate data) --- src/renderer/src/stories/Dashboard.js | 2 +- .../guided-mode/data/GuidedSourceData.js | 9 ++ tests/e2e.test.ts | 106 ++++++++++++++---- 3 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index aa0104fc1..b5eaa3602 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -229,7 +229,7 @@ export class Dashboard extends LitElement { if (this.#transitionPromise.value) this.#transitionPromise.trigger(page); // This ensures calls to page.to() can be properly awaited until the next page is ready - const { skipped } = this.subSidebar.sections[info.section].pages[info.id]; + const { skipped } = this.subSidebar.sections[info.section]?.pages?.[info.id] ?? {}; if (skipped) { // Run skip functions Object.entries(page.workflow).forEach(([key, state]) => { 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 990ca6e33..a239f404e 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -220,6 +220,15 @@ export class GuidedSourceDataPage extends ManagedPage { if (this.#globalModal) this.#globalModal.remove(); } + updated() { + const dashboard = document.querySelector('nwb-dashboard') + const page = dashboard.page + setTimeout(() => { + console.log(page.forms[0].form.accordions["SpikeGLX Recording"]) + }) + console.log(page.forms[0].form.accordions) + } + render() { this.localState = { results: structuredClone(this.info.globalState.results ?? {}) }; diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 66ea5650b..d283b44bf 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -16,7 +16,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const screenshotPath = join(__dirname, 'screenshots') const guideRootPath = join(homedir(), paths.root) const testRootPath = join(guideRootPath, '.test') -const testDataPath = join(testRootPath, 'test-data') +const testDataRootPath = join(testRootPath, 'test-data') +const testDataPath = join(testDataRootPath, 'data') +const testDatasetPath = join(testDataRootPath, 'dataset') const alwaysDelete = [ join(testRootPath, 'pipelines'), @@ -31,15 +33,33 @@ const alwaysDelete = [ // ----------------------------------------------------------------------- const testInterfaceInfo = { - SpikeGLXRecordingInterface: { - format: '{subject_id}/{subject_id}_{session_id}/{subject_id}_{session_id}_g0/{subject_id}_{session_id}_g0_imec0/{subject_id}_{session_id}_g0_t0.imec0.ap.bin' + common: { + SpikeGLXRecordingInterface: { + id: 'SpikeGLX Recording', + }, + PhySortingInterface: { + id: 'Phy Sorting' + } }, - PhySortingInterface: { - format: '{subject_id}/{subject_id}_{session_id}/{subject_id}_{session_id}_phy' + multi: { + SpikeGLXRecordingInterface: { + format: '{subject_id}/{subject_id}_{session_id}/{subject_id}_{session_id}_g0/{subject_id}_{session_id}_g0_imec0/{subject_id}_{session_id}_g0_t0.imec0.ap.bin' + }, + PhySortingInterface: { + format: '{subject_id}/{subject_id}_{session_id}/{subject_id}_{session_id}_phy' + } + }, + single: { + SpikeGLXRecordingInterface: { + file_path: join(testDataPath, 'spikeglx', 'Session1_g0', 'Session1_g0_imec0', 'Session1_g0_t0.imec0.ap.bin') + }, + PhySortingInterface: { + folder_path: join(testDataPath, 'phy') + } } } -const regenerateTestData = !existsSync(testDataPath) || false // Generate only if doesn't exist +const regenerateTestData = !existsSync(testDataRootPath) || false // Generate only if doesn't exist const dandiInfo = { id: '212750', @@ -57,7 +77,7 @@ if (skipUpload) console.log('No DANDI API key provided. Will skip upload step... beforeAll(() => { if (regenerateTestData) { - if (existsSync(testDataPath)) rmSync(testDataPath, { recursive: true }) + if (existsSync(testDataRootPath)) rmSync(testDataRootPath, { recursive: true }) } alwaysDelete.forEach(path => existsSync(path) ? rmSync(path, { recursive: true }) : '') @@ -183,7 +203,15 @@ describe('E2E Test', () => { await takeScreenshot('valid-name', 300) // Advance to formats page - await toNextPage('structure') + await toNextPage('workflow') + + }) + + test('View the pre-form workflow page', async () => { + + await takeScreenshot('workflow-page', 300) + await toNextPage('structure') + }) @@ -211,38 +239,35 @@ describe('E2E Test', () => { const dashboard = document.querySelector('nwb-dashboard') const page = dashboard.page const [name, info] = Object.entries(interfaces)[0] - page.list.add({ key: name, value: name }); + page.list.add({ key: info.id, value: name }); page.searchModal.toggle(false); - }, testInterfaceInfo) + }, testInterfaceInfo.common) await takeScreenshot('interface-added', 1000) await evaluate((interfaces) => { const dashboard = document.querySelector('nwb-dashboard') const page = dashboard.page - Object.keys(interfaces).slice(1).forEach(name => page.list.add({ key: name, value: name })) - }, testInterfaceInfo) + Object.entries(interfaces).slice(1).forEach(([ name, info ]) => page.list.add({ key: info.id, value: name })) + }, testInterfaceInfo.common) await takeScreenshot('all-interfaces-added') - await toNextPage('locate') + // await toNextPage('locate') + await toNextPage('subjects') }) - test('Locate all your source data programmatically', async () => { - - await takeScreenshot('pathexpansion-page', 300) + // NOTE: Locate data is skipped in single session mode + test.skip('Locate all your source data programmatically', async () => { await evaluate(async () => { const dashboard = document.querySelector('nwb-dashboard') const page = dashboard.page - page.optional.yes.onClick() - Object.values(page.form.accordions).forEach(accordion => accordion.toggle(true)) + }, testInterfaceInfo.multi) - }, testInterfaceInfo) - - await takeScreenshot('pathexpansion-selected') + await takeScreenshot('pathexpansion-page') // Fill out the path expansion information await evaluate((interfaceInfo, basePath) => { @@ -260,8 +285,8 @@ describe('E2E Test', () => { dashboard.main.querySelector('main > section').scrollTop = 200 }, - testInterfaceInfo, - join(testDataPath, 'dataset') + testInterfaceInfo.multi, + testDatasetPath ) @@ -321,13 +346,46 @@ describe('E2E Test', () => { }) - test('Review source data information', async () => { + // NOTE: This isn't pre-filled in single session mode + test.skip('Review source data information', async () => { await takeScreenshot('sourcedata-page', 100) await toNextPage('metadata') }) + test('Specify source data information', async () => { + + await references.page.evaluate(async () => { + const dashboard = document.querySelector('nwb-dashboard') + const page = dashboard.page + Object.values(page.forms[0].form).forEach(accordion => accordion.toggle(true)) + }) + + await takeScreenshot('sourcedata-page', 100) + + await references.page.evaluate(({ single, common }) => { + const dashboard = document.querySelector('nwb-dashboard') + const page = dashboard.page + + Object.entries(common).forEach(([name, info]) => { + const form = page.forms[0].form.forms[info.id] + + const interfaceInfo = single[name] + for (let key in single[name]) { + const input = form.getFormElement([ key ]) + input.updateData(interfaceInfo[key]) + } + }) + }, testInterfaceInfo) + + await takeScreenshot('sourcedata-page-specified', 100) + + + await toNextPage('metadata') + + }) + test('Review metadata', async () => { await takeScreenshot('metadata-page', 100) From ce1808c2422905f7d80140d63d4f73e1a4008fb5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 21:58:48 +0000 Subject: [PATCH 06/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../stories/pages/guided-mode/data/GuidedSourceData.js | 10 +++++----- tests/e2e.test.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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 e576f44bc..6bd47bc30 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -220,12 +220,12 @@ export class GuidedSourceDataPage extends ManagedPage { } updated() { - const dashboard = document.querySelector('nwb-dashboard') - const page = dashboard.page + const dashboard = document.querySelector("nwb-dashboard"); + const page = dashboard.page; setTimeout(() => { - console.log(page.forms[0].form.accordions["SpikeGLX Recording"]) - }) - console.log(page.forms[0].form.accordions) + console.log(page.forms[0].form.accordions["SpikeGLX Recording"]); + }); + console.log(page.forms[0].form.accordions); } render() { diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index d283b44bf..f0e5b70f8 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -208,7 +208,7 @@ describe('E2E Test', () => { }) test('View the pre-form workflow page', async () => { - + await takeScreenshot('workflow-page', 300) await toNextPage('structure') From cddd2fc3913c3c794ef7d65597ea590763a231ef Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Mon, 11 Mar 2024 16:53:00 -0700 Subject: [PATCH 07/10] Exclude subject table if single session --- src/renderer/src/stories/Dashboard.js | 4 ++-- src/renderer/src/stories/pages/guided-mode/data/utils.js | 2 +- .../stories/pages/guided-mode/setup/GuidedSubjects.js | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/stories/Dashboard.js b/src/renderer/src/stories/Dashboard.js index b5eaa3602..f4fda44ac 100644 --- a/src/renderer/src/stories/Dashboard.js +++ b/src/renderer/src/stories/Dashboard.js @@ -237,8 +237,8 @@ export class Dashboard extends LitElement { }); // Skip right over the page if configured as such - if (previous.info.previous === this.page) this.back(); - else this.next(); + if (previous.info.previous === this.page) this.page.onTransition(-1); + else this.page.onTransition(1); } }); } diff --git a/src/renderer/src/stories/pages/guided-mode/data/utils.js b/src/renderer/src/stories/pages/guided-mode/data/utils.js index 8dff00a62..df2f9e522 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/utils.js +++ b/src/renderer/src/stories/pages/guided-mode/data/utils.js @@ -25,7 +25,7 @@ export const getInfoFromId = (key) => { }; export function resolveGlobalOverrides(subject, globalState, resolveMultiSessionOverrides = true) { - const subjectMetadataCopy = { ...globalState.subjects[subject] }; + const subjectMetadataCopy = { ...globalState.subjects?.[subject] ?? {} }; delete subjectMetadataCopy.sessions; // Remove extra key from metadata if (resolveMultiSessionOverrides) { 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 162de5d32..d1d9fc912 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -43,6 +43,14 @@ export class GuidedSubjectsPage extends Page { controls: [this.#globalButton], }; + workflow = { + multiple_sessions: { + skip: () => { + + } + } + } + // Abort save if subject structure is invalid beforeSave = () => { try { @@ -113,6 +121,7 @@ export class GuidedSubjectsPage extends Page { } render() { + const hasMultipleSessions = this.workflow.multiple_sessions.value; const subjects = (this.localState = structuredClone(this.info.globalState.subjects ?? {})); From b4058ad35212f96a1dc5ebe86d75355f1e872c57 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 23:53:16 +0000 Subject: [PATCH 08/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/renderer/src/stories/pages/guided-mode/data/utils.js | 2 +- .../stories/pages/guided-mode/setup/GuidedSubjects.js | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/stories/pages/guided-mode/data/utils.js b/src/renderer/src/stories/pages/guided-mode/data/utils.js index df2f9e522..bbc3e0dbe 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/utils.js +++ b/src/renderer/src/stories/pages/guided-mode/data/utils.js @@ -25,7 +25,7 @@ export const getInfoFromId = (key) => { }; export function resolveGlobalOverrides(subject, globalState, resolveMultiSessionOverrides = true) { - const subjectMetadataCopy = { ...globalState.subjects?.[subject] ?? {} }; + const subjectMetadataCopy = { ...(globalState.subjects?.[subject] ?? {}) }; delete subjectMetadataCopy.sessions; // Remove extra key from metadata if (resolveMultiSessionOverrides) { 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 d1d9fc912..0f9dd7885 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -45,11 +45,9 @@ export class GuidedSubjectsPage extends Page { workflow = { multiple_sessions: { - skip: () => { - - } - } - } + skip: () => {}, + }, + }; // Abort save if subject structure is invalid beforeSave = () => { @@ -121,7 +119,6 @@ export class GuidedSubjectsPage extends Page { } render() { - const hasMultipleSessions = this.workflow.multiple_sessions.value; const subjects = (this.localState = structuredClone(this.info.globalState.subjects ?? {})); From a43d43d9384f6baeaf8a81c623c9a2a20ed8131b Mon Sep 17 00:00:00 2001 From: Garrett Michael Flynn Date: Tue, 12 Mar 2024 15:12:26 -0700 Subject: [PATCH 09/10] Fix tests given that the subject table is skipped --- tests/e2e.test.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index f0e5b70f8..ef0851cd2 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -59,6 +59,12 @@ const testInterfaceInfo = { } } +const subjectInfo = { + sex: 'M', + species: 'Mus musculus', + age: 'P30D' +} + const regenerateTestData = !existsSync(testDataRootPath) || false // Generate only if doesn't exist const dandiInfo = { @@ -254,7 +260,8 @@ describe('E2E Test', () => { await takeScreenshot('all-interfaces-added') // await toNextPage('locate') - await toNextPage('subjects') + // await toNextPage('subjects') + await toNextPage('sourcedata') }) @@ -296,7 +303,8 @@ describe('E2E Test', () => { }) - test('Provide subject information', async () => { + // NOTE: Subject information is skipped in single session mode + test.skip('Provide subject information', async () => { await takeScreenshot('subject-page', 300) @@ -328,12 +336,7 @@ describe('E2E Test', () => { const data = { ...table.data } for (let name in data) { - data[name] = { - ...data[name], - sex: 'M', - species: 'Mus musculus', - age: 'P30D' - } + data[name] = { ...data[name], ...subjectInfo } } table.data = data // This changes the render but not the update flag @@ -359,7 +362,7 @@ describe('E2E Test', () => { await references.page.evaluate(async () => { const dashboard = document.querySelector('nwb-dashboard') const page = dashboard.page - Object.values(page.forms[0].form).forEach(accordion => accordion.toggle(true)) + Object.values(page.forms[0].form.accordions).forEach(accordion => accordion.toggle(true)) }) await takeScreenshot('sourcedata-page', 100) @@ -396,6 +399,18 @@ describe('E2E Test', () => { page.forms[0].form.accordions["Subject"].toggle(true) }) + // Update for single session + await evaluate((subjectInfo) => { + const dashboard = document.querySelector('nwb-dashboard') + const page = dashboard.page + const form = page.forms[0].form.forms['Subject'] + + for (let key in subjectInfo) { + const input = form.getFormElement([ key ]) + input.updateData(subjectInfo[key]) + } + }, subjectInfo) + await takeScreenshot('metadata-open', 100) await toNextPage('inspect') From 5de2e669072b0ccf22f28e9d476e95261c12b5d4 Mon Sep 17 00:00:00 2001 From: Cody Baker <51133164+CodyCBakerPhD@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:47:42 -0400 Subject: [PATCH 10/10] Update .codespellrc --- .codespellrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codespellrc b/.codespellrc index 6b0aa92f0..5afc45ef0 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,3 @@ [codespell] -ignore-words-list= afterall +ignore-words-list= afterall,preform skip = .git,*.svg,package-lock.json,node_modules,*lotties*,nwb-guide.spec,prepare_pyinstaller_spec.py