Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Persistence following Subject Table Interactions #663

Merged
merged 9 commits into from
Mar 13, 2024
59 changes: 49 additions & 10 deletions src/renderer/src/stories/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { emojiFontFamily } from "./globals";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";

const rowSymbol = Symbol("row");

const maxRows = 20;

const isRequired = (col, schema) => {
Expand Down Expand Up @@ -166,7 +168,7 @@ export class Table extends LitElement {
});
}

#getData(rows = this.rowHeaders, cols = this.colHeaders) {
#getData(rows = Object.keys(this.data), cols = this.colHeaders) {
return rows.map((row, i) => this.#getRowData(row, cols));
}

Expand Down Expand Up @@ -215,6 +217,18 @@ export class Table extends LitElement {
this.#itemProps = { ...this.#itemSchema.properties };
}

getRowName = (row) =>
this.keyColumn ? Object.entries(this.data).find(([k, v]) => v[rowSymbol] === row)?.[0] : row;

revalidate = (skipped = []) => {
this.table.getData().forEach((rowData, i) => {
rowData.forEach((value, j) => {
const isSkipped = skipped.find(({ row, prop }) => row === i && j === prop);
if (!isSkipped) this.table.setDataAtCell(i, j, value);
});
});
};

updated() {
const div = (this.shadowRoot ?? this).querySelector("div");

Expand Down Expand Up @@ -256,7 +270,9 @@ export class Table extends LitElement {
if (foundKey) this.keyColumn = foundKey;
}

const rowHeaders = (this.rowHeaders = Object.keys(this.data));
Object.keys(this.data).forEach((row, i) =>
Object.defineProperty(this.data[row], rowSymbol, { value: i, configurable: true })
); // Set initial row trackers

const displayHeaders = [...colHeaders].map(header);

Expand Down Expand Up @@ -296,7 +312,7 @@ export class Table extends LitElement {
const valid = this.validateOnChange
? await this.validateOnChange(
[k],
{ ...this.data[rowHeaders[row]] }, // Validate on a copy of the parent
{ ...this.data[this.getRowName(row)] }, // Validate on a copy of the parent
value,
info
)
Expand Down Expand Up @@ -330,8 +346,16 @@ export class Table extends LitElement {
return;
}

if (value && k === instanceThis.keyColumn && unresolved[this.row]) {
if (value in instanceThis.data) {
if (value && k === instanceThis.keyColumn) {
if (value in instanceThis.data && instanceThis.data[value]?.[rowSymbol] !== this.row) {
// Convert previously valid value to unresolved
const previousKey = instanceThis.getRowName(this.row);
if (previousKey) {
unresolved[this.row] = instanceThis.data[previousKey];
delete instanceThis.data[previousKey];
}

// Throw error
instanceThis.#handleValidationResult(
[{ message: `${header(k)} already exists`, type: "error" }],
this.row,
Expand All @@ -342,6 +366,19 @@ export class Table extends LitElement {
}
}

if (name === "subject_id") {
if (v) {
if (Object.values(this.data).filter((s) => s.subject_id === v).length > 1) {
return [
{
message: "Subject ID must be unique",
type: "error",
},
];
}
}
}

if (!(await runThisValidator(value, this.row, this.col))) {
callback(false);
return;
Expand Down Expand Up @@ -421,7 +458,7 @@ export class Table extends LitElement {

const data = this.#getData();

let nRows = rowHeaders.length;
let nRows = Object.keys(data).length;

let contextMenu = ["row_below", "remove_row"];
if (this.#itemSchema.additionalProperties) contextMenu.push("col_right", "remove_col");
Expand Down Expand Up @@ -476,9 +513,10 @@ export class Table extends LitElement {
table.addHook("afterValidate", (isValid, value, row, prop) => {
const isUserUpdate = initialCellsToUpdate <= validated;

let rowName = this.getRowName(row);

if (isUserUpdate) {
const header = typeof prop === "number" ? colHeaders[prop] : prop;
let rowName = this.keyColumn ? rowHeaders[row] : row;

// NOTE: We would like to allow invalid values to mutate the results
// if (isValid) {
Expand All @@ -504,7 +542,8 @@ export class Table extends LitElement {
this.data[value] = old;
delete target[rowName];
delete unresolved[row];
rowHeaders[row] = value;
Object.defineProperty(this.data[value], rowSymbol, { value: row, configurable: true }); // Setting row tracker
this.revalidate([{ row, prop }]);
}
}

Expand Down Expand Up @@ -548,11 +587,11 @@ export class Table extends LitElement {
table.addHook("afterRemoveRow", (_, amount, physicalRows) => {
nRows -= amount;
physicalRows.map(async (row) => {
const rowName = rowHeaders[row];
const rowName = this.getRowName(row);
// const cols = this.data[rowHeaders[row]]
// Object.keys(cols).map(k => cols[k] = '')
// if (this.validateOnChange) Object.keys(cols).map(k => this.validateOnChange([ k ], { ...cols }, cols[k])) // Validate with empty values before removing
delete this.data[rowHeaders[row]];
delete this.data[rowName];
delete unresolved[row];
this.onUpdate(rowName, null, undefined); // NOTE: Global metadata PR might simply set all data values to undefined
});
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/stories/forms/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const toCapitalizeAll = ['nwb', 'api', 'id']
const toCapitalizeNone = ['or', 'and']

const createRandomString = () => Math.random().toString(36).substring(7);
export const createRandomString = () => Math.random().toString(36).substring(7);
export const tempPropertyKey = createRandomString();
export const tempPropertyValueKey = createRandomString();

Expand Down
34 changes: 20 additions & 14 deletions src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,32 @@ export class GuidedSubjectsPage extends Page {
throw error;
}

// Delete old subjects before merging
const { subjects: globalSubjects } = this.info.globalState;

const localState = this.table.data;

for (let key in globalSubjects) {
if (!localState[key]) delete globalSubjects[key];
// Create map of original names to new names
const nameMap = {};
for (let key in this.#originalState) {
const renamed = Object.keys(localState).find(
(k) => localState[k].identifier === this.#originalState[key].identifier
);
nameMap[key] = renamed;
}

this.info.globalState.subjects = merge(localState, globalSubjects); // Merge the local and global states
// Remove identifiers
for (let key in localState) delete localState[key].identifier;

const { results, subjects } = this.info.globalState;
// Local state is the source of truth
this.info.globalState.subjects = localState;

// Object.keys(subjects).forEach((sub) => {
// if (!subjects[sub].sessions?.length) {
// delete subjects[sub]
// }
// });
const { results, subjects } = this.info.globalState;

const sourceDataObject = Object.keys(this.info.globalState.interfaces).reduce((acc, key) => {
acc[key] = {};
return acc;
}, {});

// Modify the results object to track new subjects / sessions
updateResultsFromSubjects(results, subjects, sourceDataObject); // NOTE: This directly mutates the results object
updateResultsFromSubjects(results, subjects, sourceDataObject, nameMap); // NOTE: This directly mutates the results object
};

footer = {};
Expand Down Expand Up @@ -118,6 +118,8 @@ export class GuidedSubjectsPage extends Page {
if (this.#globalModal) this.#globalModal.remove();
}

#originalState = {};

render() {
const hasMultipleSessions = this.workflow.multiple_sessions.value;

Expand All @@ -129,9 +131,12 @@ export class GuidedSubjectsPage extends Page {
toRemove.forEach((sub) => delete subjects[sub]);
toHave.forEach((sub) => (subjects[sub] = subjects[sub] ?? {}));

this.#originalState = structuredClone(subjects);

for (let subject in subjects) {
const sessions = Object.keys(this.info.globalState.results[subject]);
subjects[subject].sessions = sessions;
subjects[subject].identifier = this.#originalState[subject].identifier = Symbol("subject"); // Add identifier to subject
}

const contextMenuConfig = { ignore: ["row_below"] };
Expand All @@ -156,7 +161,8 @@ export class GuidedSubjectsPage extends Page {
this.unsavedUpdates = "conversions";
},
validateOnChange: (localPath, parent, v) => {
if (localPath.slice(-1)[0] === "sessions") {
const name = localPath[localPath.length - 1];
if (name === "sessions") {
if (v?.length) return true;
else {
return [
Expand Down
22 changes: 19 additions & 3 deletions src/renderer/src/stories/pages/guided-mode/setup/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@

export const updateResultsFromSubjects = (results: any, subjects: any, sourceDataObject = {}) => {
export const updateResultsFromSubjects = (results: any, subjects: any, sourceDataObject = {}, nameMap: {[x:string]: string} = {}) => {

const oldResults = structuredClone(results);

const toRemove = Object.keys(results).filter((sub) => !Object.keys(subjects).includes(sub));
for (let sub of toRemove) delete results[sub]; // Delete extra subjects from results
for (let sub of toRemove) {
if (sub in nameMap) results[nameMap[sub]] = results[sub];
delete results[sub]; // Delete extra subjects from results
}


for (let subject in subjects) {
const { sessions = [] } = subjects[subject];
Expand All @@ -11,7 +17,17 @@ export const updateResultsFromSubjects = (results: any, subjects: any, sourceDat
if (!subObj) subObj = results[subject] = {};
else {
const toRemove = Object.keys(subObj).filter((s) => !sessions.includes(s));
for (let s of toRemove) delete subObj[s]; // Delete extra sessions from results
for (let s of toRemove) {

// Skip removal if your data has been mapped
if (subject in nameMap) {
const oldSessionInfo = oldResults[subject]
const newSubResults = results[nameMap[subject]]
if (s in oldSessionInfo) newSubResults[s] = oldSessionInfo[s];
}

delete subObj[s]; // Delete extra sessions from results
}
if (!sessions.length && !Object.keys(subObj).length) delete results[subject]; // Delete subjects without sessions
}

Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/stories/pages/settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function saveNewPipelineFromYaml(name, sourceData, rootFolder) {

results: {
[subjectId]: sessions.reduce((acc, sessionId) => {
acc[subjectId] = {
acc[sessionId] = {
metadata: {
Subject: {
subject_id: subjectId,
Expand Down
Loading