diff --git a/src/renderer/src/stories/DateTimeSelector.js b/src/renderer/src/stories/DateTimeSelector.js index 10ffe3a80..acf062092 100644 --- a/src/renderer/src/stories/DateTimeSelector.js +++ b/src/renderer/src/stories/DateTimeSelector.js @@ -1,5 +1,14 @@ import { LitElement, css } from "lit"; +const convertToDateTimeLocalString = (date) => { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + return `${year}-${month}-${day}T${hours}:${minutes}`; +}; + export class DateTimeSelector extends LitElement { static get styles() { return css` @@ -15,10 +24,15 @@ export class DateTimeSelector extends LitElement { } set value(newValue) { - this.input.value = newValue; + if (newValue) this.input.value = newValue; + else { + const d = new Date(); + d.setHours(0, 0, 0, 0); + this.input.value = convertToDateTimeLocalString(d); + } } - constructor() { + constructor({ value } = {}) { super(); this.input = document.createElement("input"); this.input.type = "datetime-local"; @@ -27,6 +41,8 @@ export class DateTimeSelector extends LitElement { this.input.focus(); this.input.showPicker(); }); + + this.value = value ? convertToDateTimeLocalString(value) : value; } focus() { diff --git a/src/renderer/src/stories/Table.js b/src/renderer/src/stories/Table.js index eaa0f4948..2b2928052 100644 --- a/src/renderer/src/stories/Table.js +++ b/src/renderer/src/stories/Table.js @@ -50,6 +50,16 @@ const styles = ` .handsontable { overflow: unset !important; } + + th > [data-required] > *:first-child::after { + content: '*'; + margin-left: 2px; + color: gray; + } + + th > [data-required=true] > *:first-child::after { + color: red; + } `; const styleSymbol = Symbol("table-styles"); @@ -105,7 +115,7 @@ export class Table extends LitElement { let value; if (col === this.keyColumn) { if (hasRow) value = row; - else return ""; + else return undefined; } else value = (hasRow ? this.data[row][col] : undefined) ?? @@ -138,8 +148,8 @@ export class Table extends LitElement { const nUnresolved = Object.keys(this.unresolved).length; if (nUnresolved) - message = `${nUnresolved} row${nUnresolved > 1 ? "s are" : " is"} missing a${ - this.keyColumn ? `${this.keyColumn} ` : "n " + message = `${nUnresolved} row${nUnresolved > 1 ? "s are" : " is"} missing a ${ + this.keyColumn ? `${header(this.keyColumn)} ` : "n " }entry`; if (message) throw new Error(message); @@ -149,6 +159,10 @@ export class Table extends LitElement { onStatusChange = () => {}; onUpdate = () => {}; + isRequired = (col) => { + return this.schema?.required?.includes(col); + }; + updated() { const div = (this.shadowRoot ?? this).querySelector("div"); @@ -169,13 +183,26 @@ export class Table extends LitElement { } // Sort Columns by Key Column and Requirement - const colHeaders = (this.colHeaders = Object.keys(entries).sort((a, b) => { - if (a === this.keyColumn) return -1; - if (b === this.keyColumn) return 1; - if (entries[a].required && !entries[b].required) return -1; - if (!entries[a].required && entries[b].required) return 1; - return 0; - })); + const colHeaders = (this.colHeaders = Object.keys(entries) + .sort((a, b) => { + //Sort alphabetically + if (a < b) return -1; + if (a > b) return 1; + return 0; + }) + .sort((a, b) => { + const aRequired = this.isRequired(a); + const bRequired = this.isRequired(b); + if (aRequired && bRequired) return 0; + if (aRequired) return -1; + if (bRequired) return 1; + return 0; + }) + .sort((a, b) => { + if (a === this.keyColumn) return -1; + if (b === this.keyColumn) return 1; + return 0; + })); // Try to guess the key column if unspecified if (!Array.isArray(this.data) && !this.keyColumn) { @@ -236,7 +263,7 @@ export class Table extends LitElement { }; let ogThis = this; - const isRequired = ogThis.schema?.required?.includes(k); + const isRequired = this.isRequired(k); const validator = async function (value, callback) { if (!value) { @@ -282,11 +309,18 @@ export class Table extends LitElement { return info; }); - const onAfterGetHeader = function (index, TH) { - const desc = entries[colHeaders[index]].description; + const onAfterGetHeader = (index, TH) => { + const col = colHeaders[index]; + const desc = entries[col].description; + + const rel = TH.querySelector(".relative"); + + const isRequired = this.isRequired(col); + if (isRequired) rel.setAttribute("data-required", this.validateEmptyCells ? true : undefined); + if (desc) { - const rel = TH.querySelector(".relative"); let span = rel.querySelector(".info"); + if (!span) { span = document.createElement("span"); span.classList.add("info"); @@ -373,7 +407,8 @@ export class Table extends LitElement { // Transfer data to object if (header === this.keyColumn) { - if (value !== rowName) { + console.log(value, rowName); + if (value && value !== rowName) { const old = target[rowName] ?? {}; this.data[value] = old; delete target[rowName]; @@ -407,11 +442,13 @@ export class Table extends LitElement { table.addHook("afterRemoveRow", (_, amount, physicalRows) => { nRows -= amount; physicalRows.map(async (row) => { + const rowName = rowHeaders[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 unresolved[row]; + this.onUpdate(rowName, null, undefined); // NOTE: Global metadata PR might simply set all data values to undefined }); }); diff --git a/src/renderer/src/stories/hot.js b/src/renderer/src/stories/hot.js index 795dc949c..23333d186 100644 --- a/src/renderer/src/stories/hot.js +++ b/src/renderer/src/stories/hot.js @@ -38,21 +38,6 @@ class DateTimeEditor extends Handsontable.editors.BaseEditor { // Attach node to DOM, by appending it to the container holding the table this.hot.rootElement.appendChild(this.DATETIME); - - // // Immediately transfers the CopyPastePlugin FocusableWrapper element to the WC Shadow Root - const copyPastePlugin = this.hot.getPlugin("copyPaste"); - const ogFn = copyPastePlugin.getOrCreateFocusableElement.bind(copyPastePlugin); - copyPastePlugin.getOrCreateFocusableElement = () => { - const res = ogFn(); - const focusable = copyPastePlugin.focusableElement.getFocusableElement(); - const root = this.hot.rootElement.getRootNode(); - focusable.style.position = "absolute"; - focusable.style.opacity = "0"; - focusable.style.pointerEvents = "none"; - copyPastePlugin.getOrCreateFocusableElement = ogFn; - root.append(focusable); - return res; - }; } getValue() { @@ -94,6 +79,7 @@ class ArrayEditor extends Handsontable.editors.TextEditor { getValue() { const value = super.getValue(); + if (!value) return []; else { const split = value 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 694cf4719..c0c40066b 100644 --- a/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js +++ b/src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js @@ -18,17 +18,21 @@ export class GuidedSubjectsPage extends Page { // Abort save if subject structure is invalid beforeSave = () => { - this.info.globalState.subjects = merge(this.localState, this.info.globalState.subjects); // Merge the local and global states + try { + this.table.validate(); + } catch (e) { + this.notify(e.message, "error"); + throw e; + } - const { results, subjects } = this.info.globalState; + // Delete old subjects before merging + const { subjects: globalSubjects } = this.info.globalState; - // Object.keys(subjects).forEach((sub) => { - // if (!subjects[sub].sessions?.length) { - // delete subjects[sub] - // } - // }); + for (let key in globalSubjects) { + if (!this.localState[key]) delete globalSubjects[key]; + } - const noSessions = Object.keys(subjects).filter((sub) => !subjects[sub].sessions?.length); + 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" @@ -37,6 +41,16 @@ export class GuidedSubjectsPage extends Page { throw new Error(error); } + this.info.globalState.subjects = merge(this.localState, globalSubjects); // Merge the local and global states + + const { results, subjects } = this.info.globalState; + + // Object.keys(subjects).forEach((sub) => { + // if (!subjects[sub].sessions?.length) { + // delete subjects[sub] + // } + // }); + const sourceDataObject = Object.keys(this.info.globalState.interfaces).reduce((acc, key) => { acc[key] = {}; return acc; @@ -44,13 +58,6 @@ export class GuidedSubjectsPage extends Page { // Modify the results object to track new subjects / sessions updateResultsFromSubjects(results, subjects, sourceDataObject); // NOTE: This directly mutates the results object - - try { - this.table.validate(); - } catch (e) { - this.notify(e.message, "error"); - throw e; - } }; footer = {