From a50895ab8677025e0f621652fd5cdf08ec811081 Mon Sep 17 00:00:00 2001 From: Marko Petzold Date: Mon, 2 Nov 2026 09:31:17 +0100 Subject: [PATCH 1/2] feat: add edit column functionality with Material Icons support - Introduced an 'edit' column type in the widget that allows users to trigger actions via an icon button. - Updated README and documentation to reflect the new 'edit' column type and its usage. - Enhanced the demo to showcase the edit column alongside links to board pages. - Added Material Symbols font loading for the edit button icons. - Updated the Vite configuration to externalize the @material/web dependency. - Modified the definition schema to include new properties for the edit column. - Improved rendering logic to handle edit buttons and links appropriately. --- CLAUDE.md | 20 ++-- README.md | 74 ++++++++---- demo/index.html | 44 +++++++- package-lock.json | 27 +++++ package.json | 2 + src/definition-schema.d.ts | 22 +++- src/definition-schema.json | 43 +++++-- src/widget-table.ts | 226 ++++++++++++++++++++++++++++++++----- vite.config.ts | 2 +- 9 files changed, 383 insertions(+), 77 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 890c264..f2deddd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,30 +16,36 @@ No test framework or linter is configured. Node `>=24.9.0`, npm `>=10.0.2`. ## Architecture -Single-file Lit 3 web component (`src/widget-table.ts`) wrapping `@vaadin/grid` (peer dependency, externalized in the bundle). The component is registered as `widget-table-versionplaceholder`; the Vite build replaces `versionplaceholder` with the package version via `@rollup/plugin-replace`, so the live tag is e.g. `widget-table-1.1.17`. Consumers must use the version-suffixed tag matching the installed package — this lets multiple widget versions coexist on one page in the IronFlock dashboard. +Single-file Lit 3 web component (`src/widget-table.ts`) wrapping `@vaadin/grid` (peer dependency, externalized in the bundle), with `@material/web`'s icon button for edit columns (peer dependency, externalized too: Studio's import map provides both and loads the Material Symbols font `md-icon` draws with). The component is registered as `widget-table-versionplaceholder`; the Vite build replaces `versionplaceholder` with the package version via `@rollup/plugin-replace`, so the live tag is e.g. `widget-table-1.1.17`. Consumers must use the version-suffixed tag matching the installed package — this lets multiple widget versions coexist on one page in the IronFlock dashboard. Build pipeline (`vite.config.ts`): - Library mode, ES-only output, entry `src/widget-table.ts` → `dist/widget-table.js`. -- Externalizes `lit*`, `@lit*`, `@vaadin*` so consumers provide them. +- Externalizes `lit*`, `@lit*`, `@vaadin*` and `@material/web` so consumers provide them. - `tslib` aliased to `tslib/tslib.es6.js`; `process.env.NODE_ENV` defined to `"production"`. ### IronFlock platform contract -The widget exposes two reactive properties consumed by the host dashboard: +The widget exposes two reactive properties consumed by the host dashboard (it needs no `route`: it never reads the URL, only asks Studio to change it): - `inputData: TableConfiguration` — typed by `src/definition-schema.d.ts`, generated from `src/definition-schema.json`. The JSON schema is the source of truth for both the runtime type and the dashboard's config UI (note the non-standard `order`, `dataDrivenDisabled`, `condition.showIfValueIn`, and `color: true` keys consumed by the IronFlock config editor). - `theme: { theme_name, theme_object }` — ECharts-style theme object. `registerTheme()` resolves colors with this precedence: CSS custom properties (`--re-text-color`, `--re-hover-color`, `--re-border-color`) on the host element override `theme_object` values. These are not snapshotted: `registerTheme()` stores a `var(--re-…, )` chain, so a change to the host property repaints the tile live without the widget being told. The grid paints no background of its own (`--grid-bg-color` stays unset): studio's `widget-item` already paints `--re-tile-background-color` on the tile, and painting it again here composited a translucent tile colour twice, and the grid, its rows and its cells each painting it stacked it several times. Only the sticky header is opaque: `headerSurfaceColor()` composites the tile as displayed (`--re-tile-background-color` over `--re-background-color` over white, read at render time, cached by `opaqueSurface()`), so rows scrolling under it stay hidden. +The widget acts on the platform only through two events dispatched on itself (`bubbles: true, composed: true`): `param-submit` `{ params: { [key]: value } }` from an edit column's button (Studio merges the parameter into the page URL, hands every widget the new `route` and re-queries bindings whose filter uses it), and `nav-submit` `{ path }` from a `button` or `image` link to a board page. Nothing handles them in the Widget Editor's preview or the AI chat. + ### Data shape and rendering -`inputData.columns` is column-oriented (each column carries its own `values[]`). `transformInputData()` pivots columns into row records keyed `col_` for Vaadin Grid, then reverses (newest-first). Vaadin sort paths use `col_.value`. +`inputData.columns` is column-oriented (each column carries its own `values[]`, bound separately in Studio, so rows line up by position only and a row has no identity of its own). `transformInputData()` pivots columns into row records keyed `col_` for Vaadin Grid, in the order the data arrives (oldest first, as Studio delivers it). Vaadin sort paths use `col_.value`. + +Cell rendering dispatches on `column.type` in `renderCell()`: `string`, `number` (with `precision`), `boolean`, `state` (parses `stateMap` string `"'ONLINE': 'green', ..."` into a colored `.statusbox`), `button`, `image`, `timestamp`, `edit`. + +Links (`renderLink()`, for `button` and `image`): a link with a scheme or `//` is a web address and opens in a new tab (`rel="noopener noreferrer"`); anything else is a board page and dispatches `nav-submit`, from a real `` + } + renderButton(cell: Values[number], colDef: Column) { if (this.isEmptyValue(cell?.value)) return this.renderEmptyValue() - return html`${cell?.value}` + return this.renderLink(cell?.value, cell?.link) } renderImage(cell: Values[number], colDef: Column) { if (this.isEmptyValue(cell?.value)) return this.renderEmptyValue() - return html`` + return this.renderLink(html``, cell?.link) + } + + /** + * An edit column's cell: an icon button that hands the row's value, the + * record key, to Studio as a URL parameter (param-submit), for a form that + * opens on that parameter or a widget filtering on it. Without a key the + * button is disabled, with the reason as a tooltip on the wrapper: a + * disabled Material button has pointer-events: none, so a title on the + * button itself never shows. vaadin-grid does not count md-icon-button as + * focusable: focus-target lets Enter in the cell reach it, and + * skipCellActivate keeps the click from also toggling the row's active + * item. The label names the row, so screen readers can tell the buttons + * apart. + */ + renderEditButton(cell: Values[number], colDef: Column) { + const value = this.parameterValue(cell?.value) + if (value === undefined) return this.renderEmptyValue() + const key = String(colDef.styling?.parameterKey ?? '').trim() + const icon = String(colDef.styling?.iconName ?? '').trim() || DEFAULT_EDIT_ICON + const label = String(colDef.header ?? '').trim() || 'Edit' + return html` + { + ;(event as Event & { skipCellActivate?: boolean }).skipCellActivate = true + if (key) this.submitParameter(key, value) + }} + >${icon} + ` } renderTimestamp(value: any, colDef: Column) { @@ -346,6 +445,7 @@ export class WidgetTable extends LitElement { case 'boolean': case 'state': case 'image': + case 'edit': return 'center' default: return 'start' @@ -546,6 +646,43 @@ export class WidgetTable extends LitElement { color: inherit; text-decoration: underline; } + + /* A button column's link to a board page, drawn like the anchors. */ + .link-button { + all: unset; + cursor: pointer; + color: inherit; + font: inherit; + text-decoration: underline; + } + + .link-button:focus-visible { + outline: auto; + } + + /* An edit column's icon button, sized by the cell's font (Material's + 40px button would make these rows taller than text rows) and + coloured by the cell's text: currentColor, substituted into + Material's shadow DOM, resolves to the .cell colour chain, so a + board style edit repaints it. The focus ring's default is a Material + system colour that is invisible on a dark tile. */ + .edit-button { + display: inline-flex; + } + + md-icon-button { + --md-icon-button-icon-size: 1.25em; + --md-icon-button-state-layer-width: 2em; + --md-icon-button-state-layer-height: 2em; + --md-icon-button-icon-color: currentColor; + --md-icon-button-hover-icon-color: currentColor; + --md-icon-button-focus-icon-color: currentColor; + --md-icon-button-pressed-icon-color: currentColor; + --md-icon-button-disabled-icon-color: currentColor; + --md-icon-button-hover-state-layer-color: currentColor; + --md-icon-button-pressed-state-layer-color: currentColor; + --md-focus-ring-color: currentColor; + } ` /** @@ -564,6 +701,26 @@ export class WidgetTable extends LitElement { return opaqueSurface(dashboard ? [dashboard, tile || themeBg] : [themeBg, tile]) } + /** + * The body renderer of a column: the cell's content in the column's font + * and colour. An edit column is a plain vaadin-grid-column (sorting by the + * record key means nothing, and the sort column forbids a header renderer) + * whose header is centred by a renderer: the cell content is a flex + * container (see the styles), so vaadin's textAlign alone does not centre + * a bare header text. + */ + private cellRenderer(col: Column, colIndex: number) { + return (item: RowData) => html` +
+ ${this.renderCell(item[`col_${colIndex}`], colIndex)} +
+ ` + } + render() { const columns = this.inputData?.columns ?? [] @@ -603,32 +760,43 @@ export class WidgetTable extends LitElement { ${repeat( columns, (col, i) => `${col.header}-${i}`, - (col, colIndex) => html` - ( - (item) => html` -
- ${this.renderCell(item[`col_${colIndex}`], colIndex)} -
- `, - [colIndex, col, this.themeTitleColor] - )} - >
- ` + (col, colIndex) => + col?.type === 'edit' + ? html` + + html`
${col.header}
`, + [col.header] + )} + ${columnBodyRenderer( + this.cellRenderer(col, colIndex), + [colIndex, col, this.themeTitleColor] + )} + >
+ ` + : html` + ( + this.cellRenderer(col, colIndex), + [colIndex, col, this.themeTitleColor] + )} + > + ` )} diff --git a/vite.config.ts b/vite.config.ts index bc35a2f..2645095 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,7 +32,7 @@ export default defineConfig({ }, sourcemap: true, rollupOptions: { - external: [/^lit/, /^@lit/, /^@vaadin/], + external: [/^lit/, /^@lit/, /^@vaadin/, /^@material\/web/], output: { banner: '/* @license Copyright (c) 2025 IronFlock GmbH. All rights reserved.*/' } From cf6ff0fdc4c34a73c2d3eacdbdfa7cdadff729e6 Mon Sep 17 00:00:00 2001 From: Marko Petzold Date: Mon, 2 Nov 2026 09:42:46 +0100 Subject: [PATCH 2/2] release widget-table 1.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a9b67a5..7bd3a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ironflock/widget-table", - "version": "1.1.27", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ironflock/widget-table", - "version": "1.1.27", + "version": "1.2.0", "license": "MIT", "dependencies": { "lit": "^3.3.3", diff --git a/package.json b/package.json index 5da298b..ae075ad 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "REWidget widget-table", "license": "MIT", "author": "widget-table", - "version": "1.1.27", + "version": "1.2.0", "engines": { "node": ">=24.9.0", "npm": ">=10.0.2"