diff --git a/pyflask/app.py b/pyflask/app.py index 977dae0f3..ea666c0ce 100644 --- a/pyflask/app.py +++ b/pyflask/app.py @@ -105,6 +105,13 @@ def get_cpu_count(): return dict(physical=physical, logical=logical) +@app.route("/get-recommended-species") +def get_species(): + from dandi.metadata import species_map + + return species_map + + @api.route("/server_shutdown", endpoint="shutdown") class Shutdown(Resource): def get(self): diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts index 372979e22..8cd5c7912 100644 --- a/schemas/base-metadata.schema.ts +++ b/schemas/base-metadata.schema.ts @@ -1,23 +1,90 @@ +import { serverGlobals, resolve } from '../src/renderer/src/server/globals' + +import { header } from '../src/renderer/src/stories/forms/utils' + import baseMetadataSchema from './json/base_metadata_schema.json' assert { type: "json" } -export const preprocessMetadataSchema = (schema: any = baseMetadataSchema) => { +function getSpeciesNameComponents(arr: any[]) { + const split = arr[arr.length - 1].split(' - ') + return { + name: split[0], + label: split[1] + } +} + + +function getSpeciesInfo(species: any[][] = []) { + + + return { + + enumLabels: species.reduce((acc, arr) => { + acc[getSpeciesNameComponents(arr).name] = arr[arr.length - 1] + return acc + }, {}), + + enumKeywords: species.reduce((acc, arr) => { + const info = getSpeciesNameComponents(arr) + acc[info.name] = info.label ? [`${header(info.label)} — ${arr[0].join(', ')}`] : arr[0] + return acc + }, {}), + + enum: species.map(arr => getSpeciesNameComponents(arr).name), // Remove common names so this passes the validator + } + +} + +export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, global = false) => { + + + const copy = structuredClone(schema) // Add unit to weight - schema.properties.Subject.properties.weight.unit = 'kg' + const subjectProps = copy.properties.Subject.properties + subjectProps.weight.unit = 'kg' + + // subjectProps.order = ['weight', 'age', 'age__reference', 'date_of_birth', 'genotype', 'strain'] - schema.properties.Subject.properties.sex.enumLabels = { + subjectProps.sex.enumLabels = { M: 'Male', F: 'Female', U: 'Unknown', O: 'Other' } + + subjectProps.species = { + type: 'string', + ...getSpeciesInfo(), + items: { + type: 'string' + }, + strict: false, + description: 'The species of your subject.' + } + + // Resolve species suggestions + resolve(serverGlobals.species, (res) => { + const info = getSpeciesInfo(res) + Object.assign(subjectProps.species, info) + }) + // Ensure experimenter schema has custom structure - schema.properties.NWBFile.properties.experimenter = baseMetadataSchema.properties.NWBFile.properties.experimenter + copy.properties.NWBFile.properties.experimenter = baseMetadataSchema.properties.NWBFile.properties.experimenter // Override description of keywords - schema.properties.NWBFile.properties.keywords.description = 'Terms to describe your dataset (e.g. Neural circuits, V1, etc.)' // Add description to keywords - return schema + copy.properties.NWBFile.properties.keywords.description = 'Terms to describe your dataset (e.g. Neural circuits, V1, etc.)' // Add description to keywords + + + + // Remove non-global properties + if (global) { + Object.entries(copy.properties).forEach(([globalProp, schema]) => { + instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]); + }); + } + + return copy } @@ -39,13 +106,7 @@ export const instanceSpecificFields = { }; -const globalSchema = structuredClone(preprocessMetadataSchema()); -Object.entries(globalSchema.properties).forEach(([globalProp, schema]) => { - instanceSpecificFields[globalProp]?.forEach((prop) => delete schema.properties[prop]); -}); +export const globalSchema = preprocessMetadataSchema(undefined, true); -export { - globalSchema -} export default preprocessMetadataSchema() diff --git a/schemas/subject.schema.ts b/schemas/subject.schema.ts index 2aa8b0f26..36c2db437 100644 --- a/schemas/subject.schema.ts +++ b/schemas/subject.schema.ts @@ -1,4 +1,4 @@ -import nwbBaseSchema from './base-metadata.schema' +import { preprocessMetadataSchema } from './base-metadata.schema' const removeSubset = (data, subset) => { const subsetData = subset.reduce((acc, key) => { acc[key] = data[key]; return acc }, {}) @@ -6,39 +6,22 @@ const removeSubset = (data, subset) => { return subsetData } - const species = [ - "Mus musculus - House mouse", - "Homo sapiens - Human", - "Rattus norvegicus - Norway rat", - "Rattus rattus - Black rat", - "Macaca mulatta - Rhesus monkey", - "Callithrix jacchus - Common marmoset", - "Drosophila melanogaster - Fruit fly", - "Danio rerio - Zebra fish", - "Caenorhabditis elegans" - ].map(str => str.split(' - ')[0]) // Remove common names so this passes the validator +export default (schema) => { -nwbBaseSchema.properties.Subject.properties.species = { - type: 'string', - enum: species, - items: { - type: 'string' - }, - strict: false, - description: 'The species of your subject.' -} + const nwbBaseSchema = preprocessMetadataSchema(schema) -// Sort the subject schema -const ageGroupKeys = ['age', 'age__reference', 'date_of_birth'] -const genotypeGroupKeys = ['genotype', 'strain'] -const groups = [...ageGroupKeys, ...genotypeGroupKeys] -const standardOrder = {...nwbBaseSchema.properties.Subject.properties} -const group = removeSubset(standardOrder, groups) -const required = removeSubset(standardOrder, nwbBaseSchema.properties.Subject.required) + // Sort the subject schema + const ageGroupKeys = ['age', 'age__reference', 'date_of_birth'] + const genotypeGroupKeys = ['genotype', 'strain'] + const groups = [...ageGroupKeys, ...genotypeGroupKeys] + const standardOrder = {...nwbBaseSchema.properties.Subject.properties} + const group = removeSubset(standardOrder, groups) + const required = removeSubset(standardOrder, nwbBaseSchema.properties.Subject.required) -let newRequiredArray = [...nwbBaseSchema.properties.Subject.required, 'sessions'] + let newRequiredArray = [...nwbBaseSchema.properties.Subject.required, 'sessions'] -export default { + + return { ...nwbBaseSchema.properties.Subject, properties: { sessions: { @@ -53,3 +36,4 @@ export default { }, required: newRequiredArray } +} diff --git a/src/renderer/src/globals.js b/src/renderer/src/globals.js index b235577dd..e9d98b013 100644 --- a/src/renderer/src/globals.js +++ b/src/renderer/src/globals.js @@ -1,4 +1,4 @@ -import { path, port } from "./electron/index.js"; +import { path } from "./electron/index.js"; import guideGlobalMetadata from "../../../guideGlobalMetadata.json" assert { type: "json" }; @@ -9,7 +9,4 @@ export let runOnLoad = (fn) => { else window.addEventListener("load", fn); }; -// Base Request URL for Python Server -export const baseUrl = `http://127.0.0.1:${port}`; - export const supportedInterfaces = guideGlobalMetadata.supported_interfaces; diff --git a/src/renderer/src/index.ts b/src/renderer/src/index.ts index 7d25747a0..7cc801eac 100644 --- a/src/renderer/src/index.ts +++ b/src/renderer/src/index.ts @@ -10,7 +10,9 @@ import { } from './dependencies/globals.js' import Swal from 'sweetalert2' -import { loadServerEvents, pythonServerOpened, statusBar } from "./server.js"; +import { loadServerEvents, pythonServerOpened } from "./server/index.js"; + +import { statusBar } from "./server/globals.js"; // Set the sidebar subtitle to the current app version const dashboard = document.querySelector('nwb-dashboard') as Dashboard diff --git a/src/renderer/src/server/globals.ts b/src/renderer/src/server/globals.ts new file mode 100644 index 000000000..7570a881e --- /dev/null +++ b/src/renderer/src/server/globals.ts @@ -0,0 +1,79 @@ +import { isElectron, app, port } from '../electron/index.js' + +import serverSVG from "../stories/assets/server.svg?raw"; +import webAssetSVG from "../stories/assets/web_asset.svg?raw"; +import wifiSVG from "../stories/assets/wifi.svg?raw"; + +// Base Request URL for Python Server +export const baseUrl = `http://127.0.0.1:${port}`; + +const isPromise = (o) => typeof o === 'object' && typeof o.then === 'function' + +export const resolve = (object, callback) => { + if (isPromise(object)) { + return new Promise(resolvePromise => { + object.then((res) => resolvePromise((callback) ? callback(res) : res)) + }) + } else return (callback) ? callback(object) : object +} + +// ------------------------------------------------- + +import { StatusBar } from "../stories/status/StatusBar.js"; +import { unsafeSVG } from "lit/directives/unsafe-svg.js"; + +const appVersion = app?.getVersion(); + +export const statusBar = new StatusBar({ + items: [ + { label: unsafeSVG(webAssetSVG), value: isElectron ? appVersion ?? 'ERROR' : 'Web' }, + { label: unsafeSVG(wifiSVG) }, + { label: unsafeSVG(serverSVG) } + ] +}) + + +let serverCallbacks: Function[] = [] +export const onServerOpen = (callback:Function) => { + if (statusBar.items[2].status === true) return callback() + else { + return new Promise(res => { + serverCallbacks.push(() => { + res(callback()) + }) + + }) + } +} + +export const activateServer = () => { + statusBar.items[2].status = true + + serverCallbacks.forEach(cb => cb()) + serverCallbacks = [] +} + +export const serverGlobals = { + species: new Promise((res, rej) => { + onServerOpen(() => { + fetch(new URL("get-recommended-species", baseUrl)) + .then((res) => res.json()) + .then((species) => { + res(species) + serverGlobals.species = species + }) + .catch(() => rej()); + }); + }), + cpus: new Promise((res, rej) => { + onServerOpen(() => { + fetch(new URL("cpus", baseUrl)) + .then((res) => res.json()) + .then((cpus) => { + res(cpus) + serverGlobals.cpus = cpus + }) + .catch(() => rej()); + }); + }) + } diff --git a/src/renderer/src/server.ts b/src/renderer/src/server/index.ts similarity index 71% rename from src/renderer/src/server.ts rename to src/renderer/src/server/index.ts index d0add2853..e2dae2fcb 100644 --- a/src/renderer/src/server.ts +++ b/src/renderer/src/server/index.ts @@ -1,31 +1,13 @@ -import { isElectron, electron, app } from './electron/index.js' +import { isElectron, electron, app, port } from '../electron/index.js' const { ipcRenderer } = electron; import { notyf, -} from './dependencies/globals.js' - -import { baseUrl } from './globals.js' +} from '../dependencies/globals.js' import Swal from 'sweetalert2' - -import { StatusBar } from "./stories/status/StatusBar.js"; -import { unsafeSVG } from "lit/directives/unsafe-svg.js"; -import serverSVG from "./stories/assets/server.svg?raw"; -import webAssetSVG from "./stories/assets/web_asset.svg?raw"; -import wifiSVG from "./stories/assets/wifi.svg?raw"; - -const appVersion = app?.getVersion(); - -export const statusBar = new StatusBar({ - items: [ - { label: unsafeSVG(webAssetSVG), value: isElectron ? appVersion ?? 'ERROR' : 'Web' }, - { label: unsafeSVG(wifiSVG) }, - { label: unsafeSVG(serverSVG) } - ] -}) - +import { activateServer, baseUrl } from './globals.js'; // Check if the Flask server is live const serverIsLiveStartup = async () => { @@ -39,27 +21,6 @@ const serverIsLiveStartup = async () => { else throw new Error('Error preloading Flask imports') }) - -let serverCallbacks: Function[] = [] -export const onServerOpen = (callback:Function) => { - if (statusBar.items[2].status === true) return callback() - else { - return new Promise(res => { - serverCallbacks.push(() => { - res(callback()) - }) - - }) - } -} - -export const activateServer = () => { - statusBar.items[2].status = true - - serverCallbacks.forEach(cb => cb()) - serverCallbacks = [] -} - export async function pythonServerOpened() { // Confirm requests are actually received by the server diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js index 259f0644f..3fe751608 100644 --- a/src/renderer/src/stories/JSONSchemaForm.js +++ b/src/renderer/src/stories/JSONSchemaForm.js @@ -376,7 +376,7 @@ export class JSONSchemaForm extends LitElement { let message = isValid ? "" : requiredButNotSpecified.length === 1 - ? `${requiredButNotSpecified[0]} is not defined` + ? `${header(requiredButNotSpecified[0])} is not defined` : `${requiredButNotSpecified.length} required inputs are not specified properly`; if (requiredButNotSpecified.length !== nMissingRequired) console.warn("Disagreement about the correct error to throw..."); @@ -387,7 +387,6 @@ export class JSONSchemaForm extends LitElement { if (flaggedInputs.length) { flaggedInputs[0].focus(); if (!message) { - console.log(flaggedInputs); if (flaggedInputs.length === 1) message = `${header(flaggedInputs[0].path.join("."))} is not valid`; else message = `${flaggedInputs.length} invalid form values`; diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js index 97ea5729e..176fddd34 100644 --- a/src/renderer/src/stories/JSONSchemaInput.js +++ b/src/renderer/src/stories/JSONSchemaInput.js @@ -11,6 +11,7 @@ import { Modal } from "./Modal"; import { capitalize } from "./forms/utils"; import { JSONSchemaForm } from "./JSONSchemaForm"; +import { Search } from "./Search"; const isFilesystemSelector = (name, format) => { if (Array.isArray(format)) return format.map((f) => isFilesystemSelector(name, f)).every(Boolean) ? format : null; @@ -111,7 +112,8 @@ export class JSONSchemaInput extends LitElement { // onValidate = () => {} updateData(value, forceValidate = false) { - if (this.value === value && !forceValidate) { + if (this.value !== value && !forceValidate) { + // Update the actual input element const el = this.getElement(); if (el.type === "checkbox") el.checked = value; else if (el.classList.contains("list")) @@ -120,6 +122,7 @@ export class JSONSchemaInput extends LitElement { return { value }; }) : []; + else if (el instanceof Search) el.shadowRoot.querySelector("input").value = value; else el.value = value; } @@ -356,7 +359,33 @@ export class JSONSchemaInput extends LitElement { } // Basic enumeration of properties on a select element - if (info.enum) { + if (info.enum && info.enum.length) { + if (info.strict === false) { + // const category = categories.find(({ test }) => test.test(key))?.value; + + const options = info.enum.map((v) => { + return { + key: v, + keywords: info.enumKeywords?.[v], + }; + }); + + const search = new Search({ + options, + value: this.value, + showAllWhenEmpty: false, + listMode: "click", + onSelect: async ({ value, key }) => { + const result = value ?? key; + this.#updateData(fullPath, result); + if (validateOnChange) await this.#triggerValidation(name, path); + }, + }); + + search.classList.add("schema-input"); + return search; + } + return html` { - const input = ev.target.value; - - // Hide all if empty - if (!input) { - this.#initialize(); - return; +
+ { + ev.stopPropagation(); + if (this.listMode === "click") { + const input = ev.target.value; + this.#populate(input); } - - const toShow = []; - // Check if the input value matches the label - this.#options.forEach(({ option, label }, i) => { - if (label.toLowerCase().includes(input.toLowerCase()) && !toShow.includes(i)) toShow.push(i); - }); - - // Check if the input value matches any of the keywords - this.#options.forEach(({ option, keywords }, i) => { - keywords.forEach((keyword) => { - if (keyword.toLowerCase().includes(input.toLowerCase()) && !toShow.includes(i)) toShow.push(i); - }); - }); - - this.#options.forEach(({ option }, i) => { - if (toShow.includes(i)) { - option.removeAttribute("hidden"); - } else { - option.setAttribute("hidden", ""); - } - }); - - categories.forEach(({ entries, element }) => { - if (entries.reduce((acc, el) => acc + el.hasAttribute("hidden"), 0) === entries.length) - element.setAttribute("hidden", ""); - else element.removeAttribute("hidden"); - }); + }} @input=${(ev) => { + const input = ev.target.value; + this.#populate(input); }}> + ${unsafeHTML(searchSVG)}
${this.list} `; diff --git a/src/renderer/src/stories/Table.stories.js b/src/renderer/src/stories/Table.stories.js index 47d38cdf6..1d6225e40 100644 --- a/src/renderer/src/stories/Table.stories.js +++ b/src/renderer/src/stories/Table.stories.js @@ -1,6 +1,6 @@ import { Table } from "./Table.js"; -import subjectSchema from "../../../../schemas/subject.schema"; +import getSubjectSchema from "../../../../schemas/subject.schema"; import { SimpleTable } from "./SimpleTable.js"; import { BasicTable } from "./BasicTable.js"; @@ -26,6 +26,8 @@ const data = subjectIds.reduce((acc, key) => { const BasicTableTemplate = (args) => new BasicTable(args); +const subjectSchema = getSubjectSchema(); + subjectSchema.additionalProperties = true; export const Basic = BasicTableTemplate.bind({}); diff --git a/src/renderer/src/stories/assets/search.svg b/src/renderer/src/stories/assets/search.svg new file mode 100644 index 000000000..c8519dc10 --- /dev/null +++ b/src/renderer/src/stories/assets/search.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/stories/forms/utils.ts b/src/renderer/src/stories/forms/utils.ts index 64e8fad8c..ea3cdfb16 100644 --- a/src/renderer/src/stories/forms/utils.ts +++ b/src/renderer/src/stories/forms/utils.ts @@ -7,9 +7,7 @@ export const capitalize = (str: string) => { } -export const header = (headerStr: string) => { - return headerStr.split('_').filter(str => !!str).map(capitalize).join(' ') -} +export const header = (headerStr: string) => headerStr.split(/[_\s]/).filter(str => !!str).map(capitalize).join(' ') export const textToArray = (value: string) => value.split("\n") .map((str) => str.trim()) 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 ebfe5f8c9..14de7a9bb 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedMetadata.js @@ -15,7 +15,6 @@ import { header } from "../../../forms/utils"; import { createGlobalFormModal } from "../../../forms/GlobalFormModal"; import { Button } from "../../../Button.js"; -import { globalSchema } from "../../../../../../../schemas/base-metadata.schema"; import globalIcon from "../../../assets/global.svg?raw"; @@ -86,7 +85,7 @@ export class GuidedMetadataPage extends ManagedPage { const modal = (this.#globalModal = createGlobalFormModal.call(this, { header: "Global Metadata", propsToRemove: [...propsToIgnore], - schema: globalSchema, // Provide HARDCODED global schema for metadata properties (not automatically abstracting across sessions)... + schema: preprocessMetadataSchema(undefined, true), // Provide HARDCODED global schema for metadata properties (not automatically abstracting across sessions)... hasInstances: true, mergeFunction: function (globalResolved, globals) { merge(globalResolved, globals); @@ -144,8 +143,6 @@ export class GuidedMetadataPage extends ManagedPage { resolveResults(subject, session, globalState); - console.log(subject, session, results); - // 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 48c2c5592..f8bbfcbdc 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedSourceData.js @@ -3,7 +3,6 @@ import { isStorybook } from "../../../../dependencies/globals.js"; import { JSONSchemaForm } from "../../../JSONSchemaForm.js"; import { InstanceManager } from "../../../InstanceManager.js"; import { ManagedPage } from "./ManagedPage.js"; -import { baseUrl } from "../../../../globals.js"; import { onThrow } from "../../../../errors"; import { merge, sanitize } from "../../utils.js"; import preprocessSourceDataSchema from "../../../../../../../schemas/source-data.schema"; @@ -13,7 +12,8 @@ import { header } from "../../../forms/utils"; import { Button } from "../../../Button.js"; import globalIcon from "../../../assets/global.svg?raw"; -import { run } from "../options/utils.js"; + +import { baseUrl } from "../../../../server/globals"; const propsToIgnore = [ "verbose", diff --git a/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js b/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js index 3fc80b22b..92e8e30cc 100644 --- a/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js +++ b/src/renderer/src/stories/pages/guided-mode/data/GuidedStructure.js @@ -3,10 +3,11 @@ import { Page } from "../../Page.js"; // For Multi-Select Form import { Button } from "../../../Button.js"; -import { baseUrl, supportedInterfaces } from "../../../../globals.js"; +import { supportedInterfaces } from "../../../../globals.js"; import { Search } from "../../../Search.js"; import { Modal } from "../../../Modal"; import { List } from "../../../List"; +import { baseUrl } from "../../../../server/globals"; const defaultEmptyMessage = "No interfaces selected"; @@ -27,8 +28,8 @@ export class GuidedStructurePage extends Page { // Handle Search Bar Interactions this.search.list.style.position = "unset"; - this.search.onSelect = (...args) => { - this.list.add(...args); + this.search.onSelect = (item) => { + this.list.add(item); this.searchModal.toggle(false); }; @@ -46,6 +47,9 @@ export class GuidedStructurePage extends Page { search = new Search({ disabledLabel: "Not supported", + headerStyles: { + padding: "15px", + }, }); list = new List({ diff --git a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js index 8294691df..cd4dd6f45 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedInspectorPage.js @@ -74,8 +74,6 @@ export class GuidedInspectorPage extends Page { const { globalState } = this.info; const { stubs, inspector } = globalState.preview; - console.log("Already got", inspector); - const opts = {}; // NOTE: Currently options are handled on the Python end until exposed to the user const title = "Inspecting your file"; 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 2816a4af7..49fd68827 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js +++ b/src/renderer/src/stories/pages/guided-mode/options/GuidedUpload.js @@ -8,8 +8,8 @@ import dandiUploadSchema from "../../../../../../../schemas/dandi-upload.schema" import { dandisetInfoContent, uploadToDandi } from "../../uploads/UploadsPage.js"; import { InfoBox } from "../../../InfoBox.js"; import { until } from "lit/directives/until.js"; -import { onServerOpen } from "../../../../server"; -import { baseUrl } from "../../../../globals.js"; + +import { baseUrl, onServerOpen } from "../../../../server/globals"; export class GuidedUploadPage extends Page { constructor(...args) { diff --git a/src/renderer/src/stories/pages/guided-mode/options/utils.js b/src/renderer/src/stories/pages/guided-mode/options/utils.js index 83bef4acc..1b5b9c7fd 100644 --- a/src/renderer/src/stories/pages/guided-mode/options/utils.js +++ b/src/renderer/src/stories/pages/guided-mode/options/utils.js @@ -1,7 +1,6 @@ import Swal from "sweetalert2"; -import { baseUrl } from "../../../../globals.js"; import { sanitize } from "../../utils.js"; -import { Loader } from "../../../Loader"; +import { baseUrl } from "../../../../server/globals"; export const openProgressSwal = (options, callback) => { return new Promise((resolve) => { 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 38265cd3a..9bf89c765 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedNewDatasetInfo.js @@ -9,6 +9,7 @@ import projectGlobalSchema from "../../../../../../../schemas/json/project/globa import { merge } from "../../utils.js"; import { onThrow } from "../../../../errors"; import { header } from "../../../forms/utils"; +import { preprocessMetadataSchema } from "../../../../../../../schemas/base-metadata.schema"; const projectMetadataSchema = merge(projectGlobalSchema, projectGeneralSchema); 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 1078bd76b..c9c18f5d4 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -1,12 +1,12 @@ import { html } from "lit"; import { Page } from "../../Page.js"; -import subjectSchema from "../../../../../../../schemas/subject.schema"; +import getSubjectSchema from "../../../../../../../schemas/subject.schema"; import { validateOnChange } from "../../../../validation/index.js"; import { Table } from "../../../Table.js"; import { updateResultsFromSubjects } from "./utils"; import { merge } from "../../utils.js"; -import { globalSchema } from "../../../../../../../schemas/base-metadata.schema"; +import { preprocessMetadataSchema } from "../../../../../../../schemas/base-metadata.schema"; import { Button } from "../../../Button.js"; import { createGlobalFormModal } from "../../../forms/GlobalFormModal"; import { header } from "../../../forms/utils"; @@ -84,7 +84,7 @@ export class GuidedSubjectsPage extends Page { const modal = (this.#globalModal = createGlobalFormModal.call(this, { header: "Global Subject Metadata", key: "Subject", - schema: globalSchema.properties.Subject, + schema: preprocessMetadataSchema(undefined, true).properties.Subject, validateOnChange: (key, parent, path) => { return validateOnChange(key, parent, ["Subject", ...path]); }, @@ -112,7 +112,7 @@ export class GuidedSubjectsPage extends Page { } this.table = new Table({ - schema: subjectSchema, + schema: getSubjectSchema(), data: subjects, globals: this.info.globalState.project.Subject, keyColumn: "subject_id", diff --git a/src/renderer/src/stories/pages/guided-mode/storyStates.ts b/src/renderer/src/stories/pages/guided-mode/storyStates.ts index c9e529244..211ee2c5c 100644 --- a/src/renderer/src/stories/pages/guided-mode/storyStates.ts +++ b/src/renderer/src/stories/pages/guided-mode/storyStates.ts @@ -2,8 +2,7 @@ import nwbBaseSchema from "../../../../../../schemas/base-metadata.schema"; // import exephysExampleSchema from "../../../../../../schemas/json/ecephys_metadata_schema_example.json"; import { dashboard } from "../../../pages.js"; -import { activateServer } from "../../../server"; - +import { activateServer } from "../../../server/globals"; activateServer(); diff --git a/src/renderer/src/stories/pages/preview/PreviewPage.js b/src/renderer/src/stories/pages/preview/PreviewPage.js index 45f4c5b7b..be1f00070 100644 --- a/src/renderer/src/stories/pages/preview/PreviewPage.js +++ b/src/renderer/src/stories/pages/preview/PreviewPage.js @@ -3,7 +3,7 @@ import { Page } from "../Page.js"; import { onThrow } from "../../../errors"; import { JSONSchemaInput } from "../../JSONSchemaInput.js"; import { Neurosift } from "../../preview/Neurosift.js"; -import { baseUrl } from "../../../globals.js"; +import { baseUrl } from "../../../server/globals"; export class PreviewPage extends Page { header = { diff --git a/src/renderer/src/stories/pages/uploads/UploadsPage.js b/src/renderer/src/stories/pages/uploads/UploadsPage.js index a1b3adc5b..a63573ddf 100644 --- a/src/renderer/src/stories/pages/uploads/UploadsPage.js +++ b/src/renderer/src/stories/pages/uploads/UploadsPage.js @@ -26,8 +26,7 @@ import { header } from "../../forms/utils"; import { validateDANDIApiKey } from "../../../validation/dandi"; import { InfoBox } from "../../InfoBox.js"; -import { onServerOpen } from "../../../server"; -import { baseUrl } from "../../../globals.js"; +import { baseUrl, onServerOpen } from "../../../server/globals"; export const isStaging = (id) => parseInt(id) >= 100000; diff --git a/src/renderer/src/stories/preview/Neurosift.js b/src/renderer/src/stories/preview/Neurosift.js index ee14df8bb..8def8c8dd 100644 --- a/src/renderer/src/stories/preview/Neurosift.js +++ b/src/renderer/src/stories/preview/Neurosift.js @@ -1,8 +1,8 @@ import { LitElement, css, html } from "lit"; -import { baseUrl } from "../../globals"; import { Loader } from "../Loader"; import { FullScreenToggle } from "../FullScreenToggle"; +import { baseUrl } from "../../server/globals"; export function getURLFromFilePath(file, projectName) { const regexp = new RegExp(`.+(${projectName}.+)`); diff --git a/src/renderer/src/validation/index.js b/src/renderer/src/validation/index.js index 86d13a7b8..beccffbc9 100644 --- a/src/renderer/src/validation/index.js +++ b/src/renderer/src/validation/index.js @@ -1,5 +1,5 @@ -import { baseUrl } from "../globals"; import { resolveAll } from "../promises"; +import { baseUrl } from "../server/globals"; import validationSchema from "./validation"; // NOTE: Only validation missing on NWBFile Metadata is check_subject_exists and check_processing_module_name diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts index b5dab4e9e..2396154bc 100644 --- a/tests/metadata.test.ts +++ b/tests/metadata.test.ts @@ -24,6 +24,10 @@ describe('metadata is specified correctly', () => { test('session-specific metadata is merged with project and subject metadata correctly', () => { const globalState = createMockGlobalState() + + // Allow mouse (full list populated from server) + baseMetadataSchema.properties.Subject.properties.species.enum = ['Mus musculus'] + const result = mapSessions(info => createResults(info, globalState), globalState) const res = v.validate(result[0], baseMetadataSchema) // Check first session with JSON Schema expect(res.errors).toEqual([])