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) => entries

The 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.

PartWhere it is setEntriesContext
Every grid menucompose.menuGridMenuEntry{ api, menu, … } (by menu, below)
Commands and their keyscompose.commandsGridCommand{ api }
Status barcompose.statusBarGridStatusItem{ api }
Command barworkspace.compose.barWorkspaceSection{ api, view }
Bulk barworkspace.compose.bulkBarWorkspaceControl{ api, view, ids }
Every workspace menuworkspace.compose.menuGridMenuEntry{ api, view, menu, … } (by menu, below)
Record inspector tabsworkspace.compose.inspectorWorkspaceInspectorTab{ api, view, record }
The workspace's commandsworkspace.compose.commandsGridCommand{ api, view }
Customize panelworkspace.compose.customizeWorkspaceSettingsSection{ api, view }
Board and gallery cardsworkspace.compose.cardWorkspaceCardPart{ api, view }
Column and section headsworkspace.compose.groupHeaderWorkspaceGroupPart{ api, view }
Quick filter chipsworkspace.compose.quickFiltersWorkspaceQuickFilter{ 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.

Every menu uses one entry shape: a GridMenuItem or 'separator'.

FieldMeaning
idNames a built-in entry; give yours one so other code can find them
labelThe text
iconAn icon from the grid's set by name (copy, trash, bolt…), an <svg>, or text (an emoji)
hintA key shown beside it
titleA tooltip
checkedMarks the current choice
disabledShown but not pressable
dangerDrawn as destructive
headingA label row, not an item
colorA 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:

menuOpens fromContextBuilt-in ids
cellRight-click a cellrowId, colField, pointer, selectioncopy, cut, paste, clear, selectAll, filterByValue, excludeValue, clearColumnFilter, chartRange, chartData, exportAll, exportExcel, exportSelected, action:<id>
columnA column header's menucolField, columnsortAscending, sortDescending, clearSort, pinLeft, pinRight, unpin, group, pivot, editField, insertLeft, insertRight, deleteField, filter
viewA saved view in the Views panelviewdefault, 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:

menuOpens fromContextBuilt-in ids
recordRight-click a record, its … buttonrecord, idsopen, showInTable, action:<id>, duplicate, delete
exportThe bar's Exportids (selected)print, download, xlsx, csv, selected, selected:xlsx, selected:csv
viewTabA saved view's tab menutabscope, useShared, save, revert, lock, rename, duplicate, default, delete, saveAs
boardColumnA board column's menucolumn, label, idsselect, add, limit, moveLeft, moveRight, collapse, split, hide
workloadA Gantt workload cellresource, dayreassign:<id>, level
fieldValueA field's quick edit on a cardrecord, fieldheading, 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.

Edit on GitHub

Last updated on

On this page