diff --git a/src/renderer/src/stories/CodeBlock.js b/src/renderer/src/stories/CodeBlock.js new file mode 100644 index 000000000..6c1f8c73d --- /dev/null +++ b/src/renderer/src/stories/CodeBlock.js @@ -0,0 +1,35 @@ +import { LitElement, css, html } from "lit"; + +export class CodeBlock extends LitElement { + static get styles() { + return css` + :host { + display: block; + font-size: 85%; + background: #f2f1f1; + border-radius: 10px; + border: 1px solid gray; + overflow: hidden; + } + + pre { + overflow: auto; + padding: 5px 10px; + box-sizing: border-box; + user-select: text; + margin: 0; + } + `; + } + + constructor({ text = "" }) { + super(); + this.text = text; + } + + render() { + return html`
${this.text}
`; + } +} + +customElements.get("code-block") || customElements.define("code-block", CodeBlock); diff --git a/src/renderer/src/stories/InfoBox.js b/src/renderer/src/stories/InfoBox.js index 42130b898..c11b4c54b 100644 --- a/src/renderer/src/stories/InfoBox.js +++ b/src/renderer/src/stories/InfoBox.js @@ -76,37 +76,40 @@ export class InfoBox extends LitElement { `; } - constructor({ header = "Info", content, type = "info" } = {}) { + constructor({ header = "Info", content, type = "info", open = false } = {}) { super(); this.header = header; this.content = content; this.type = type; + this.open = open; } updated() { - const infoDropdowns = this.shadowRoot.querySelectorAll(".guided--info-dropdown"); - for (const infoDropdown of Array.from(infoDropdowns)) { - const infoTextElement = infoDropdown.querySelector("#header"); - - // Auto-add icons if they're not there - if (this.type === "info") infoTextElement.insertAdjacentHTML("beforebegin", `ℹ️`); - if (this.type === "warning") - infoTextElement.insertAdjacentHTML("beforebegin", ` ⚠️`); - - infoDropdown.onclick = () => { - const infoContainer = infoDropdown.nextElementSibling; - const infoContainerChevron = infoDropdown.querySelector("nwb-chevron"); - - const infoContainerIsopen = infoContainer.classList.contains("container-open"); - - if (infoContainerIsopen) { - infoContainerChevron.direction = "right"; - infoContainer.classList.remove("container-open"); - } else { - infoContainerChevron.direction = "bottom"; - infoContainer.classList.add("container-open"); - } - }; + const infoDropdown = this.shadowRoot.querySelector(".guided--info-dropdown"); + const infoTextElement = infoDropdown.querySelector("#header"); + + // Auto-add icons if they're not there + if (this.type === "info") infoTextElement.insertAdjacentHTML("beforebegin", `ℹ️`); + if (this.type === "warning") infoTextElement.insertAdjacentHTML("beforebegin", ` ⚠️`); + + const infoContainer = infoDropdown.nextElementSibling; + infoDropdown.onclick = () => this.onToggle(!infoContainer.classList.contains("container-open")); + + this.onToggle(); + } + + onToggle(open = this.open) { + const infoDropdown = this.shadowRoot.querySelector(".guided--info-dropdown"); + + const infoContainer = infoDropdown.nextElementSibling; + const infoContainerChevron = infoDropdown.querySelector("nwb-chevron"); + + if (open) { + infoContainerChevron.direction = "bottom"; + infoContainer.classList.add("container-open"); + } else { + infoContainerChevron.direction = "right"; + infoContainer.classList.remove("container-open"); } } diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index f8c82241a..0753e421b 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -96,15 +96,15 @@ hr { margin: 1em 0 1.5em 0; } -pre { - white-space: pre-wrap; /* Since CSS 2.1 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ - font-family: unset; - color: DimGray; -} + pre { + white-space: pre-wrap; /* Since CSS 2.1 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + font-family: unset; + color: DimGray; + } .required label:after { content: " *"; @@ -434,6 +434,7 @@ export class JSONSchemaForm extends LitElement { ${interactiveInput}
+
`; }; @@ -587,6 +588,8 @@ export class JSONSchemaForm extends LitElement { ? valid?.filter((info) => info.type === "error" || (isRequired && info.missing)) : []; + const info = Array.isArray(valid) ? valid?.filter((info) => info.type === "info") : []; + const hasLinks = this.#getLink(externalPath); if (hasLinks) { if (checkLinks) { @@ -612,6 +615,7 @@ export class JSONSchemaForm extends LitElement { // Clear old errors and warnings this.#clearMessages(fullPath, "errors"); this.#clearMessages(fullPath, "warnings"); + this.#clearMessages(fullPath, "info"); const isFunction = typeof valid === "function"; const isValid = @@ -629,6 +633,7 @@ export class JSONSchemaForm extends LitElement { // Show aggregated errors and warnings (if any) warnings.forEach((info) => this.#addMessage(fullPath, info, "warnings")); + info.forEach((info) => this.#addMessage(fullPath, info, "info")); if (isValid && errors.length === 0) { element.classList.remove("invalid"); diff --git a/src/renderer/src/stories/OptionalSection.js b/src/renderer/src/stories/OptionalSection.js index 90d27e901..15347c59f 100644 --- a/src/renderer/src/stories/OptionalSection.js +++ b/src/renderer/src/stories/OptionalSection.js @@ -7,7 +7,6 @@ export class OptionalSection extends LitElement { return css` :host { display: block; - text-align: center; } h2 { @@ -15,6 +14,10 @@ export class OptionalSection extends LitElement { margin-bottom: 15px; } + .optional-section__toggle { + padding-bottom: 20px; + } + .optional-section__content { text-align: left; } @@ -99,17 +102,15 @@ export class OptionalSection extends LitElement { render() { return html` -
-
- ${this.header ? html`

${this.header}

` : ""} -
${this.description}
-
${this.yes} ${this.no}
-
- - +
+ ${this.header ? html`

${this.header}

` : ""} +
${this.description}
+
${this.yes} ${this.no}
+
+ + `; } } 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 5e6d0beeb..21eae1594 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedPathExpansion.js @@ -11,6 +11,121 @@ import { onThrow } from "../../../../errors"; import pathExpansionSchema from "../../../../../../../schemas/json/path-expansion.schema.json" assert { type: "json" }; import { InfoBox } from "../../../InfoBox.js"; import { merge } from "../../utils.js"; +import { CodeBlock } from "../../../CodeBlock.js"; +import { List } from "../../../List"; +import { fs } from "../../../../electron/index.js"; +import { joinPath } from "../../../../globals.js"; + +const exampleFileStructure = `mylab/ + ¦ Subjects/ + ¦ +-- NR_0017/ + ¦ ¦ +-- 2022-03-22/ + ¦ ¦ ¦ +-- 001/ + ¦ ¦ ¦ ¦ +-- raw_video_data/ + ¦ ¦ ¦ ¦ ¦ +-- _leftCamera.raw.6252a2f0-c10f-4e49-b085-75749ba29c35.mp4 + ¦ ¦ ¦ ¦ ¦ +-- ... + ¦ ¦ ¦ ¦ +-- ... + ¦ +-- NR_0019/ + ¦ ¦ +-- 2022-04-29/ + ¦ ¦ ¦ +-- 001/ + ¦ ¦ ¦ ¦ +-- raw_video_data/ + ¦ ¦ ¦ ¦ ¦ +-- _leftCamera.raw.9041b63e-02e2-480e-aaa7-4f6b776a647f.mp4 + ¦ ¦ ¦ ¦ ¦ +-- ... + ¦ ¦ ¦ ¦ +-- ... + ¦ ...`; + +const exampleFormatPath = + "Subjects/{subject_id}/{session_start_time:%Y-%m-%d}/{session_id}/raw_video_data/leftCamera.raw.{}.mp4"; + +const exampleMetadata = { + source_data: { + IBL_video: { + file_path: + "/mylab/Subjects/NR_0017/2022-03-22/001/raw_video_data/leftCamera.raw.6252a2f0-c10f-4e49-b085-75749ba29c35.mp4", + }, + }, + metadata: { + NWBFile: { + session_id: "001", + session_start_time: "2022-03-22", + }, + Subject: { + subject_id: "NR_0017", + }, + }, +}; + +const pathExpansionInfoBox = new InfoBox({ + header: "How do I use a Python format string for path expansion?", + content: html` +
+

+ Consider a dataset of that includes video recordings from three cameras, stored in the following + directory structure. +

+ ${new CodeBlock({ text: exampleFileStructure })} + +

+ Using mylab as the base directory, the correct format string to extract the subject ID, + session start time, and session number would be: +

+ ${new CodeBlock({ text: exampleFormatPath })} + +
+ +

+ The above example applies all of the supported f-string variables, which are used to extract information + into the resulting metadata: +

+ ${new List({ + items: [ + { + value: "subject_id", + }, + { + value: "session_id", + }, + { + value: "session_start_time", + }, + ], + editable: false, + })} + +

Additional variables (or blank braces) can be specified to indicate wildcard patterns.

+ +

+ Consequently, the metadata extracted from the first file found using this approach would be the + following: +

+ ${new CodeBlock({ text: JSON.stringify(exampleMetadata, null, 2) })} + +
+ + For complete documentation of the path expansion feature of NeuroConv, visit the + path expansion documentation + page. + +
+ `, +}); + +pathExpansionInfoBox.style.margin = "10px 0px"; + +function getFiles(dir) { + const dirents = fs.readdirSync(dir, { withFileTypes: true }); + let entries = []; + for (const dirent of dirents) { + const res = joinPath(dir, dirent.name); + if (dirent.isDirectory()) entries.push(...getFiles(res)); + else entries.push(res); + } + + return entries; +} export class GuidedPathExpansionPage extends Page { constructor(...args) { @@ -144,6 +259,7 @@ export class GuidedPathExpansionPage extends Page { optional = new OptionalSection({ header: "Would you like to locate data programmatically?", + description: pathExpansionInfoBox, onChange: () => (this.unsavedUpdates = true), // altContent: this.altForm, }); @@ -155,6 +271,7 @@ export class GuidedPathExpansionPage extends Page { const state = structureState.state; if (state !== undefined) this.optional.state = state; + else pathExpansionInfoBox.open = true; // Open the info box if no option has been selected // Require properties for all sources const generatedSchema = { type: "object", properties: {} }; @@ -167,22 +284,74 @@ export class GuidedPathExpansionPage extends Page { const form = (this.form = new JSONSchemaForm({ ...structureState, onThrow, - onUpdate: () => (this.unsavedUpdates = true), + validateOnChange: async (name, parent, parentPath) => { + const value = parent[name]; + if (fs) { + // if (name === 'base_directory') { + // for (const f of getFiles(value)) { + // console.log(f); + // } + // const res = getFiles(value); + // } + // else + if (name === "format_string_path") { + const base_directory = [...parentPath, "base_directory"].reduce( + (acc, key) => acc[key], + this.form.resolved + ); + if (!base_directory) return true; // Do not calculate if base is not found + + const entry = { base_directory }; + + if (value.split(".").length > 1) entry.file_path = value; + else entry.folder_path = value; + + const interfaceName = parentPath.slice(-1)[0]; + + const results = await run(`locate`, { [interfaceName]: entry }, { swal: false }).catch((e) => { + this.notify(e.message, "error"); + throw e; + }); + + const resolved = []; + + for (let sub in results) { + for (let ses in results[sub]) { + const source_data = results[sub][ses].source_data[interfaceName]; + const path = source_data.file_path ?? source_data.folder_path; + resolved.push(path); + } + } + + console.log("Metadata Results for", interfaceName, results); + + return [ + { + message: html`

Source Files Found

+ Inspect the Developer Console to preview the metadata results + ${new List({ + items: resolved.map((path) => { + return { value: path }; + }), + emptyMessage: "N/A", + editable: false, + })}`, + type: "info", + }, + ]; + + // console.log('Updated format string', value, resolved) + } + } + }, })); - const pathExpansionInfoBox = new InfoBox({ - header: "How do I use a Python format string for path expansion?", - content: html`For complete documentation of the path expansion feature of neuroconv, visit the - Path Expansion documentation - page.`, - }); + this.optional.innerHTML = ""; - pathExpansionInfoBox.style.margin = "10px 0px"; + this.optional.style.paddingTop = "10px"; - this.optional.innerHTML = ""; - this.optional.style.marginTop = "15px"; this.optional.append(pathExpansionInfoBox, form); form.style.width = "100%"; diff --git a/src/renderer/src/stories/preview/inspector/InspectorList.js b/src/renderer/src/stories/preview/inspector/InspectorList.js index b4fedb670..3d5a0d7b0 100644 --- a/src/renderer/src/stories/preview/inspector/InspectorList.js +++ b/src/renderer/src/stories/preview/inspector/InspectorList.js @@ -67,7 +67,7 @@ export class InspectorListItem extends LitElement { return css` :host { display: block; - background: gainsboro; + background: WhiteSmoke; border: 1px solid gray; border-radius: 10px; padding: 5px 10px;