diff --git a/schemas/base-metadata.schema.ts b/schemas/base-metadata.schema.ts
index 87d237af9..fc7ccf792 100644
--- a/schemas/base-metadata.schema.ts
+++ b/schemas/base-metadata.schema.ts
@@ -5,6 +5,14 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema) => {
// Add unit to weight
schema.properties.Subject.properties.weight.unit = 'kg'
+ schema.properties.Subject.properties.sex.enumLabels = {
+ M: 'Male',
+ F: 'Female',
+ U: 'Unknown',
+ O: 'Other'
+ }
+
+
// 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
diff --git a/src/renderer/src/stories/JSONSchemaForm.js b/src/renderer/src/stories/JSONSchemaForm.js
index 05d5fdda8..21d214573 100644
--- a/src/renderer/src/stories/JSONSchemaForm.js
+++ b/src/renderer/src/stories/JSONSchemaForm.js
@@ -501,6 +501,8 @@ export class JSONSchemaForm extends LitElement {
}
};
+ // willValidateWhenEmpty = (k) => (Array.isArray(this.validateEmptyValues) && this.validateEmptyValues.includes(k)) || this.validateEmptyValues;
+
#validateRequirements = async (resolved = this.resolved, requirements = this.#requirements, parentPath) => {
let invalid = [];
diff --git a/src/renderer/src/stories/JSONSchemaInput.js b/src/renderer/src/stories/JSONSchemaInput.js
index f2ae8b2da..6029bff6f 100644
--- a/src/renderer/src/stories/JSONSchemaInput.js
+++ b/src/renderer/src/stories/JSONSchemaInput.js
@@ -333,7 +333,10 @@ export class JSONSchemaInput extends LitElement {
>
${info.enum.map(
- (item, i) => html``
+ (item, i) =>
+ html``
)}
`;
diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js
index 968d3577f..e88abb036 100644
--- a/src/renderer/src/stories/Table.js
+++ b/src/renderer/src/stories/Table.js
@@ -1,5 +1,4 @@
import { LitElement, html } from "lit";
-import { notify } from "../dependencies/globals";
import { Handsontable, css } from "./hot";
import { header } from "./forms/utils";
import { errorHue, warningHue } from "./globals";
@@ -77,6 +76,7 @@ export class Table extends LitElement {
onOverride,
validateEmptyCells,
onStatusChange,
+ onThrow,
contextMenu,
} = {}) {
super();
@@ -87,6 +87,7 @@ export class Table extends LitElement {
this.validateEmptyCells = validateEmptyCells ?? true;
this.contextMenu = contextMenu ?? {};
+ if (onThrow) this.onThrow = onThrow;
if (onUpdate) this.onUpdate = onUpdate;
if (onOverride) this.onOverride = onOverride;
if (validateOnChange) this.validateOnChange = validateOnChange;
@@ -138,6 +139,12 @@ export class Table extends LitElement {
validate = () => {
let message;
+ const nUnresolved = Object.keys(this.unresolved).length;
+ if (nUnresolved)
+ message = `${nUnresolved} row${nUnresolved > 1 ? "s are" : " is"} missing a ${
+ this.keyColumn ? `${header(this.keyColumn)} ` : "n "
+ }entry`;
+
if (!message) {
const errors = this.querySelectorAll("[error]");
const len = errors.length;
@@ -145,12 +152,6 @@ export class Table extends LitElement {
else if (len) message = `${len} errors exist on this table.`;
}
- const nUnresolved = Object.keys(this.unresolved).length;
- if (nUnresolved)
- message = `${nUnresolved} row${nUnresolved > 1 ? "s are" : " is"} missing a ${
- this.keyColumn ? `${header(this.keyColumn)} ` : "n "
- }entry`;
-
if (message) throw new Error(message);
};
@@ -158,6 +159,7 @@ export class Table extends LitElement {
onStatusChange = () => {};
onUpdate = () => {};
onOverride = () => {};
+ onThrow = () => {};
isRequired = (col) => {
return this.schema?.required?.includes(col);
@@ -166,6 +168,8 @@ export class Table extends LitElement {
updated() {
const div = (this.shadowRoot ?? this).querySelector("div");
+ const unresolved = (this.unresolved = {});
+
const entries = { ...this.schema.properties };
// Add existing additional properties to the entries variable if necessary
@@ -223,7 +227,7 @@ export class Table extends LitElement {
// Enumerate Possible Values
if (colInfo.enum) {
- info.source = colInfo.enum;
+ info.source = colInfo.enumLabels ? Object.values(colInfo.enumLabels) : colInfo.enum;
if (colInfo.strict === false) info.type = "autocomplete";
else info.type = "dropdown";
}
@@ -266,44 +270,76 @@ export class Table extends LitElement {
const isRequired = this.isRequired(k);
const validator = async function (value, callback) {
- if (!value) {
- if (!ogThis.validateEmptyCells) {
- ogThis.#handleValidationResult(
- [], // Clear errors
- this.row,
- this.col
- );
- callback(true); // Allow empty value
- return true;
- }
+ const validateEmptyCells = ogThis.validateEmptyCells;
+ const willValidate =
+ validateEmptyCells === true ||
+ (Array.isArray(validateEmptyCells) && validateEmptyCells.includes(k));
+
+ value = ogThis.#getValue(value, colInfo);
+
+ // Clear empty values if not validated
+ if (!value && !willValidate) {
+ ogThis.#handleValidationResult(
+ [], // Clear errors
+ this.row,
+ this.col
+ );
+ callback(true); // Allow empty value
+ return;
+ }
- if (isRequired) {
+ if (value && k === ogThis.keyColumn && unresolved[this.row]) {
+ if (value in ogThis.data) {
ogThis.#handleValidationResult(
- [{ message: `${k} is a required property.`, type: "error" }],
+ [{ message: `${header(k)} already exists`, type: "error" }],
this.row,
this.col
);
callback(false);
- return true;
+ return;
}
}
if (!(await runThisValidator(value, this.row, this.col))) {
callback(false);
- return true;
+ return;
+ }
+
+ if (!value && isRequired) {
+ ogThis.#handleValidationResult(
+ [{ message: `${header(k)} is a required property.`, type: "error" }],
+ this.row,
+ this.col
+ );
+ callback(false);
+ return;
}
};
if (info.validator) {
const og = info.validator;
info.validator = async function (value, callback) {
- const called = await validator.call(this, value, callback);
- if (!called) og(value, callback);
+ let wasCalled = false;
+
+ const newCallback = (valid) => {
+ wasCalled = true;
+ callback(valid);
+ };
+
+ await validator.call(this, value, newCallback);
+ if (!wasCalled) og(value, callback);
};
} else
info.validator = async function (value, callback) {
- const called = await validator.call(this, value, callback);
- if (!called) callback(true); // Default to true if not called earlier
+ let wasCalled = false;
+
+ const newCallback = (valid) => {
+ wasCalled = true;
+ callback(valid);
+ };
+
+ await validator.call(this, value, newCallback);
+ if (!wasCalled) callback(true); // Default to true if not called earlier
};
return info;
@@ -316,7 +352,15 @@ export class Table extends LitElement {
const rel = TH.querySelector(".relative");
const isRequired = this.isRequired(col);
- if (isRequired) rel.setAttribute("data-required", this.validateEmptyCells ? true : undefined);
+ if (isRequired)
+ rel.setAttribute(
+ "data-required",
+ this.validateEmptyCells
+ ? Array.isArray(this.validateEmptyCells)
+ ? this.validateEmptyCells.includes(col)
+ : true
+ : undefined
+ );
if (desc) {
let span = rel.querySelector(".info");
@@ -362,8 +406,10 @@ export class Table extends LitElement {
if (this.table) this.table.destroy();
+ console.log("Rendered data", this.#getRenderedData(data));
+
const table = new Handsontable(div, {
- data,
+ data: this.#getRenderedData(data),
// rowHeaders: rowHeaders.map(v => `sub-${v}`),
colHeaders: displayHeaders,
columns,
@@ -384,8 +430,6 @@ export class Table extends LitElement {
const menu = div.ownerDocument.querySelector(".htContextMenu");
if (menu) this.#root.appendChild(menu); // Move to style root
- const unresolved = (this.unresolved = {});
-
let validated = 0;
const initialCellsToUpdate = data.reduce((acc, v) => acc + v.length, 0);
@@ -409,9 +453,11 @@ export class Table extends LitElement {
const isUserUpdate = initialCellsToUpdate <= validated;
- // Transfer data to object
+ value = this.#getValue(value, entries[header]);
+
+ // Transfer data to object (if valid)
if (header === this.keyColumn) {
- if (value && value !== rowName) {
+ if (isValid && value && value !== rowName) {
const old = target[rowName] ?? {};
this.data[value] = old;
delete target[rowName];
@@ -431,7 +477,14 @@ export class Table extends LitElement {
this.onOverride(header, value, rowName);
}
target[rowName][header] = undefined;
- } else target[rowName][header] = value === globalValue ? undefined : value;
+ } else {
+ // Correct for expected arrays (copy-paste issue)
+ if (entries[header]?.type === "array") {
+ if (value && !Array.isArray(value)) value = value.split(",").map((v) => v.trim());
+ }
+
+ target[rowName][header] = value === globalValue ? undefined : value;
+ }
}
validated++;
@@ -445,7 +498,7 @@ export class Table extends LitElement {
// If only one row, do not allow deletion
table.addHook("beforeRemoveRow", (index, amount) => {
if (nRows - amount < 1) {
- notify("You must have at least one row", "error");
+ this.onThrow("You must have at least one row", "error");
return false;
}
});
@@ -473,8 +526,31 @@ export class Table extends LitElement {
data.forEach((row, i) => this.#setRow(i, row));
}
+ #getRenderedValue = (value, colInfo) => {
+ // Handle enums
+ if (colInfo.enumLabels) return colInfo.enumLabels[value] ?? value;
+ return value;
+ };
+
+ #getRenderedData = (data) => {
+ return Object.values(data).map((row) =>
+ row.map((value, j) => this.#getRenderedValue(value, this.schema.properties[this.colHeaders[j]]))
+ );
+ };
+
+ #getValue = (value, colInfo) => {
+ // Handle enums
+ if (colInfo.enumLabels)
+ return Object.keys(colInfo.enumLabels).find((k) => colInfo.enumLabels[k] === value) ?? value;
+
+ return value;
+ };
+
#setRow(row, data) {
- data.forEach((value, j) => this.table.setDataAtCell(row, j, value));
+ data.forEach((value, j) => {
+ value = this.#getRenderedValue(value, this.schema.properties[this.colHeaders[j]]);
+ this.table.setDataAtCell(row, j, value);
+ });
}
#handleValidationResult = (result, row, prop) => {
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 36a01d298..09f4e4593 100644
--- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js
+++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js
@@ -48,15 +48,6 @@ export class GuidedSubjectsPage extends Page {
if (!this.localState[key]) delete globalSubjects[key];
}
- const noSessions = Object.keys(this.localState).filter((sub) => !this.localState[sub].sessions?.length);
- if (noSessions.length) {
- const error = `${noSessions.length} subject${
- noSessions.length > 1 ? "s are" : " is"
- } missing Sessions entries`;
- this.notify(error, "error");
- throw new Error(error);
- }
-
this.info.globalState.subjects = merge(this.localState, globalSubjects); // Merge the local and global states
const { results, subjects } = this.info.globalState;
@@ -125,10 +116,11 @@ export class GuidedSubjectsPage extends Page {
data: subjects,
globals: this.info.globalState.project.Subject,
keyColumn: "subject_id",
- validateEmptyCells: false,
+ validateEmptyCells: ["subject_id", "sessions"],
contextMenu: {
ignore: ["row_below"],
},
+ onThrow: (message, type) => this.notify(message, type),
onOverride: (name) => {
this.notify(`${header(name)} has been overriden with a global value.`, "warning", 3000);
},
@@ -136,9 +128,18 @@ export class GuidedSubjectsPage extends Page {
this.unsavedUpdates = true;
},
validateOnChange: (key, parent, v) => {
- if (key === "sessions") return true;
- else {
- delete parent.sessions; // Delete dessions from parent copy
+ if (key === "sessions") {
+ if (v?.length) return true;
+ else {
+ return [
+ {
+ message: "Sessions must have at least one entry",
+ type: "error",
+ },
+ ];
+ }
+ } else {
+ delete parent.sessions; // Delete sessions from parent copy
return validateOnChange(key, parent, ["Subject"], v);
}
},