Composing
Every menu, bar, tab list and panel the grid and the workspace draw is a list you can rewrite, built-in entries included.
The grid's menus and the workspace's bars are not fixed. Each one is a list of declared entries, and the grid hands you that list before it draws it. You get the built-in entries, each with a stable id, plus what the list is about (the column, the record, the view). You return the entries to show.
(defaults, context) => entriesThe built-in surfaces are made the same way. The grid declares its own entries as data and draws them through the same renderer as yours, so keeping, dropping, moving, relabelling or adding an entry is ordinary list work. Built-in entries you pass through unchanged keep all their built-in behaviour.
The parts
The grid's parts are composed by the grid's compose (a createClientGrid option, a live <Grid compose> prop, or
api.setOptions({ compose })). The workspace's are composed by the workspace's compose.
| Part | Where it is set | Entries | Context |
|---|---|---|---|
| Every grid menu | compose.menu | GridMenuEntry | { api, menu, … } (by menu, below) |
| Commands and their keys | compose.commands | GridCommand | { api } |
| Status bar | compose.statusBar | GridStatusItem | { api } |
| Command bar | workspace.compose.bar | WorkspaceSection | { api, view } |
| Bulk bar | workspace.compose.bulkBar | WorkspaceControl | { api, view, ids } |
| Every workspace menu | workspace.compose.menu | GridMenuEntry | { api, view, menu, … } (by menu, below) |
| Record inspector tabs | workspace.compose.inspector | WorkspaceInspectorTab | { api, view, record } |
| The workspace's commands | workspace.compose.commands | GridCommand | { api, view } |
| Customize panel | workspace.compose.customize | WorkspaceSettingsSection | { api, view } |
| Board and gallery cards | workspace.compose.card | WorkspaceCardPart | { api, view } |
| Column and section heads | workspace.compose.groupHeader | WorkspaceGroupPart | { api, view } |
| Quick filter chips | workspace.compose.quickFilters | WorkspaceQuickFilter | { api, view } |
Composers are asked each time their surface opens or refreshes, so they can follow the app's state. A workspace
composer is told the grid's api, the active view, selected(), focused(), recordData(id), navigate,
updateSettings and toast, plus what its part is about.
A package of composers is a plain compose object, for the grid or the workspace. combineCompose(a, b, c) makes several
into the one compose each takes: each part runs through them in order, a later one seeing what an earlier one added
(see Your own additions). In React,
compose and actions are live props. A new compose takes effect on the next open, and the status bar redraws at
once. api.setOptions({ compose: undefined }) brings back the built-in parts.
Menu entries
Every menu uses one entry shape: a GridMenuItem or 'separator'.
| Field | Meaning |
|---|---|
id | Names a built-in entry; give yours one so other code can find them |
label | The text |
icon | An icon from the grid's set by name (copy, trash, bolt…), an <svg>, or text (an emoji) |
hint | A key shown beside it |
title | A tooltip |
checked | Marks the current choice |
disabled | Shown but not pressable |
danger | Drawn as destructive |
heading | A label row, not an item |
color | A dot beside it (field values, lanes) |
run() | What it does |
Separators that end up leading, trailing or doubled after you compose are dropped, so you can filter entries out without tidying up after them.
Every menu draws its entries the same way, wherever it opens. A row shows its icon or colour dot, its label, and a check mark or key hint. Headings and separators work in every menu. Arrow keys, Home and End move between rows, Enter or Space picks one, and Escape closes the menu. An entry looks and behaves the same in the cell menu, a column menu, a workspace menu or the sidebar.
Grid menus
compose.menu composes every menu the grid opens. context.menu says which one, and its other fields describe it:
menu | Opens from | Context | Built-in ids |
|---|---|---|---|
cell | Right-click a cell | rowId, colField, pointer, selection | copy, cut, paste, clear, selectAll, filterByValue, excludeValue, clearColumnFilter, chartRange, chartData, exportAll, exportExcel, exportSelected, action:<id> |
column | A column header's menu | colField, column | sortAscending, sortDescending, clearSort, pinLeft, pinRight, unpin, group, pivot, editField, insertLeft, insertRight, deleteField, filter |
view | A saved view in the Views panel | view | default, rename, duplicate, delete |
import { Grid, type ColumnDef, type GridCompose } from '@witbybit/wit-grid-react';
interface Invoice {
id: string;
amount: number;
}
declare const rows: Invoice[];
declare const columns: ColumnDef<Invoice>[];
declare function annotate(rowId: string, field: string): void;
declare function showHistory(field: string): void;
const compose: GridCompose<Invoice> = {
menu: (entries, context) => {
// Narrow on `menu` first: each kind brings its own fields.
if (context.menu === 'cell')
return [
...entries.filter((entry) => entry === 'separator' || !entry.id?.startsWith('export')),
'separator',
{ id: 'note', label: 'Add a note', run: () => annotate(context.rowId, context.colField) },
];
if (context.menu === 'column')
return [
// No pinning on this screen.
...entries.filter((entry) => entry === 'separator' || !['pinLeft', 'pinRight', 'unpin'].includes(entry.id ?? '')),
'separator',
{ id: 'history', label: 'Column history', run: () => showHistory(context.colField) },
];
// Shared views are managed by an admin: only duplicating them here.
if (context.view.scope === 'team') return entries.filter((entry) => entry !== 'separator' && entry.id === 'duplicate');
return entries;
},
};
export function Payables() {
return <Grid<Invoice> rows={rows} columns={columns} compose={compose} sidebar={{ panels: ['views'] }} />;
}A cell menu's entries that do not apply to the cell (filtering a column that cannot be filtered, exporting an empty
selection) are already left out of the defaults, and a cell whose menu you compose empty opens none. In a column
menu, the filter entry is the column's filter editor: wherever you place it, the editor is drawn there, and leaving
it out removes the filter from the menu. A column whose menu you compose empty shows no menu button. For a filter UI
of your own in the menu, give the column a custom filter editor (filterDef): the filter entry draws it.
The status bar
compose.statusBar composes the bar under the grid (shown with statusBar). Its defaults are rows, selected
(while records are selected) and sync (an offline grid's sync state). An item is a label and a value, or your own
element. Items with end: true are docked at the bar's end. The bar is composed again whenever it refreshes: as
rows, filters, grouping, selection, paging or sync change.
import type { GridCompose } from '@witbybit/wit-grid-react';
interface Invoice {
id: string;
amount: number;
}
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
const compose: GridCompose<Invoice> = {
statusBar: (items, { api }) => {
const total = api.records('displayed').rows.reduce((sum, invoice) => sum + invoice.amount, 0);
return [...items, { id: 'total', label: 'Total', value: usd.format(total), end: true }];
},
};Workspace menus
compose.menu composes every menu the workspace opens. context.menu says which one, and its other fields
describe it:
menu | Opens from | Context | Built-in ids |
|---|---|---|---|
record | Right-click a record, its … button | record, ids | open, showInTable, action:<id>, duplicate, delete |
export | The bar's Export | ids (selected) | print, download, xlsx, csv, selected, selected:xlsx, selected:csv |
viewTab | A saved view's tab menu | tab | scope, useShared, save, revert, lock, rename, duplicate, default, delete, saveAs |
boardColumn | A board column's menu | column, label, ids | select, add, limit, moveLeft, moveRight, collapse, split, hide |
workload | A Gantt workload cell | resource, day | reassign:<id>, level |
fieldValue | A field's quick edit on a card | record, field | heading, option:<value>, clear |
import type { GridWorkspaceOptions } from '@witbybit/wit-grid-react';
const workspace: GridWorkspaceOptions = {
compose: {
menu: (entries, context) => {
// Narrow on `menu` first: each kind brings its own fields.
if (context.menu === 'record') return entries.filter((entry) => entry === 'separator' || entry.id !== 'delete');
if (context.menu === 'export') return entries.filter((entry) => entry === 'separator' || entry.id !== 'csv');
return entries;
},
},
};The bars
The command bar and the bulk bar have their own sections in Views:
compose.bar takes WorkspaceSections (built-in parts by id: title, tabs, newView, search… viewControls, actions,
summary) and compose.bulkBar takes WorkspaceControls (set:<field>, dates, action:<id>, duplicate,
delete).
Inspector tabs
The inspector shows a record's tabs, and the first one opens unless the person chose another. Built-in tabs:
comments, activity, files and links (from collaboration), and the view's own tabs (a schedule's schedule).
You draw your own tab with render(container): return a React element (drawn through the portal tree), a DOM node
or text, or draw into container yourself and return the cleanup.
import type { GridWorkspaceOptions } from '@witbybit/wit-grid-react';
declare function Approvals(props: { record: string }): JSX.Element;
const workspace: GridWorkspaceOptions = {
compose: {
inspector: (tabs, { record }) => [
{ id: 'approvals', label: 'Approvals', render: () => <Approvals record={record} /> },
...tabs.filter((tab) => tab.id !== 'files'),
],
},
};Commands
What an entry does is a command: copy, sort ascending, or your "Approve", declared once with
its keys and availability. compose.commands (the grid's, then the workspace's, which adds its own like view:<tab>
and palette) changes or drops one, or adds yours. Keys, every menu, the bars and the command palette follow. A menu
entry or a bar control that names a command ({ command: 'approve' }) takes its label, icon, key hint, checked state
and availability from it. The palette lists every command with a group.
import type { GridWorkspaceOptions } from '@witbybit/wit-grid-react';
declare function reconcile(): void;
const workspace: GridWorkspaceOptions = {
compose: {
commands: (commands) => [
...commands,
{ id: 'reconcile', label: 'Reconcile with the ledger', group: 'Payables', keys: ['mod+shift+r'], run: reconcile },
],
},
};The Customize panel
compose.customize composes the panel's sections. The view's own sections, plus the shared records and
show, come in as builtIn sections. You can keep, drop, move or retitle them, but their settings stay the view's.
Your own sections bring their own settings.
import type { GridWorkspaceOptions } from '@witbybit/wit-grid-react';
let strict = true;
const workspace: GridWorkspaceOptions = {
compose: {
customize: (sections) => [
...sections.filter((section) => section.id !== 'show'),
{
id: 'matching',
title: 'Matching',
settings: [{ kind: 'toggle', id: 'strict', label: 'Strict 3-way match', value: strict, onChange: (on) => (strict = on) }],
},
],
},
};Cards and group headers
compose.card composes the parts of every board and gallery card below its title (badges, meta, progress,
details, foot), and compose.groupHeader the parts of every board column and gallery section header (total;
count, total, progress in a gallery). Each is composed once per view. Your card part has a fixed height and a
render(row, reader); your header part a render(group, reader). See
Your content in the views.
Where to see it
The approval desk on the home page is composed this way. Its bar, record menu, bulk bar and inspector are the built-in ones, rewritten.
Last updated on
Views and the record workspace
One record model, many projections — table, gallery, calendar, Kanban, Gantt and chart share selection, writes, undo and the inspector.
Fields people add
Let the people using the grid add, change and delete fields themselves — kept as plain JSON wherever your app keeps it.