From b4d2c8608979868fe6d02cc4f2d9839e9bb9b825 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 18 Aug 2026 18:26:18 -0500 Subject: [PATCH 1/5] Migrate device table to TanStack Table v9 --- package.json | 2 +- pnpm-lock.yaml | 42 +++-- src/components/dashboard/device-table-grid.ts | 156 +++++++++--------- src/components/dashboard/device-table.ts | 42 ++--- src/components/dashboard/prefs.ts | 4 +- src/components/dashboard/render-content.ts | 4 +- src/components/dashboard/table-columns.ts | 9 +- src/components/dashboard/table-features.ts | 38 +++++ src/pages/dashboard.ts | 4 +- test/components/dashboard/prefs.test.ts | 4 +- .../dashboard/table-columns.test.ts | 21 ++- 11 files changed, 199 insertions(+), 127 deletions(-) create mode 100644 src/components/dashboard/table-features.ts diff --git a/package.json b/package.json index 1b38e0635..6da22b938 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@lit/reactive-element": "2.1.2", "@mdi/js": "7.4.47", "@replit/codemirror-indentation-markers": "^6.5.3", - "@tanstack/lit-table": "^8.21.3", + "@tanstack/lit-table": "^9.1.2", "codemirror": "^6.0.2", "esptool-js": "^0.6.1", "improv-wifi-serial-sdk": "^2.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f58f5a0cb..462d6f99b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,8 +62,8 @@ importers: specifier: ^6.5.3 version: 6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.8) '@tanstack/lit-table': - specifier: ^8.21.3 - version: 8.21.3(lit@3.3.3) + specifier: ^9.1.2 + version: 9.1.2(@lit/context@1.1.6)(lit@3.3.3) codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -534,15 +534,24 @@ packages: '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tanstack/lit-table@8.21.3': - resolution: {integrity: sha512-NXe/No6HRGN+OZMJ1pKmoejiFS1QfjirWYOiahBWMojSsLIB5SqYb45L/jlswpKhEAaMWCdps8c93z8HfMG9Kg==} - engines: {node: '>=12'} + '@tanstack/lit-store@0.14.1': + resolution: {integrity: sha512-YzcmHhuH9siOvFiEH56HIQqkipkPaUJ3weC0jVKe8l3AZ2autYnpMGNvCG5tZFgmdsmvxr6W01zcW+yLlsQ9Tg==} + peerDependencies: + lit: ^3.0.0 + + '@tanstack/lit-table@9.1.2': + resolution: {integrity: sha512-jNNOeajZsC2NXXUFTtsYCIr753PpxMP1qeSQ4KoPDV2hoDQqBTPjpVYktjs8LFX4gXS6ATuZDBpqBNarTbHS6g==} + engines: {node: '>=20'} peerDependencies: + '@lit/context': ^1.1.0 lit: ^3.1.3 - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} + '@tanstack/store@0.11.1': + resolution: {integrity: sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==} + + '@tanstack/table-core@9.1.2': + resolution: {integrity: sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==} + engines: {node: '>=20'} '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1824,12 +1833,23 @@ snapshots: dependencies: tslib: 2.8.1 - '@tanstack/lit-table@8.21.3(lit@3.3.3)': + '@tanstack/lit-store@0.14.1(lit@3.3.3)': dependencies: - '@tanstack/table-core': 8.21.3 + '@tanstack/store': 0.11.1 lit: 3.3.3 - '@tanstack/table-core@8.21.3': {} + '@tanstack/lit-table@9.1.2(@lit/context@1.1.6)(lit@3.3.3)': + dependencies: + '@lit/context': 1.1.6 + '@tanstack/lit-store': 0.14.1(lit@3.3.3) + '@tanstack/table-core': 9.1.2 + lit: 3.3.3 + + '@tanstack/store@0.11.1': {} + + '@tanstack/table-core@9.1.2': + dependencies: + '@tanstack/store': 0.11.1 '@tybys/wasm-util@0.10.3': dependencies: diff --git a/src/components/dashboard/device-table-grid.ts b/src/components/dashboard/device-table-grid.ts index 648a6e86f..72861c31a 100644 --- a/src/components/dashboard/device-table-grid.ts +++ b/src/components/dashboard/device-table-grid.ts @@ -8,17 +8,18 @@ import type { LocalizeFunc } from "../../common/localize.js"; import { tourAnchor } from "../guided-tour/tour-anchor.js"; import { getActiveTourConfiguration } from "../guided-tour/tour-session.js"; import type { DeviceRow } from "./table-columns.js"; +import type { DeviceTableFeatures } from "./table-features.js"; export interface DeviceTableHeadProps { - table: Table; + table: Table; selectMode: boolean; allSelected: boolean; onToggleAll: () => void; } export interface DeviceTableBodyProps { - table: Table; - rows: Row[]; + table: Table; + rows: Row[]; selectMode: boolean; selectedDevices: Set; highlightConfiguration: string | null; @@ -40,7 +41,7 @@ export function renderDeviceTableHead(p: DeviceTableHeadProps): TemplateResult { return html` ${p.table.getHeaderGroups().map( - (hg: HeaderGroup) => html` + (hg: HeaderGroup) => html` ${ p.selectMode @@ -54,50 +55,55 @@ export function renderDeviceTableHead(p: DeviceTableHeadProps): TemplateResult { ` : nothing } - ${hg.headers.map((header: Header) => { - const sorted = header.column.getIsSorted(); - const canSort = header.column.getCanSort(); - return html` - header.column.toggleSorting() : nothing} - > - - ${ - header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext()) - } - ${ - canSort - ? html`` - : nothing - } - - - `; - })} + ${hg.headers.map( + (header: Header) => { + const sorted = header.column.getIsSorted(); + const canSort = header.column.getCanSort(); + const ariaSort = + sorted === "asc" + ? "ascending" + : sorted === "desc" + ? "descending" + : "none"; + const sortIcon = + sorted === "asc" + ? "chevron-up" + : sorted === "desc" + ? "chevron-down" + : "unfold-more-horizontal"; + return html` + header.column.toggleSorting() : nothing} + > + + ${ + header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + ) + } + ${ + canSort + ? html`` + : nothing + } + + + `; + } + )} ` @@ -161,32 +167,34 @@ export function renderDeviceTableBody(p: DeviceTableBodyProps): TemplateResult { ` : nothing } - ${row.getVisibleCells().map((cell: Cell) => { - // The stacked mobile layout (table-styles.ts) shows each - // cell's column header as a field label. It's a real - // span (not a CSS ::before) so screen readers announce - // it on mobile, where the is hidden; the span is - // display:none on desktop, so it stays out of the a11y - // tree there (the column header already provides context). - // Name is the card title and actions is a button row, so - // neither gets a label. Only string headers can be used as - // a label; a future flexRender (template/function) header - // would stringify to "[object Object]", so skip it. - const id = cell.column.id; - const header = cell.column.columnDef.header; - const label = - id !== "name" && id !== "actions" && typeof header === "string" - ? header - : null; - return html` - ${ - label !== null - ? html`${label}` - : nothing - } - ${flexRender(cell.column.columnDef.cell, cell.getContext())} - `; - })} + ${row + .getVisibleCells() + .map((cell: Cell) => { + // The stacked mobile layout (table-styles.ts) shows each + // cell's column header as a field label. It's a real + // span (not a CSS ::before) so screen readers announce + // it on mobile, where the is hidden; the span is + // display:none on desktop, so it stays out of the a11y + // tree there (the column header already provides context). + // Name is the card title and actions is a button row, so + // neither gets a label. Only string headers can be used as + // a label; a future flexRender (template/function) header + // would stringify to "[object Object]", so skip it. + const id = cell.column.id; + const header = cell.column.columnDef.header; + const label = + id !== "name" && id !== "actions" && typeof header === "string" + ? header + : null; + return html` + ${ + label !== null + ? html`${label}` + : nothing + } + ${flexRender(cell.column.columnDef.cell, cell.getContext())} + `; + })}