diff --git a/src/electron/frontend/core/components/BasicTable.js b/src/electron/frontend/core/components/BasicTable.js index ad047d6d2..acfbbce2b 100644 --- a/src/electron/frontend/core/components/BasicTable.js +++ b/src/electron/frontend/core/components/BasicTable.js @@ -450,6 +450,9 @@ export class BasicTable extends LitElement { for (let key in this.ignore) delete entries[key]; for (let key in this.ignore["*"] ?? {}) delete entries[key]; + const schemaOrder = this.#itemSchema.order ?? []; + const order = this.keyColumn ? [this.keyColumn, ...schemaOrder] : schemaOrder; + // Sort Columns by Key Column and Requirement const keys = (this.#keys = @@ -459,8 +462,7 @@ export class BasicTable extends LitElement { ...this.#itemSchema, properties: entries, }, - this.keyColumn, - this.#itemSchema.order + order )); // Try to guess the key column if unspecified diff --git a/src/electron/frontend/core/components/JSONSchemaForm.js b/src/electron/frontend/core/components/JSONSchemaForm.js index 57c5b0f5e..581b4f681 100644 --- a/src/electron/frontend/core/components/JSONSchemaForm.js +++ b/src/electron/frontend/core/components/JSONSchemaForm.js @@ -1459,6 +1459,14 @@ export class JSONSchemaForm extends LitElement { this.#resetLoadState(); + // this.updateComplete.then(() => { + // this.#toggleRendered(); // Toggle internal render state + + // // Promise.all([...Object.values(this.forms), ...Object.values(this.tables)]).map(o => o.updateComplete).then(() => { + // // this.#toggleRendered(); // Toggle internal render state + // // }) + // }) + const schema = this.schema ?? {}; this.resolved = structuredClone(this.results); // Track resolved values as a copy of the user-specified results diff --git a/src/electron/frontend/core/components/SimpleTable.js b/src/electron/frontend/core/components/SimpleTable.js index 1db95ee1a..4ec4ef49b 100644 --- a/src/electron/frontend/core/components/SimpleTable.js +++ b/src/electron/frontend/core/components/SimpleTable.js @@ -968,14 +968,17 @@ export class SimpleTable extends LitElement { for (let key in this.ignore) delete entries[key]; for (let key in this.ignore["*"] ?? {}) delete entries[key]; + const schemaOrder = this.#itemSchema.order ?? []; + const order = this.keyColumn ? [this.keyColumn, ...schemaOrder] : schemaOrder; + if (!this.keyColumn && !order.includes("name")) order.unshift("name"); + // Sort Columns by Key Column and Requirement this.colHeaders = sortTable( { ...this.#itemSchema, properties: entries, }, - this.keyColumn, - this.#itemSchema.order ?? ["name"] // Specify the order of the columns + order // Specify the order of the columns ); // Try to guess the key column if unspecified diff --git a/src/electron/frontend/core/components/Table.js b/src/electron/frontend/core/components/Table.js index 6c7a66983..3882fea2a 100644 --- a/src/electron/frontend/core/components/Table.js +++ b/src/electron/frontend/core/components/Table.js @@ -12,9 +12,7 @@ const rowSymbol = Symbol("row"); const maxRows = 20; -const isRequired = (col, schema) => { - return schema.required?.includes(col); -}; +const isRequired = (col, schema) => schema.required?.includes(col); export const getEditable = (value, rowData = {}, config, colName) => { if (typeof config === "boolean") return config; @@ -22,7 +20,7 @@ export const getEditable = (value, rowData = {}, config, colName) => { return getEditable(value, rowData, config?.[colName] ?? true); }; -export function sortTable(schema, keyColumn, order) { +export function sortTable(schema, order = []) { const cols = Object.keys(schema.properties) //Sort alphabetically @@ -38,22 +36,15 @@ export function sortTable(schema, keyColumn, order) { if (aRequired) return -1; if (bRequired) return 1; return 0; - }) - .sort((a, b) => { - if (a === keyColumn) return -1; - if (b === keyColumn) return 1; - return 0; }); - return order - ? cols.sort((a, b) => { - const idxA = order.indexOf(a); - const idxB = order.indexOf(b); - if (idxA === -1) return 1; - if (idxB === -1) return -1; - return idxA - idxB; - }) - : cols; + return cols.sort((a, b) => { + const idxA = order.indexOf(a); + const idxB = order.indexOf(b); + if (idxA === -1) return 1; + if (idxB === -1) return -1; + return idxA - idxB; + }); } // Inject scoped stylesheet @@ -263,14 +254,14 @@ export class Table extends LitElement { } // Sort Columns by Key Column and Requirement + const order = this.keyColumn ? [this.keyColumn, ...(this.#itemSchema.order ?? [])] : this.#itemSchema.order; const colHeaders = (this.colHeaders = sortTable( { ...this.#itemSchema, properties: entries, }, - this.keyColumn, - this.#itemSchema.order + order )); // Try to guess the key column if unspecified diff --git a/tests/components/forms.test.ts b/tests/components/forms.test.ts index a609f1698..f4d32ea28 100644 --- a/tests/components/forms.test.ts +++ b/tests/components/forms.test.ts @@ -19,6 +19,7 @@ async function mountComponent(props) { document.body.append(form) await form.rendered + // await form.updateComplete; return form; } @@ -163,10 +164,10 @@ describe('JSONSchemaForm', () => { const row = users.getRow(0) const newData = { name: 'John Doe', age: 30 } - await Promise.all(Object.entries(newData).map(([key, value]) => { + Object.entries(newData).map(([key, value]) => { const cell = row.find(cell => cell.simpleTableInfo.col === key) return cell.setInput(value) - })) + }) await sleep(100) // Wait for updates to register on the table diff --git a/tests/components/table.test.ts b/tests/components/table.test.ts new file mode 100644 index 000000000..b281e9f01 --- /dev/null +++ b/tests/components/table.test.ts @@ -0,0 +1,92 @@ +// tests/table.test.js +import { expect, test, describe, vi, beforeEach, afterEach } from 'vitest'; +import { Table, sortTable } from '../../src/electron/frontend/core/components/Table.js'; +import { sleep } from '../puppeteer.js'; + +global.ResizeObserver = global.IntersectionObserver = vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})) + +const createComponent = async (props = {}) => { + const element = new Table(props); + document.body.appendChild(element); + await element.updateComplete; + return element; +} + +const itemSchema = { + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + aliases: { type: 'array', items: { type: 'string' } } + }, + required: ['name'] +} + +const schema = { + type: 'array', + items: itemSchema +} + +describe('Table component', () => { + + + test('should render the table', async () => { + const element = await createComponent(); + const div = element.querySelector('table'); + expect(div).toBeTruthy(); + element.remove() + }); + + test('should set schema and update columns', async () => { + const element = await createComponent({ schema }); + + const colHeaders = element.colHeaders; + expect(colHeaders).toEqual(['name', 'age', 'aliases']); + element.remove() + }); + + test('should add rows and validate data', async () => { + const element = await createComponent({ schema }); + + const row = { name: 'John Doe', age: 30, aliases: ['Johny', 'Doe'] } + element.data = [row]; + await element.updated(); + await sleep(1000); + + const tableData = element.table.getData(); + expect(tableData).toEqual([ Object.values(row) ]); + element.remove() + }); + + test('should work with key column', async () => { + const element = await createComponent({ schema, keyColumn: 'name' }); + + const key = 'John Doe'; + const row = { age: 30, aliases: ['Johny', 'Doe'] } + const data = { [key]: row }; + element.data = data; + await element.updated(); + await sleep(1000); + + const tableData = element.table.getData(); + expect(tableData).toEqual([ [key, ...Object.values(row)] ]); + element.remove() + }) + + test('should sort table columns correctly', async () => { + const schema = { + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + email: { type: 'string' }, + }, + required: ['name'] + }; + + const sortedColumns = sortTable(schema, 'name', ['age', 'email']); + expect(sortedColumns).toEqual(['name', 'age', 'email']); + }); +}); diff --git a/vite.config.js b/vite.config.js index 5667e299b..8eaacbb51 100644 --- a/vite.config.js +++ b/vite.config.js @@ -54,11 +54,15 @@ export default defineConfig({ // Just rendering "src/electron/frontend/core/components/CodeBlock.js", + // Depends on server communication + "src/electron/frontend/core/components/NWBFilePreview.js", + // Unclear how to test "src/electron/frontend/utils/popups.ts", "src/electron/frontend/utils/download.ts", "src/electron/frontend/utils/upload.ts", "src/electron/frontend/core/components/FileSystemSelector.js", // Uses Electron dialog + "src/electron/frontend/core/components/DandiResults.js", // Needs DANDI API Key and network access (unless possibly with a static mocked response) ], }, },