Skip to content

Tables

A plugin describes a table by its columns — a title, an optional description and a registered value type — and passes the rows. The shell renders each cell with the column’s type; an editable cell opens that type’s editor on double-click (or Enter) and reports the validated value. The table never writes state: you emit an event, a handler updates the store.

const { Table } = ctx.ui;
const columns = [
{ key: "title", type: "text", title: ctx.i18n.key("table.title"),
description: ctx.i18n.key("table.titleHint"), editable: true, width: "w-64", order: 10 },
{ key: "due", type: "date", title: ctx.i18n.key("table.due"), order: 20 },
{ key: "effort", type: "duration", title: ctx.i18n.key("table.effort"), order: 30 },
];
<Table
columns={columns}
rows={rows} // [{ id, title, due, effort }, …]
emptyText={ctx.i18n.key("table.empty")}
onRowClick={(rowId) => ctx.emit("notes/document.select", { docId: rowId })}
onCommit={(rowId, key, value) =>
ctx.emit("notes/document.rename", { value, context: { docId: rowId } })}
/>
  • A column’s type is any registered value type id, including plugin types.
  • title / description are i18n keys, translated at render time; the description shows as the header tooltip.
  • Without editable: true (or without onCommit) the column is read-only. Escape cancels an edit in progress. onRowClick only fires on read-only cells, so it never fights with the double-click that starts an edit.
  • config is passed to the value type (enum options, duration units…).
  • A row is identified by its id field; idKey names another field.
  • In TypeScript you can also import ValueTable from @/kernel directly.
  • Try it: install /plugins/notes-tsx/index.js and run Notes: Documents table — the title column is editable and reuses the rename event.

Clicking a header cycles the sort: ascending, descending, none. The sort is saved with the table’s per-view state, like column widths. Only the visible rows are drawn (virtualized), so long tables stay fast.

A tables binding gives either the whole row list (sorted in memory by the shell) or a paged provider, sync or async, for a database or a server:

ctx.ui.define({
tables: {
customerRows: {
async getRows({ offset, limit, sort }, context) {
const { rows, total } = await db.query(context.viewId, { offset, limit, sort });
return { rows, total };
},
subscribe: (listener) => db.onChange(listener), // drop cached pages
},
},
});

The table asks pages of 100 rows covering what is on screen, draws placeholders while they load, and asks again when the sort changes or subscribe fires. With a provider, sorting is yours: honour request.sort.

Two table-level flags turn on a control column on the left. They are independent of the per-column editable flag, which is what makes a cell editable — including in the input row.

<Table
columns={columns}
rows={rows}
creatable // input row on top, with a "+" button
deletable // checkboxes + trash button (Delete key)
onCreate={(values) => ctx.emit("notes-tsx/task.add", { docId, task: values })}
onDelete={(rowIds) => ctx.emit("notes-tsx/task.remove", { docId, taskIds: rowIds })}
/>
  • creatable shows an extra row on top of the existing ones: every editable column is filled there with its type’s editor, and the + button of the control column reports the collected values as one object through onCreate (Escape clears the input row). It stays disabled until something is typed, and the row is cleared once the values are reported.
  • deletable gives each row a checkbox — plus a “select all” checkbox in the header — and a trash button that reports the checked row identifiers through onDelete. The Delete key does the same when the table has focus and no cell is being edited.
  • Same rule as everywhere: the table emits, the handler writes. The table creates and deletes nothing by itself, and forgets a selected identifier as soon as that row leaves the data.
  • Try it: install /plugins/notes-tsx/index.js, create a note, unfold it in the Notes panel and open Tasks — name, description, due date and a done checkbox, with creation and deletion enabled.

Every table gets a compact toolbar, with nothing to declare:

  • Filter shows a filter row under the header. The editor follows the column: options in config.options → multi-choice, boolean → yes/no, number/integer/percent/duration/currency and date/datetime → min/max range, anything else → “contains” on the value text. Active filters show as removable chips. Set filterable: false on a column to opt out.
  • Columns opens a small table of every column: drag a row to reorder, tick to show/hide, tick the right checkboxes and Fit selection to auto-fit a subset.
  • Fit auto-fits every visible column; double-clicking a header edge or the header context menu fits one column (widest rendered cell, 60–480 px).

Filters live in the per-view state (TableViewState.filters, filterRowVisible) like order, widths and sort. In-memory rows are filtered by the shell (filterTableRows); a paged TableDataProvider receives them in request.filters and applies them itself (arrayTableProvider already does).

Add children to a table declaration to fold sub-items under each row:

"children": {
"rows": { "ref": "subtaskRows" },
"hasChildren": "subtaskCount",
"createKey": "name",
"createEvent": "my-plugin/sub.add",
"commitEvent": "my-plugin/sub.update",
"deleteEvent": "my-plugin/sub.remove",
"selectEvent": "my-plugin/sub.select"
}
  • A chevron unfolds a row; unfolded rows are kept in per-view state (expanded).
  • rows is a tables binding evaluated with the view context plus parentId, only for unfolded parents. Children use the parent’s columns, by key.
  • hasChildren names a row field (number > 0 or true). With createKey and a create event, an unfolded parent shows a “+ sub-item” input that emits { parentId, values: { [createKey]: text } }.
  • Commit / delete events default to the parent’s and always carry parentId. A child click emits children.selectEvent (one level per selection).
  • Sorting applies within each level; children stay under their parent. Paged providers are not unfolded (in-memory rows only).

Documentation under CC BY 4.0 · © SCFlow contributors ·Back to supplychainflow.org