Changelog
What shipped in each release.
Changelog vs. Migration Notes
This page is "what shipped." Migration Notes is "what breaks and how to fix it" — check that page before upgrading a pinned version.
next (unreleased)
Fixed — groups and the schedule, found walking Resolution Desk (plan 191 review)
- A server-side group open by
defaultExpandedread as closed (isExpanded), so its chevron did nothing: it closes and opens again. - Sticky group headers drew only for client grids: server-side groups stick too.
- A command-bar menu a redraw had closed took two presses of its button to open again.
- A saved grouping with
defaultExpandedwas refused when restored. - The workspace took any
data-record-idin a view for one of its records: a click on a schedule's task card selected it as a case and opened the inspector beside the schedule's own panel. Ids it does not hold are the view's.
Added — opening groups from their row (plan 191 review)
grouping.toggleOnRowClick: a click anywhere on a group row opens or closes it (default on fordisplay: 'row'). Saved grouping keepsdisplay,toggleOnRowClickand a datadefaultExpanded.- The workspace's Group menu, for a grouped table: headers in the group column or across the row, sticky headers, expand all and collapse all.
Added — columns that fill the grid (plan 191 decision 4)
ColumnDef.flex: flex columns grow from their width to share the room the other columns leave, withinmaxWidth, following the grid's size; one the person resizes keeps that width. A flex column'swidthis no longer copied intocolumnWidths(it is where it grows from, not a width set for it).PivotConfig.resultColumns({ width, flex, minWidth, maxWidth }) sizes a pivot's generated columns;{ flex: 1 }fills the grid beside the row labels. Resolution Desk's analysis drops its own sizing on every resize for it.
Added — a record's page, with the way back (plan 191 decision 1)
views.open(id, { record })opens a view for one record (a view definedtab: falsestays out of the switcher);views.back()returns to the view it was opened from and the record focused there.getStatus().viewssaysrecordandcanGoBack; a view type reads its record withview.record(); a link{ view, record }to a page opens it for that record.- Coming back to the table reveals the record focused when it is drawn, in the cell that was focused on it.
Fixed — found walking Resolution Desk's impact analysis in the browser (plan 190 step 16)
- The pivot view over a server asked for its pivot twice on opening (its first draw asked again); it asks once.
- The bar counted a server pivot by the rows the table had loaded ("8 records loaded" over 150 cases): it counts the
records the server's pivot counted, as complete. A view type's
count()may now return{ count, complete }(GridViewCount), andrefreshControls()re-reads the count too. - Resolution Desk's nav follows the view on show (the board, the analysis).
- A view's summary worked out over the records loaded so far says so ("Covers: Loaded records only"), whatever view drew it: the board's, the gallery's and the chart's tallies over a server-side grid looked complete. A view that names its own coverage, or counts every record itself, is taken at its word.
Added — a change says where the person made it (plan 190 step 13)
GridChanges.placeand thewriteBlockedpayload'splace:'table'(the grid's own editing, paste and fill),'inspector', a view's type ('kanban', an app's own type), or the place an app names —commitEdit(…, { place })and a field control'splaceoption. Absent for an app's own writes, the server's and collaborators'. An activity log or an audit trail reads it instead of guessing from the last element clicked.
Fixed — found by the plan 190 audit (step 13)
- A grid with no renderer (a server render, a test, a grid not mounted yet) reported the dark theme whatever its
themeName:getStatus().themeis now the theme its state names, with itsthemeoverrides, and follows a switch.
Fixed — found by Resolution Desk's impact analysis (plan 190 M4)
- The pivot view over a server-side grid showed the pages loaded as its totals. With a
getPivotdatasource it now shows the server's pivot of every matching record, asked again when the filters, the search or a record change, and never the records' own columns while it waits.GridViewTypeContext.serverPivot()gives any view type the same. - A server pivot's cell names its records by their condition: Show these records says the server holds them
(
recordsOnServer) rather than "loaded so far", a double-click shows them in the table (it did nothing), and the table's bar counts them by the server's count and never says "N changed since" against ids it could not have. refreshServerSide()while pivoting asks for the pivot again.- Color scales, data bars and icon sets paint a server-side grid's pivot (they read pivot values from the client's rows only), and the pivot view's heatmap and summary follow each answer from the server (they stayed empty).
Fixed — found by Resolution Desk's response board (plan 190 M2)
- Server data is never pre-judged by the person's rules: a server's (or another replica's) records land even when a rule is async (they were silently refused as a sync write that could not wait); their issues are reported after.
- A server-side grid's reload that left before a save landed no longer undoes the change: loaded rows are reconciled with what this device has not seen saved yet, and a save's answer wins over an older load.
- A board move waiting on its rule is marked pending on its card, and a refused one keeps its reason beside the card.
Changed — Kanban is a view type on the public contract (plan 190 step 12.1)
- The board is written against the same view contract as an app's view type; the contract gained what it needed
(
columns,create,counts,highlight,motion,presence,composeContext,composePart,composedMenu,showRecords,refuseLocked; instancesettings,count,metrics,metricValue,inspectorTabs,locate,print).
Added — React view types, and the interaction contract (plan 190 step 11.2)
view.mount(content)draws a DOM node or adapter content (a React element) into a view type's host, again in place on each render (state kept), taken down with the view.onKey(event, id)on a view type's instance hears keys on a focused record.og-view-card: the record-card style an app's view builds on, drawing the workspace's selected, focused, pending and warning marks from the theme.
Added — an app's view type is a first-class view (plan 190 step 11.1)
defineViewType<Row, Settings>({ settings: { defaults, version, migrate }, requires }): typed settings with defaults and migration of older shapes ($version); fields it needs are asked for ("needs a date field", with a list) instead of the view drawing from nothing.view.writeanswers{ status: 'applied' | 'refused' | 'pending', rejected, settled };settledis never delivered after the view is destroyed.view.signalandview.own(cleanup)end with the view;changes(),position,canEdit,memory/remember,reportPlace,accent,refreshControls,confirmandopenInTableare an app view's as they are a built-in's.
Added — Resolution Desk's first journey closes these gaps (plan 190 M1)
draft.proposed()carriesissues(the record's rules judged on the proposed record, live) andchecking(an async rule has not answered yet).draft.apply(proposal, { save: true })applies and saves; refused by the server, the draft holds the edits again with the reason (getStatus().drafts[id].refused).
Fixed — found walking Resolution Desk in the browser (plan 190 M1)
- A theme switch on a mounted grid is heard by
watch((grid) => grid.themeName, …)(the state changed before the renderer did, so watchers read the old name and heard nothing); a grid bound to a record's rows follows it live. - A bound grid with theme tokens of its own (a brand's accent) still follows its owner's light or dark theme: the name and the tokens are followed apart.
- The number editor hands on text that is not a number yet (a lone
-) as text: the record as proposed waits on it, and committing it is refused by the column's parse — never read as an empty cell. - The inspector no longer lists the grid's own hierarchy column ("Group") as a record field on a grouped server-side grid.
- A field control's empty placeholder says "Add…" for a field typed into, and "Choose…" (with its chevron) only for one picked from options or dates.
Fixed — server-side grids keep what the person had (plan 190 M1)
- The selection is kept when a query (a lens, a sort) reloads the rows; records no longer loaded are named by a
command's target (
notLoaded) and by the bulk bar ("2 selected · 1 not loaded"). A new datasource clears it. - Open and closed groups stay as left across a new query, groups follow
grouping.defaultExpanded, and changing only the grouping asks the server again. - A view's
group.defaultExpanded(when it is data) opens and saves with the view. - A view's settings are its presentation's own: only a table's
settings.columnsis a column arrangement (a board's lanes and an app type's owncolumnssetting draft and save unchanged).
Added — app content belongs to the workspace (plan 190)
- The workspace controller has its services for any app content:
toast,announce,confirmandbusy(work, label). In React,useWorkspace()gives it inside a<Grid>, beside one in a<GridScope>, or for a given api. workspace.theme:tokens(),subscribe()andbind(element)give app content outside the workspace's DOM its theme, live; React<WorkspaceTheme>wraps a dialog or portal in it.
Fixed — theme changes reach getStatus().theme watchers (plan 190)
- A theme switch now notifies
api.watch((grid) => grid.status.theme, …)(anduseGridWatch): the grid's listener was added before its theme manager existed and never heard a change.
Added — the record as proposed, read once (plan 190)
draft.proposed(): the record committed, drafted and as typed in a grid bound to one of its collections, its calculated fields worked out; pure, and the same object until what it reads changes.draft.subscribe, anduseProposed(rowId, api)in React. Input that does not parse yet is reported (input.parses: false,waiting), never counted as 0.runCommand(id, { proposed: { [rowId]: revision } }): a submit made on a record as proposed then answers{ status: 'changed' }when it moved since, and does not run.GridCollection.input(optional): told what a bound grid is typing.
Changed — a command on the selection acts on what the view shows (plan 190)
- Taken from the selection (bulk bar, palette, keys,
runCommand(id)), a command acts on the selected records in the open view's effective query (filter, lenses, search), drawn or not; the target names the others inoutside, and a server-side grid's unloaded ones innotLoaded. Records given explicitly (runCommand(id, { rowIds })) are not filtered by the view. - The workspace's bulk bar says how many selected records are outside the view and offers to include them.
- Focus on a record that leaves the view moves to the nearest record still shown instead of clearing.
Added — lenses: narrowing over whatever view is open (plan 190)
filter.lenses({ id, label, filter }): each ANDed with the view's own filter, the query and the search, never replacing them; kept as views switch and in the person's record; not part of a view (Save keeps them out,saveAs(name, { includeLenses: true })folds them in).getStatus().lensessays whether each is active, why not (a field the grid does not have), and the fields the view narrows too. Datasources receive them inrequest.filter.- The workspace's quick-filter chips are lenses: a chip on a field the view filters narrows beside it (the strip says why nothing matches), and two chips on one field both narrow. Lenses the app turns on are always shown.
Fixed — each view is its own on the workspace's stage (plan 190)
- Two of an app's view types no longer share one live instance (every app type was kind
custom), and two views of one kind (two calendars) each mount their own: the stage tells views apart by the open view and its type, and a view's memory (scroll, zoom, open groups) is kept per view. - Another view opened that draws the same presentation still mounts as itself.
- A drafted view's tab menu offers Save as a new view… beside Save changes and Revert changes; the switcher marks a drafted view's tab even while another is open.
Changed — one saving path: the grid itself is a view, and drafts (plan 190)
- Every grid has its own view from the start (the table as the app set it up), open while
activeIdisnull; every change to what the grid shows is a draft of the open view.open(null)opens it (as drafted);discard(null)is "back to the table as set up". - Drafts: kept across switches and in the person's record, joined part by part with shared changes (a part changed both
ways is a conflict, mine kept), a save owns the draft as pressed, a refusal keeps it with the reason, a deleted view
offers Save as new, a locked view's (or one the person may not manage) is their own.
getStatus().views.drafts;api.views.discardreplacesrevert. A view store maysubscribeto say its views changed elsewhere. storage.layoutis the person's record,{ v: 7, state, views }: which view is open, every draft, and what belongs to no view. A record of an earlier version is refused with a reason.storage.personalLayoutsandGridPersonalLayoutsare removed.- The workspace's built-in tabs (when the app defines no views) are the grid's own view and views offered for the session.
Changed — one named view, opened as defined, saved as its owner acknowledges (plan 190)
- A view is a destination:
{ id, name, presentation, query: { filter, sort, group }, scope, locked, revision }.api.viewsopens one as defined in one change (open;open(null)for none), keeps what the grid shows as a new one (saveAs), saves the open one (save), andrevert,rename,lock,duplicate,remove,setDefault. Its status isgetStatus().views(list,activeId,defaultId,modified,changes,persistent,loading,error). - The
viewsoption: the app's own views (defined, each with an orderedrevision), where stored views live (store, aGridViewStore; default: the grid'sstorage; without either, views made here last the session), andonSavefor the app to acknowledge saves of its own views (otherwise Save saves them as new). A save resolves tosaved,refused(with the owner's reason) orstale(the owner holds a newer definition). - Removed:
hasWorkspace,listViews,saveView,updateView,applyView,revertView,deleteView,duplicateView,renameView,setDefaultView,getStatus().savedViews, theviewSaved/viewApplied/viewDeleted/viewRenamedevents,GridSavedViews,GridWorkspaceState,SaveViewOptionsand saved-viewstateblobs;workspace.viewstabs (the switcher listsviews.definedand the stored views); a view settings'locked(a view is locked, not its settings).storage.viewsholds{ views, defaultId }. - Opening a view, like restoring a kept layout, adds no undo step.
- A server-side grid asks once for a change that moves several parts of its query (a view opening sets the sort and the filter together): the request goes out a microtask after the change.
Changed — how records are drawn is a presentation, { type, settings } (plan 190)
- The state key
viewis nowpresentation:{ type, settings }—{ type: 'table' }for the table, a built-in view, or an app's view type. Each built-in type has a factory that types its settings:tableView(),galleryView({ … }),kanbanView({ … }),calendarView,ganttView,chartView,pivotView(their settings types:GallerySettings,KanbanSettings, …).GridViewConfig,GridViewKindand the*ViewConfigtypes are no longer public. - An app's view type is a presentation like any other (
{ type: 'dock-schedule', settings }, was{ kind: 'custom', type, settings }); kept state keeps it, and restores it, whether or not the type is registered. workspace.viewstabs takepresentation(wasview); a composer's context haspresentationandupdateSettings(patch)(wasviewandupdateView);<Grid onPresentationChange>(wasonViewChange).- Kept state's schema version is 6: a state kept by an earlier version is refused with a reason.
Fixed — a collision on a collection names its lines (plan 190)
- A conflict on a collection field names the rows and fields that collided:
partson the conflict,collisionson a draft's state ({ lines: [{ id: 'l10', field: 'qty' }] }); the inspector's draft bar lists them. - A field type can name a collision's parts with
collisions(base, mine, theirs). - The collections contract says what holds: the field is written whole, changes made apart join row by row, and a collision is settled for the field.
Fixed — a bound grid belongs to its record (plan 190)
api.collectionanddraft.collectionreturn the same handle every call.- The record field's
canEdit(as proposed) applies to a bound grid's edits and to adding and removing its rows;canShowhides it in an entry layout or the inspector.GridCollectiongainsownerandallows(action). - A bound grid follows its owner's theme and
themetokens live, unless given its own. - Grids of rows in
createRecordForm,<RecordForm>and the inspector mount through the owner's adapter (React cells draw);createRecordFormtakesmountGrid, and an adapter gives one withmountGrid(AdapterGridMount). - The record deleted: a grid bound to it empties and refuses writes; bound to its draft, its rows stay readable. Out of a server-side cache: its rows stay and writes wait until it is loaded.
Fixed — calculated fields are deterministic, and their failures are told (plan 190)
- Calculated fields (
derive) are worked out in dependency order, whatever the column order; a cycle throws when the columns are set. - A calculation that throws refuses the write with the field and the error (it used to keep the old value silently).
- Records a write adds get their calculated fields worked out; a draft that adds shows them in its proposal.
reseton a column removes settings of its type (reset: ['renderer']); a setting leftundefinedinherits the type's, as before.
Fixed — saving some records keeps the save's boundary (plan 190)
api.data.save({ records })saves only those records' changes, as queued when it is called: other records' changes stay queued and their issues do not stop it; a change spanning them and others is refused (blocked,reason), never split.- Each save running together is judged by its own records: one save's blocking issue no longer holds another back.
requires: { saved: true }saves its records only: confirming one order no longer sends another's unfinished changes, and an edit made while it saves stays queued instead of blocking the send.- A server-side
matchingtarget stayscomplete: false, with itsquery, through its prerequisites.
Fixed — an operation's answer never overwrites what changed since it was sent (plan 190)
sendis given the operation:send({ id, target, input, signal })(wassend(target, input, signal)). Itsidis the same for its rechecks.- Its answer is joined with what changed since it was sent: an edit made meanwhile stands (queued, with
data.save), one the answer changed too is a conflict, a later answer is not undone by an older one, a record removed meanwhile is not brought back. recheck(operation): a command that can ask the server whether anunknownsend landed; the result'srecheck()never sends it again, answerspendingwhile the server works, and can be asked again.- Destroying the grid aborts an operation waiting on the server: its answer applies nothing (
failed). - Work that waits takes a basis:
collection.basis(),draft.basis(),api.basis(ids);writewrites only what the work changed and reportsconflictswith what changed meanwhile, which stands. - A collection type always names its rows (
schema.rows.getRowId);columnsis optional there.
Fixed — a draft owns its undo, and undo follows where the person works (plan 190)
- A grid bound to a record refuses, with the record's reason, a write the record refuses: nothing commits there and nothing hears it.
draft.history: a draft's own undo history. A grid or field control bound to the draft records into it; cancelling discards it, so undo never brings cancelled lines back; applying makes one step of the grid's history.draft.reset(fields)takes a list: resolving several conflicts is one step.- The workspace's undo and redo (the bar, the keys, the palette) act on the history of the part being worked in: the record's draft in a drafting inspector, else the grid's. An emptied draft history says so and stops there.
GridCollectionHistoryis nowGridUndoHistory; a collection always has a history (a draft's collection, the draft's).
Added — field types, collections, operations and a composable workspace (plan 189)
- Field types.
defineFieldTypedefines a kind of value once; a column names it withtype. The grid's own types are made with it (numberType,selectType…).deriveworks a value out from other fields in the same change;mergejoins changes made apart.fields.typeslets people pick your types for fields they add (api.getFieldTypes()). Field types - Field controls bound to what they edit.
createFieldControl(api, field, { bind: { record } | { draft } | { value } })(<FieldControl record | draft | value>), following the record or draft; the inspector's fields are field controls. - Collections.
collectionType,api.collection(id, field),draft.collection(field)andcreateClientGrid({ collection })/<Grid collection>: a record's rows edited in a grid bound to them, one write of the record per change, saved with it, joined row by row when changed apart.cellEditor: falsemarks a field never typed into. Collections and entries - Entries.
createRecordForm/<RecordForm>andinspector.layout: a record's fields in sections,whenper stage, rows as bound grids. - Saving on server-side and infinite grids, the record as the server saved it (
accepted[].current), andsave.version(versions read from the record).records('matching')carries aqueryon remote data. - Operations.
GridCommand.sendwithrequires: { drafts, saved }:accepted,refused,conflictorunknown; a command result can beblocked. Commands - Capabilities.
canShow: whether a field applies to a record, asked with the record as proposed. - Your own views.
defineViewTypeandworkspace.viewTypes; a tab names one with{ kind: 'custom', type }. - Your own header. The workspace controller's
state(),subscribe()andpart(id);location.app. - One content contract for bar sections and inspector tabs:
rendergives a DOM node, text or a React element. - Option lookups that fail are asked again after a short wait;
invalidate()forgets learnt labels.
Removed (plan 189)
- The
*ColumnType()spread factories andColumnTypeDefinition(usetype: *Type()),GridRemoteDataOptions,inspector.fields(useinspector.layout), a section'srenderSectionandupdateand a tab'srenderTab(userender), andcreateFieldControl'svalue/onChange/rowIdoptions (usebind).
Added — data entry, and the inspector as a reading pane
editing.tabTo: 'editable'(Tab skips cells that cannot be edited),editing.tabInList: 'accept'(Tab takes the highlighted choice) andediting.newRows(Enter, ↓ or Tab past the end add a row; an untouched new row is taken back), with thenewRowcommand. Each default keeps the grid's usual behaviour.CellEditorProps.onCommit(value, move)andapi.commitEdit(…, { move }): an editor that handles Tab or Enter goes on to the next cell, as the grid's own does. The built-in number, date, colour, long-text, cascade and choice editors now go on to the next cell on Tab and to the one below on Enter, as the grid's own text editor always did.- The inspector:
placement: 'bottom',opens: 'onFocus',height,resizable(drag or arrow keys),expandable(inspector:expand; Escape gives the room back) andfieldsIn: 'tab'. A wide inspector lays its fields out in columns. - Quick filter chips:
count: true(how many records match, kept current) andtone. - View tabs that do not fit wait behind More. The saved views status has
changes(what differs from the view); the sidebar's Views button shows a dot while there are some, and the panel lists them. The open panel, the chart window and the page no longer mark a view as edited. - The inspector moves beside or under the view live (
movable,inspector:placement). Under the view it closes when closed (its leaving animation was overridden, so it stayed on screen and stopped following the selection). applyView(null); a view saved from the table shows the table when applied. Picking a built-in view tab no longer marks the saved view you came from as edited.columnStatereorders without naming the grid's own columns: dragging a column in the Columns panel moves it with row checkboxes on (the drop was ignored).- The bar's Save view button is gone: a changed view's tab shows a dot, and saving is in the tab's menu, the Views
panel and the palette (
view:save). - React content the workspace draws (inspector tabs, bar sections, sidebar panels) unmounts cleanly when its tab changes: switching inspector tabs drawn by React no longer throws.
- A grid inside the workspace (an inspector tab) keeps its keys from the workspace's commands.
- The PO workbench showcase: a buyer's day on one screen, after the approval desk on the landing page.
Added — a record's issues show in every view and the inspector
The rules' issues, the server's refusals and the app's own now show wherever the record does: a card, a calendar entry or a Gantt bar carries the record's worst issue (and rings the fields it shows that an issue names), and the inspector lists the record's issues and rings their fields. In the table, an issue that names no field marks the record's row. The workspace's bulk bar now shows over a selection in the table too.
Added — a write says what it is; the server's answers are an event
actionontransactionandprepare: 'approve', 'hold'. It rides on thechangedevent and on each changedata.savesends, and undo and redo keep it. Commits of different actions are never merged into one change to send.savedevent: what the server took and what it refused, for every save (automatic ones too).
The approval desk names each of its actions: its audit trail says "You approved on the board" and logs the AP's
answers, and the AP request carries each decision.
Fixed — live settings: new rules apply, the workspace keeps its place
- A
dataprop with new rule functions applies. React compareddataby shape and skipped functions, so a rule made again for new app state (an approval limit) was silently dropped.datanow compares like any live prop: plain parts by value, functions by identity. Rules in a constant or a memo still never re-check on a re-render. - New workspace options keep the person's place. A workspace mounted again (a live setting, new words) opens on
the tab and record the person was on, not back at
location. - A server answering per record can refuse several records of one change, each with its own
rowsand reason; the rest of the change is saved.
Added — a server can refuse some records of a change
A push's rejection can name rows: only those records of the change are refused (rolled back, the reason on their
fields), and the rest of it is saved. A bulk approval with one invoice the server will not take no longer loses the
other approvals. The approval desk saves to a pretend AP server that does exactly this.
Async rules (a server lookup) now check records at once, up to 16 at a time, in api.data.check(), before a save and
after a write: checking 160 records against a 60 ms lookup takes about a second, not ten.
Added — a pivot cell shows its records
Show these records (the drill-down popover and the cell's menu) runs the new showPivotRecords command: the
pivot turns off and the table shows exactly the cell's records, its keys as column filters. In a workspace the pivot
opens them in the table, the workspace's other filters kept.
Changed — views remember their own place
The pivot keeps its open groups and scroll across view switches. Each view's memory (scroll, zoom, open groups) is its own, per tab and per kind of view: a tab switching from a gallery to a board no longer hands one view the other's saved scroll (which could break the board).
Added — the views by keyboard
Arrow keys go where a view lays records out: on a board down a column and across to the nearest column with cards, on a
calendar by day (and a week on). In the schedule, Alt + Shift + ←/→ moves a task's end and L then L links
two tasks; each task's row describes its bar (dates, progress, what it waits on). A view says where an arrow goes
with neighbor(id, direction); without it, arrows follow its order.
Fixed — export answers to canExport everywhere
The grid's own export commands (exportAll, exportExcel, exportSelected) are unavailable when canExport says no,
from the menu and the keys alike. The workspace pivot's grid takes the app's capabilities (and follows them live), so
its menus cannot export, sort or copy what the app's grid may not; it pivots the workspace's matching records, and
says "Loaded records only" over a server or infinite grid.
Changed — views say when their figures are partial
Over a server or infinite grid, the workspace views qualify what they count: a board column shows 12+ (with a
tooltip saying it counts the records loaded so far), a chart's subtitle adds "loaded records only", and the schedule's
summary adds "Covers: Loaded tasks only". Over a WIP limit stays certain; under it no longer reads as a promise.
Changed — useGrid() and <GridScope> replace useGridApi()
useGrid() gives a React component its grid: inside a <Grid> it is that grid; beside one, wrap the grid and the
components that need it in <GridScope> and useGrid() anywhere inside is the scope's grid (api null until it
mounts, null again once it unmounts). Outside both it throws, rather than handing out a grid that never arrives.
One grid per scope; a grid inside a grid keeps to itself. useGridApi() goes; onGridReady stays, saying when the
grid is ready (and handing its api to the component that renders it).
Changed — React cell renderers are typed
In @witbybit/wit-grid-react, ColumnDef types a React renderer's component as
ComponentType<CellRendererProps<TRow>> (ReactCellRenderer): row is the row's type and value is unknown. A
component that declared narrower props ({ value: string }) takes unknown and narrows it. A column type
(ColumnTypeDefinition) draws with the grid's own renderers, never an adapter's. The as any on renderer specs in the
examples, and the leftover cast on onGridReady's api, are gone.
Changed — column filters patch per field
setState({ filter: { columns } }) changes only the fields it names: a filter sets that column's, null removes it,
and the rest stay (columns: null still clears all). Turning one column's filter on or off no longer means copying
the whole map. A saved layout or view applied still replaces the column filters exactly. The server and infinite
request types were already exact; the as FilterModel / as SortModel casts in the examples are gone.
Changed — what can be undone is status
getStatus().history ({ canUndo, canRedo }, watchable) replaces canUndo() and canRedo().
Changed — fewer implementation methods
The grid's own hooks leave the public api: flushCellUpdatesSync, getInsightDiagnostics, getRowLoadState (apps
ask isRowLoading(rowId)), getColumnIndex, getColumnField and extendSelection (apps set selection state).
isAutoSaveEnabled() goes: getStatus().persistence.autoSave says it.
Changed — language, workspace and rules are live
messages, workspace and data are options now, applied by setOptions (or a changed prop): new words redraw the
grid, its sidebar, workspace and menus; a new workspace mounts again (or goes, with false); new rules check every
record again and drop the issues of rules taken away. Only the records' identity and where they are kept stay fixed:
getRowId, initialState, storage, fields, and data.save / data.offline (read once, when saving starts).
Changed — one data api: rules, issues and saving
Everything about the records beyond showing them is one option, one action object and one read (Rules and saving):
dataoption:rules(cell,row),save(push, optionalpull/subscribe,when),offline(offline({ name }), a device copy),quality,diff,liveStream. It replacesdataIntegrityand theofflineoption. Saving is core: it needs no import.api.data:save()checks the records it sends (a blocking issue on one: nothing is sent), sends, settles, and returnssaved | nothing | blocked | rejected | failedwith every issue on the records sent.check(),discard(),resolve(),setIssues(),compareTo(),acceptChange(),stream(),clearDevice().getStatus().data:changes(whatpushgets),state,issues(every source),blocking,conflicts,diff,lastSavedAt,lastError,device.- Rules always run: on every edit, paste and fill (a blocking rule refuses the write), on rows that arrive, and on
save. The
validateOn*flags are gone. - Removed:
api.integrity,getStatus().integrityand.sync,getConflicts,resolveConflict,clearConflict,syncNow,getPendingChanges,discardPendingChanges,clearOfflineData, thechangesSyncedandchangesRejectedevents,DEFAULT_SYNC_MESSAGES.
Changed — dependable composition (plan 186)
preparetakes its own copy of the request; the proposal is frozen all the way down.- A blocking row rule refuses a write wherever its message lands; integrity issues carry
fields(one issue per rule and record). - Workspace delete, quick-add and pivot export follow
canDelete,editableandcanExport. - A command's target lists the records it could not find (
unavailable);completeis false then. getStatus().commandActivity(running,revision);useGridCommandsanduseGridRecords('range')follow everything their answer reads.- Lookups and rollups read a field as the table does (nested fields, getters, formulas).
- A display can name the other fields it
reads; its cells redraw when they change. - A composer that throws, or returns no list, leaves its part as built and reports a
composeruntime fault;setOptionsstores only the options that applied.
Changed — style rules see the public selection
A row or cell style rule's params.selection is typed as GridSelectionTarget (focus, anchor, bounds), the
selection commands receive; the grid's internal GridSelectionState is no longer exported. The cell being edited is
getStatus().edit (the editing option configures how editing works).
Changed — no single-value state getters
getOpenPanel(), isChartOpen(), getThemeName() and getView() go: each repeated a key of getState()
(sidebarPanel, chartOpen, themeName, view).
Changed — no state snapshot; no plugins
getStateSnapshot() and GridStateSnapshot go: each value has one read, getState(), getStatus(),
getOptions(), getColumns() or getFields(), and watch selects from the same reads (grid.columns beside the
state keys, status, options and fields). getColumns() is frozen and the same list until the columns change;
getState() and getStatus() hand out frozen copies, so a read cannot change the grid. A cell selection in
getState() always has a focus (no cell selected is selection: null). The plugin registry, which no app could
reach, goes: the cell context menu is the host's, as the sidebar and the chart window are.
Changed — one status read: getStatus()
What the grid reports, set by no one, is one read: getStatus() holds loading, serverSide, pagination
({ pageSize, pageCount, totalRows }), edit (the cell being edited), sync, persistence, savedViews,
integrity and theme. It is frozen, and the same object until any of it changes. watch selects it as
g.status (watch((g) => g.status.sync, …)). getPagination, getSyncState, getPersistenceStatus,
getWorkspaceState and getTheme go, and so do watch's top-level sync, persistence, workspace, integrity,
theme, loading, serverSide, activeEdit and pagination. The page shown is state: getState().page.
Changed — getState() is stable
getState() returns a frozen object, the same one until the state changes (each value inside keeps its identity
until it changes). watch selects from it: watch((g) => g.selection, …) hears the public selection
({ focus, anchor? }, or null), and g.aggregation is the list of aggregations, as getState() has them.
Changed — a visual row's record is its id and data
A data row in a VisualRow (a group render context's row, a row renderer's) carries its record as
node: VisualRowRecord ({ id, data }): the grid's internal row node no longer shows through the public types.
Changed — a curated public surface
Core publishes four entries, one per audience, each name in exactly one: the root for apps, /offline, /adapter
(framework bindings; was /internal) and /testing (the flight recorder, trace replay, recording instrumentation and
createServerSideGetRowsRequest; was part of /experimental, which is gone). The root drops the grid's internals
(filter operator tables and model helpers, cell-pointer, hierarchy-column and server-route helpers, query-model
helpers, the insight registry, ThemeManager and the four theme constants, and others) and gains the types its
signatures name but could not be imported (GridWriteResult, RowSelectionGesture, ScrollToCellOptions,
SortModelItem, RuntimeFault, the saved-state blob types, the integrity state…). systemColorScheme() reads the
scheme the system prefers. A command or a menu is given the public cell pointer and GridSelectionTarget (the
selection with its bounds). The API contract is the manifest: every entry, every name, its kind.
Changed — one filter
The column filters, the quick filter and the query builder are one piece of state, filter: { columns, search, query }: set with setState({ filter }) (the parts given change, the rest stay; null clears all), read from
getState().filter, followed with watch((g) => g.filter), and sent to a server or infinite datasource as one
filter in each request. The quick filter is the search part, { text, fields? }. One function evaluates it.
getQuickFilter and evaluateQueryForRow go, and so do the exported evaluateQueryModel, applyQueryModelFilter
and createQueryEvaluationContext. Saved layouts keep filter (schema version 5).
Added — compose every menu, bar and panel
Every list the grid and the workspace draw is composed the same way: (defaults, context) => entries, the built-in
entries declared with stable ids and drawn by the same renderer as yours. See Composing.
- The grid:
compose(acreateClientGridoption, a live<Grid compose>prop, orapi.setCompose).compose.menucomposes every menu the grid opens, told apart bycontext.menu: the cell context menu (cell), a column's header menu (column, its filter editor thefilterentry) and a saved view's menu in the sidebar's Views panel (view).compose.statusBarcomposes the status bar:rows,selectedand an offline grid'ssyncstate, plus your own items (GridStatusItem). - The workspace:
compose.menunow composes every menu (record, export, view tab, board column, workload, field value), told apart bycontext.menu;compose.inspectorthe record's tabs (renderTabfor React content);compose.commandsadds the workspace's commands (and yours) to the grid's;compose.customizethe Customize panel.compose.barowns every part of the command bar, the new-view button (newView) included.compose.cardandcompose.groupHeadercompose the parts of board and gallery cards and of their column and section headers, your own among them.combineCompose(a, b, c)makes several packages of composers (for the grid or the workspace) into onecompose, each part through them in order, and every workspace composer is told the selection and the workspace's moves (WorkspaceComposeContext).compose.quickFilterscomposes the quick filter chips, the built-in ones by id. The bar'ssummarysection is in every view and the table, forcompose.barto fill (the workspace's ownsummaryoption is gone). - One entry shape for every menu:
GridMenuItem/GridMenuEntry. - One menu for every surface: the cell menu, column menus, workspace menus and sidebar menus draw the same rows on the same surface, in the grid's font, with the same keys. Built-in labels are sentence case ("Copy", "Sort ascending", "Export as CSV"). The app's actions in the cell menu show their icon, or a bolt when they have none.
Added — a composable command bar
The workspace's command bar is a list of sections, and its own parts are sections too: compose.bar(defaults, { view })
keeps, drops, moves or changes them and adds your own, in the same row and style as the built-in ones. See
Composing the command bar.
- Sections hold declared
metrics,controls(WorkspaceControl: buttons, toggles, menus) oractions, your own DOM (render/update), or a React component (renderSection, drawn through the portal tree). - Every view's own controls are declared
WorkspaceControls, drawn by the same renderer as yours. - Every menu the workspace opens is composed by
compose.menu(entries, context)(context.menusays which; a record's is'record'), its entriesGridMenuItems; the bar over a selection bycompose.bulkBar, its buttons declared controls. - The summary strip's defaults are declared numbers chosen from the records' roles: an invoice board no longer shows
"Active 0 · Blocked 0", an invoice gallery no longer "Average progress 100%". Schedules, pivots and charts add their
own numbers as
ComputedMetrics with ids. ViewMetricgainsidandwarn(the dot's colour while the value is above zero); new aggregatesoverdue,activeandblocked.
Changed — state: one read, one write
What a person changes and a saved layout keeps is one GridState: the columns' arrangement (columnState: width,
hidden, pin lane, order), row heights, sort, filters, the search text, the query, grouping, aggregation, the pivot,
the view, the page, the theme picked, the open sidebar panel and the chart window. api.getState() reads all of it;
api.setState(partial) changes the keys given, each by that key's own rules (a bare group column keeps its
GroupDef, the search text is trimmed), and setState(api.getState()) restores it. A columnState entry changes
only the column it names; a list naming every column also sets the order. A column hidden from a pin lane returns to
it when shown. The per-key setters go.
Persistence and saved views keep the whole GridState (before, the search text, aggregation, row heights, the page,
the open panel and the chart were lost on reload): the stored state is GridState as JSON (schema version 4), and
a restore applies it through setState. getGridState / applyGridState go: read and set state with getState /
setState.
The selection is state too: selectedRowIds, and selection (the focused cell, with an anchor when it spans a
range). selectCell, selectRange and clearRowSelection give way to setState; selectRows, deselectRows,
toggleRowSelection and selectAllRows to the one gesture they were shortcuts for, applyRowSelectionGesture.
Saved layouts keep every key but the selection.
Changed — what a column means, on the column
A column says what it means to the record views with role (title, status, owner, due, schedule, value…);
a role no column claims is still inferred, never from a column with a role, and role: false keeps a column out of
inference. A status column says which of its values are done and blocked: selectColumnType(options, { done, blocked })
(schema.done, schema.blocked). The workspace's records option and doneValues / blockedValues are gone; a view's
own records still picks another column for that view.
Changed — columns: one property per job
A column type is spread into a column (...dateColumnType()); the type registry and columnTypes prop are gone.
A column's text is one pair: format (renamed from valueFormatter; a display's text is now its format) and, new,
parse, which reads text back as a value wherever text comes in: paste, the text editor and the inspector. Every
built-in type sets both, so pasting $1,250.75 into a currency column writes 1250.75, and text a column cannot read is
refused and reported instead of written. onCopy / onPaste, the column required flag (use the required rule) and
checkboxSelection (the grid's own column, rowSelection.checkboxes for single selection) are gone.
Changed — a row node is a read
record(id).node keeps its fields (data, rowIndex, loadState, selected, expanded, level…) and loses its
writes: each has the grid's one way (transaction, applyRowSelectionGesture, revealRecord, api.integrity). A
failed row of a server or infinite grid is retried with the retryLoad command, aimed at the row (rowId); server and
infinite grids now find their loading and failed rows by id.
Changed — row shaping is state, its gestures are commands
Grouping, aggregation, pivot and, new, expansion (the groups, tree rows and details open) are state, read with
getState and set with setState (initialState.expansion too). Grouping a column, opening a group or a tree row,
opening every one and showing a detail are commands (group, ungroup, expand, collapse, expandAll,
collapseAll, openDetail, closeDetail): the keys, the toggles, the menus and your code run the same ones, and
composing them changes them everywhere. A command can aim at a row (rowId), a group's included. The pivot is read
with one pivotResult(), the records behind a cell from it.
Changed — one write
transaction (and { async: true }) and prepare / commit are the writes. A transaction's order sets the rows'
order in the same commit as its row and cell writes; a formula is a value written like any other. setCellValue,
setRowOrder, setFormula, clearFormula, refreshRows and flushTransactions leave the api, and a transaction no
longer carries columns, sortModel or filterModel (setColumns, setState).
Changed — two row reads
records(scope) and record(id) are the row reads. record(id) is { id, data, node } (null when there is none
or it is not loaded); row nodes gain level; records('range') is the data rows the cell selection spans. rows(),
getRowNode, getRawRowById, getDisplayedRowAtIndex, getDataRowAtVisualIndex, forEachNode,
forEachDisplayedNode, getSelectedRowIds and getSelectedRowCount go.
Changed — one cell read
api.cell(rowId, field) reads a cell as { raw, value, text }: as stored, through the column's getter or formula,
and as shown. text is the one text of a cell: export, copy and charts use it (the column's formatter, or a
display's text). getCellValue goes: cell(rowId, field).value.
Changed — events are things that happen
The events that only reported a state change leave the public event map: sortChanged, filterChanged,
quickFilterChanged, queryModelChanged, the grouping, pivot, tree, aggregation, detail and hierarchy events,
expansionChanged, selectionChanged, focusChanged, rowSelectionChanged, paginationChanged, the column events,
rowResized, fieldsChanged, commandsChanged, workspaceStateChanged, syncStateChanged and
serverSideStateChanged; and the render plumbing (cellInvalidated, renderInvalidated,
layoutTransitionCaptureRequested). Follow state with watch, which also has options beside the state;
diffIds(previous, next) says which ids joined or left. The flight recorder records each commit's cause, and
subscribeFlightRecorder (experimental) hears it record.
Added — why a state change happened
watch listeners hear a third argument, the change's cause: a person in the grid's own UI (source: 'user', with
the gesture), the app's code (api), a saved layout or view applied (restore), undo / redo, another
replica (remote) or a server (server). The watched view gains quickFilter, columnState, rowHeights,
expansion, serverSide and fields; sidebarOpenPanel is sidebarPanel, as in GridState.
Changed — options: one shape, every option live
What the app configures is one GridOptions shape: the same keys for createClientGrid and <Grid>, and every
option changes live through api.setOptions(partial), read back with api.getOptions(). theme holds tokens over
the theme the person picks, and stays over every theme they switch to.
Changed — <Grid> props are the grid's options
<Grid> takes GridOptions as its props, under the same names, and passes a changed prop to setOptions, so each
applies live. What only React could set is now an option, so a grid without React has it too: navigation,
editing (trigger, arrowKeysKeepEditing), contextMenu, chart, sidebar, dragOutHidesColumns,
autoRowHeight, pinnedRows (top, bottom) and performance (rowOverscanPx, colBuffer). The sidebar and the
chart window come and go with their options. mountGrid(api, element) draws a grid without an adapter, with keys,
the pointer and the cell menu as its options say.
Fixed: a column pinned in its definition (pinned: 'left') lost its lane when the grid had multiple row selection;
initialState.sidebarOpenPanel and initialState.chartOpen were ignored.
Changed — one storage option
Where the grid keeps what outlives a session is one create option, storage: { layout, views, personalLayouts, quickFilters }, each the same GridStorageAdapter (load / save / remove, sync or async), or a string for all
four in localStorage. workspace holds the workspace's options only and is a create option of the grid, so a grid
mounted without React (mountGrid) has its workspace too. Who may change a saved view's shared layout is the
canManageView capability; export and quick filters are turned off by canExport and by composing, as every other
part.
Fixed: a saved view of another schema version no longer disappears from the list; it says why it cannot be applied.
Changed — initialState is state; every other setting is an option
initialState takes state keys only (GridState, the keys getState() reads), applied as setState applies them:
what shapes the rows before they load (one sort, one first request to a server), the page and the selection once
they have. The rest of what used to be initial-only is an option, live through setOptions: rowSelection,
pagination, rowDrag, treeData, detail, hierarchyColumn, defaultColWidth, loadingSkeletonCount, and the
render limits under performance. GridCreateOptions names what a grid is made with; React warns only for those.
Fixed: new columns from the app (setColumns, React's columns prop) dropped the row selection checkbox column and
pinned the app's first column in its place.
Changed — one theme, one column menu
- The workspace wears the grid's theme: its accent is
focusRing, its corners and card shadow the newradiusandshadowtokens. The workspace's ownthemeoption is gone. - A column's menu is changed only by composing it (
compose.menu); its header shows the menu button only when the composed menu has something in it.headerMenuRenderer,headerMenuComponentandsuppressHeaderMenuare gone. - "Insert field" is offered only beside a field (not the group or selection column).
Added — displays: a way to show a field, defined once
displays (a grid option, a live <Grid displays> prop, or api.setDisplays) are named ways to show a field:
render(value, { row, column, surface, api }) and an optional text(value). A column names one (display: 'health')
and its cells, the workspace's cards and the record inspector draw it, while export and copy use its text. A card
field can name one too. The workspace's cardDisplays are these now. See Columns.
Changed — two ways to listen: on and watch
api.on(event, listener) hears events; api.watch(select, listener, { equals?, immediate? }) follows any state,
including an offline grid's sync, the persistence status, the workspace, data integrity and the theme. A
watch listens to exactly what its selector read, and calls once per commit. React's useGridWatch(select) is the
same as a hook. styleRules is part of the state snapshot now, so it can be read and watched. A theme watch made
before the grid mounts hears the themes it applies once mounted. See Events.
Changed — one rule per action, asked everywhere
What may happen is one rule per action (canEdit, canSelect, canSort, canGroup, …) on the column and in the
grid's capabilities: true, false, a reason, or a function answering the same way. api.can(action, target)
resolves them (the column's, then the grid's), and every surface asks it: a grid-level canEdit now shows the cells
it refuses as read-only, a grid-level canSort reaches the header, the sort panel and the sort commands, and
canSelect now stops a cell range from starting. See Columns.
Added — commands: every behaviour declared once
Every behaviour the grid and the workspace offer is a command (GridCommand): copy, paste, clear, select all, undo,
redo, edit, sort, pin, group, filter by value, export, a view switch, and the app's own ("Approve"). Each is declared
once with its label (text, or a function of its target), icon, keys, palette group, the records it acts on, when it
applies and what it does. Keys, the cell and column menus, the workspace's menus, bars and palette, and the app's own
buttons all run the same one. See Commands.
compose.commands(the grid's and the workspace's) changes, drops or adds commands: givecopyyour ownrunand Ctrl+C, the browser's Edit menu and the cell menu's Copy all run it. Composing a command'skeysrebinds it.- The plain grid now has undo (mod+z), redo (mod+y, mod+shift+z) and select all (mod+a) keys, not only the workspace.
- Menu entries and bar controls name a command (
{ command: 'approve' }) and take its label, icon, key hint, checked state and availability from it; every built-in menu is made this way. A menu about some records (a record's, a board column's) aims its commands at them. api.getCommands(target),api.runCommand(id, target), thecommandsChangedevent, anduseGridCommands()in React. A refused write fireswriteBlockedfromaction; a throw is a runtime fault.
Added — prepare a write, review it, commit it
api.prepare({ cells }) (or { async: true }) shows what cell writes would change, setters' effects included,
checked by the rules and capabilities, and changes nothing. api.commit(proposal) commits it as prepared, once, on the
grid that prepared it, and refuses it (stale: true) if one of its records changed since. See
Preview, approve, commit.
Added — the records an operation means
api.records(scope): every matching record (other pages and collapsed groups too), all, selected or
displayed, and whether they are complete (remote data: only the loaded ones). api.revealRecord(id) shows a record
wherever it is: it opens its groups and tree parents and turns to its page. See Grid API.
Added — paging, hooks and what a refusal was
api.getPagination()andapi.setPage(page): read and turn pages for client, infinite and server paging alike; the pagination bar uses them too.- React:
useGridRecords(scope), inside a<Grid>or given its api, re-renders as the records change. preparetakes record changes too (rows: { add, addIndex, remove }): a delete can be shown before it happens; proposals listadded,removedandrejectedRows.writeBlockedcarriesstale: truefor a reviewed write whose records changed after it was prepared.
Fixed — totals that depended on what was collapsed
- A chart over a grid with collapsed groups totalled 0, and a chart of a paged grid counted one page; charts now count every matching record, and say when remote data was counted only as far as it is loaded.
- A flat CSV or Excel export of a paged grid wrote one page; it now writes every matching record.
- The status bar's Rows counted every record, filtered out or not; it now counts the records that pass the filters (on every page, whatever is collapsed).
Added — offline and sync
A grid can keep its records on the device and keep working without a network: offline() from
@witbybit/wit-grid-react/offline (or @witbybit/wit-grid-core/offline). Records are stored in IndexedDB, every
edit is queued until your server acknowledges it, server changes are merged with pending edits, and a field changed
on both sides becomes a conflict to resolve. You provide two endpoints (push and pull) or a sync adapter;
httpSync speaks the documented protocol. See Offline and Sync.
<Grid offline={…}>, withrowsoptional and used once as seed data;useGridSyncState().api.getSyncState(),subscribeToSyncState,syncNow,getPendingChanges,discardPendingChanges,clearOfflineData; eventssyncStateChanged,changesSynced,changesRejected.- Cells not yet saved and conflicts are marked; the status bar shows the sync state and announces changes of it.
Changed — every cell write is prepared one way
A cell write is prepared the same way from every surface (a transaction, an edit, a paste, a board or calendar gesture): per record, each write through its column's value setter (awaited when async), then the cell and row rules once against the whole proposed record. See How a cell write is prepared.
- Rules see every field a write changes together; a dotted field (
plan.start) is written and checked by path, and the record's previous version keeps its nested values. - Every field a value setter sets is written, from an edit as from a transaction.
- Only what a value setter changed is written: a nested field it sets beside a plain write to the same object is kept, a field it left alone is not rewritten (so undoing the edit no longer puts back a value someone else changed since), and a date it changes on a refused write leaves the record as it was.
- A write is prepared against the record as it commits: async writes queued before a sync transaction land first, and an edit whose record changed while it was saving is prepared again (or refused as changed while saving).
- A field a value setter changes besides its own is checked by that field's cell rules before the write commits.
- A row rule that throws reports an issue, as a cell rule does; a blocking one blocks the write (it let it through).
- A refused write reaches the app the same way from every surface.
writeBlockednow also fires for a workspace view's gesture (source: 'view': a card moved, an entry or a Gantt bar dragged) and when someone tries to edit a cell they may not (capabilityDenied; it was silent). - In a workspace, a refused edit, paste or fill is told the user as a refused gesture is (
Not changed: …), and a capability's own reason is shown instead of a generic “read-only”. - A workspace gesture whose value setter or rule is async runs it once (it ran twice: once to find it async, again to await it), and is reported when it settles, not as done before it was.
- A
transaction(…, { async: true })still waiting when the grid is destroyed settles asrejected(“The grid closed before this change was saved.”) instead of never settling. - A nested field (
plan.amount) reads as the table shows it in charts, in formulas that use it, in diffs, in a pivot's field sampling and in its value picker (each readrow["plan.amount"]and got nothing). - New page, Core Guarantees: what every surface promises about your records, reads, writes, refusals and history, and the limits stated plainly. A core test keeps the built-in views to it.
transactiongainspolicy('atomic'|'bestEffort') andasUser. A move on a board, a span on a calendar or timeline is all or nothing; bulk actions apply to the records they can.- A record that changes while an async write waits is prepared again, then the write is rejected if it changed again.
- Views read a field as the table does: a nested field (
client.name) as a role, a formula's computed value, a getter's cached value.RecordReadertakes how to read a field;readFieldByPathis the default (headless). - The views show every record matching the filters and search, whatever the table does with them: collapsing its groups, paging it or pivoting it no longer removes records from the board, calendar or gallery. With a server or infinite model the views count the records loaded so far, and say so.
- The record inspector edits a column with a React
cellEditorin place (it sent you to the table).
Added — translate the grid
Every word the grid shows now comes from one typed table of English defaults, by area (menus, filters, the
sidebar, views, editors, the status bar, offline sync). The messages option (<Grid messages>, or any
create*Grid) lays your words over it; text with values in it is a function, so a language orders its own words.
DEFAULT_MESSAGES is the whole table and api.getMessages() the one in use. See
Translating the grid.
- Offline's sync words are the grid's
messages.sync(theoffline({ messages })option is gone). - The filter operators, chips and relative dates follow the grid's words;
getOpsForType,getOpMeta,summarizeFiltertake the words as an optional last argument (English by default). - The English table costs about 11 KB gzip in the core; the workspace views' words (another ~7 KB) load with the views, so a table-only grid does not ship them.
Changed — one list of conflicts
Conflicts from every source (a live stream, offline sync) are one list on the grid, drawn on their cells and shown
in the data-integrity panel, whether or not data integrity is configured: api.getConflicts(source?),
api.resolveConflict(id, 'local' | 'remote' | { value }), api.clearConflict(id). Resolving to the side the cell
shows keeps it; resolving to the other side writes it as a local edit, validated and undoable like any edit.
api.transaction({ origin: 'server' })applies the server's data: out of the undo history, never queued.- A grid that does not import offline ships none of it (the core grows by under 1 KB). Measured in Chrome with real IndexedDB: a live feed costs the same with offline on (transaction p95 1.2 ms either way); opening 100,000 stored rows takes ~400 ms; 50,000 server changes apply without a long task.
Changed — the grid keeps what it shows up to date instead of rebuilding it
The rows on display, and everything derived from them, are now kept up to date as records change, rather than derived again from every record: sorted, filtered, grouped and tree-data grids, aggregates and totals, pivots, pagination. A change costs work in proportion to the rows it touches. Measured on one machine (live feeds of 100 changed records per tick, transaction p50 before → after):
| Feed | Before | After |
|---|---|---|
| Grouped grid, rows added and removed | 108.6 ms | 1.2 ms |
| Tree data | 97.9 ms | 1.4 ms |
| One edit in a sorted 1,000,000-row grid | 31.7 ms | 0.3 ms |
| Pivot (100,000 records) | 101.4 ms | 1.3 ms |
Cells are no longer prepared again when rows only move or arrive, and a deep tree (tens of thousands of levels) builds without overflowing the stack.
Changed — React: a new rows array costs what changed
Handing the grid a new rows array (setRows, or the React rows prop) now costs one comparison per row passed
as the same object, plus work for the rows that differ: an edited row, an added or removed one. Rows added and
removed are placed without rebuilding the source order. Measured on one machine, a React app re-rendering with a
new rows array every tick (p50 per tick, render and grid commit included, before → after):
| Feed | Before | After |
|---|---|---|
| 10 rows changed of 100,000 | 7.7 ms | 2.0 ms |
| 300 rows added and 300 removed | 37.5 ms | 4.7 ms |
| One row changed of 1,000,000 | 130.4 ms | 7.5 ms |
The two larger feeds dropped about a quarter of their frames before, and under 1% after.
<Grid> also applies its live props (columns, styleRules, view, rowAnimation, showMinimap, …) in one
place, before the browser paints, and only when their content changed: a parent that re-renders with the same
props written inline no longer repaints the grid.
Fixed — moving a pivot's columns
Dragging a pivot's generated column (or moving one from the header menu or the columns panel) removed the record fields the pivot hides from the grid: drill-down then listed its records with empty cells, and the pivot put the columns back in key order on the next change. A move now reorders only the columns on display, and a pivot keeps the order its columns were moved into until its layout changes.
Changed — workspace views update in place
The calendar, Kanban board, gallery, chart, Gantt chart and pivot view keep their own layout and move only the records that changed. A table covered by a view paints nothing until it shows again. At 50,000 records under a live feed, a tick that took 83–143 ms now takes 4–15 ms (p50).
Changed — one changed event per commit
Every commit that changes data raises one changed event: rows added, removed and updated (with each field's old
and new value), computed cells that may have changed, and the projection inputs that changed (sort, filter,
grouping, …), with where the change came from (origin) and how (source). It replaces cellValueChanged and
rowsUpdated; React's onCellValueChanged becomes onChanged. Rows a server-side or infinite row model loads
raise it too (origin and source 'server'). Undo reverts exactly what the commit changed.
Added — conditional formatting
Style rules colorScale, dataBar and iconSet format numbers by where they sit in their column's range over the
displayed rows (or a fixed min / max), repaint as live data moves the range, stack on one field, and export to
Excel as native conditional formats. See Conditional Styling.
Added — presence
api.setPresence(peers) draws teammates' cell cursors with name tags: they glide between cells, follow their row
through sorts and filters, stay on pinned columns and rows, and pulse while editing. api.flashCells(cells) flashes
cells. See Presence.
Added — timeline column
timelineColumnType (built-in type: 'timeline') draws a date range as a bar on a time scale shared by the column;
drag to move, drag an end to resize. See Columns.
Added — gallery and calendar views
view (prop), api.setView(config) and initialState.view show the displayed rows as virtualized cards — fields
drawn by their columns' own renderers — or on a month calendar by a date or range field, with drag to reschedule.
onViewChange reports the grid returning to the table. See Gallery & Calendar Views.
Added — minimap
showMinimap docks a strip beside the vertical scrollbar marking the view window, selection, recent edits,
validation issues and your own marks (api.setMinimapMarks); click or drag to scroll. See Minimap.
Changed — option columns sort by option order
Select, combobox and segmented columns sort in the order their options are listed (sortBy: 'label' for
alphabetical by label) instead of by the stored value. New ColumnDef.sortValue chooses what any column sorts by,
applied in every client sort path. See Columns.
Added — getVisibleRowRange on the public API
Next to getVisibleColumnRange.
Added — checkbox selection across groups
With rowSelection: 'multiple', group rows get a tri-state checkbox in the selection column: it selects or deselects
every order beneath the group, nested and collapsed groups included, and shows a dash while the group is partly
selected. Ancestor groups update as their rows are selected. GroupRenderContext.selection ('all', 'some',
'none') lets a custom group renderer draw the same state.
Fixed — editing grouped, aggregated grids
-
Adding or removing a row in a grouped grid re-ran the whole pipeline (130–195 ms at 200k rows). A row added to an existing group, or removed from a group that stays non-empty, is now patched in place (about 15–50 ms). Without a sort, an add must land after its group's first row and a remove must not be a group's first row, since those could reorder groups; anything else, and any add that creates a group, still rebuilds.
-
Each quick-filter keystroke re-filtered and rebuilt every row (130–330 ms at 200k rows). Typing now narrows from the previous matches, recent results are cached (backspace is instant), a keystroke that matches the same rows rebuilds nothing, and clearing the box reuses the unfiltered rows: typically 15–25 ms, 0–3 ms when cached. The first keystroke after loading still builds each row's search text once.
-
Expanding or collapsing a group, opening a detail row or changing pages re-ran filtering, grouping, sorting and aggregation over every row. Opening or closing one group now replaces only that group's rows (about 5 ms on 200k grouped rows, was 100–160 ms); expand/collapse all, detail rows and page changes reuse the row tree and only re-flatten.
-
Editing a cell in a column that feeds a group aggregate re-ran the whole row pipeline (about 200 ms at 200k rows) and threw away the incremental index. Cell edits, cell transactions and undo/redo now patch the group totals incrementally, like row updates (about 3 ms).
-
Editing a field that a computed (
valueGetter) aggregate column reads left the group totals stale. DeclarevalueGetterDependencieson the computed column to keep such edits incremental; without them, any edit in a grid with that aggregate re-aggregates in full, since the getter could read any field.
Fixed — checkbox selection
- Ctrl/Cmd+click toggled a row twice, selecting and immediately deselecting it. It now toggles once and leaves the cell cursor where it is.
- A checkbox in a recycled row could show the previous row's checked state and, if clicked before scrolling settled, toggle that hidden row. Checkboxes are now correct on every scroll frame, and a click acts on the row the cell shows.
- Group rows in the selection column kept a leftover row checkbox from a recycled slot.
hierarchyColumn.rendererwas also drawn on a grouped grid's leaf rows. Leaf rows now keep the built-in cell, which also keeps React work during scroll to group rows.- A React group renderer could show a blank cell while scrolling: a deferred first mount now shows the group label until it mounts, and stuck sticky headers mount immediately.
Added — row animation styles
rowAnimation (React prop; rendererOptions.rowAnimation and api.setRowAnimation in core) chooses how rows
move on sorts, live reorders, expansion and detail rows: style ('slide', 'fade', 'none'), easing
('smooth', 'snappy', 'spring' or any CSS easing), duration, a capped stagger cascade, and on to switch
kinds of change off. See Styling.
Added — sticky group headers, native, with options
Expanded group headers stick under the column header while their rows scroll, stack one per level, and are pushed up when their group ends. They use the browser's native sticky positioning, so a stuck header moves with the scroll on the GPU, with no JavaScript per frame. grouping.stickyHeaders takes true or { levels, shadow } to cap how many levels stick or drop the shadow. A stuck header carries the class og-row-group-stuck. See Sticky group headers.
Changed — pinned rows stay put natively
Rows pinned to the top or bottom (pins: { top, bottom }, pinTopRows, pinBottomRows) no longer move with JavaScript while scrolling. They stay in place on their own, so they no longer lag behind fast scrolls.
Fixed — the selection box scrolls with the cells
The range selection box now moves with the cells it covers while you scroll.
Changed — one group renderer API across display modes
Full-width group rows, the hierarchy cell and sticky headers are all drawn by the same renderer, with the same GroupRenderContext: level, expanded, label, count, aggregates, isStuck, and actions toggle(), setExpanded(), expandAll(), collapseAll() and selectChildren(). React gets <GroupToggle> and <GroupCount>; DOM renderers get renderGroupToggle(ctx). See One context for every group renderer.
Breaking: grouping.rowRenderer now receives a GroupRenderContext, and React group row components (groupRowRenderer, totalRowRenderer) receive that context as props instead of { visualRow, api }. Read ctx.row for the row and ctx.api for the API.
Fixed — column order is kept when the columns prop is replaced with equivalent columns
Passing a new columns array with the same columns no longer undoes a reorder the user made.
Changed — every cell is final in every frame, scrolling or not
The renderer prepares each cell's content (value, formatter, readonly, decorations, tooltip) once per row version and draws every cell from it, in scroll frames and paint frames alike. React and DOM renderer cells show their real renderer during scroll, computed columns show their value, style rules and decorations apply, and data changes are painted while the grid scrolls. Nothing is repaired when the scroll ends. In the browser bench, React cells went from about 80% of visible cells wrong per scroll frame to none, and settle time after a scroll from up to 470 ms to about 10 ms. See Renderer Capabilities.
Breaking: capabilities on renderers (scroll, scrollText, scrollState), rendererOptions.live,
rendererOptions.fullWidth, the CellRendererCapabilities and CellScrollPresentation types, the
'scroll-live' / 'scroll-force-live' / 'scroll-idle' renderer phases and the scroll-approximation render
counters are removed, with no deprecation period. Delete them; nothing replaces them.
Changed — two scroll choices for custom renderers: text and live (superseded above)
A custom renderer now picks one of two behaviors during scroll with capabilities.scroll. 'text' (the React
default) shows stand-in text for cells entering view and mounts the renderer once scrolling settles. 'live' (the
DOM default) mounts and updates the renderer during scroll within a per-frame budget; a cell over budget shows
stand-in text for that frame and is completed right after, so it is never blank. scrollText sets the stand-in
text. Grid-level tuning moves to one option, rendererOptions.live. HTML snapshots are removed. See
Renderer Capabilities.
Breaking: the old options are removed, with no deprecation period.
| Removed | Use instead |
|---|---|
scrollPresentation: 'freeze' | scroll: 'text' (the React default, so usually delete it) |
scrollPresentation: 'update' | scroll: 'live' (the DOM default, so usually delete it) |
scrollPresentation: 'live' | scroll: 'live' |
scrollPresentation: 'text-impostor' + textImpostor.render | scroll: 'text' + scrollText |
scrollPresentation: 'html-snapshot' + htmlSnapshot | scroll: 'text'; HTML snapshots no longer exist |
live.update: 'imperative' | renderer kind: 'imperativeReact' |
live.priority, live.allowEmergencyShell | none; over-budget cells always show stand-in text |
rendererOptions.domUpdate.maxMsPerFrame | rendererOptions.live.maxMsPerFrame |
rendererOptions.liveReact (overscan, mount and update limits) | rendererOptions.live (same names) |
rendererOptions.htmlSnapshot, rendererOptions.textImpostor | none |
render counters htmlSnapshotHitsDuringScroll, htmlSnapshotMissesDuringScroll, textImpostorUsesDuringScroll | none |
Changed — one data-write API: transaction
api.transaction({ rows, cells }) writes rows and cells together as one atomic change with one undo entry, and
api.transaction(tx, { async: true }) queues it with other high-frequency writes and returns a promise.
setRows now diffs against the current rows, so only changed rows update. Row-level writes re-validate changed
and added rows after they commit, and removed rows drop their issues (validation.validateOnTransaction). See
Transactions.
Breaking: the old write methods are removed, with no deprecation period.
| Removed | Use instead |
|---|---|
applyTransaction(tx) | transaction({ rows: tx }); the row nodes are at result.rows |
applyTransactionAsync(tx, callback) | transaction({ rows: tx }, { async: true }).then(callback) |
flushAsyncTransactions() | flushTransactions() |
batchCellValues(updates, source) | transaction({ cells: updates, source }) |
batchCellValuesAsync(updates, source) | transaction({ cells: updates, source }, { async: true }) |
setCellValueAsync(rowId, field, value) | transaction({ cells: [{ rowId, colField: field, value }] }, { async: true }) |
updateRows(fn) | transaction({ rows: { update: changedRows } }) |
transaction({ rowTransaction }) | transaction({ rows }) |
transaction({ rows: allRows }) | setRows(allRows) |
type BatchCellValueUpdate | GridCellWrite |
Added — DOM cell renderers receive the grid api
mount() and update() get api, the same object React cell renderers receive, so DOM cells can select cells
and write values.
Changed — built-in number, date and checkbox cells render without React
The built-in column types use DOM renderers with the same markup and styles. The React components are still exported.
Changed — text cells are lighter, and long values end in an ellipsis
Cell text is laid out as a block, so text-overflow: ellipsis now applies (long values were cut off without one).
Cells of columns without a custom renderer hold their text directly, one element per cell, which cuts the layout
work of vertical scrolling by about a third. See Migration Notes if your CSS targets
.og-cell-content.
Changed — custom renderer cells have one box fewer
The portal host inside a custom-renderer cell is now display: contents: the renderer's container lays out directly
in the cell. This cut layout time for grids with custom DOM renderers by about 12%. See
Migration Notes if your CSS styles .og-cell-portal-host.
Fixed — rendering limits no longer hide visible rows or columns
When runtimeLimits bound the rendering window, rows at the bottom and columns on the right of the viewport could
be left blank. The limit now trims overscan and keeps everything in view.
Fixed — scrolling in steps kept restarting the scroll session
Each discrete scroll — an instant wheel tick, a scripted scrollTop assignment — fires its own scrollend, and the
grid ended its scroll session on each one: the scroll-end work re-ran between steps, every row was restyled twice
per step, and overscanAdaptive never took effect. The session now ends once 50 ms pass without movement after the
browser reports the end.
Fixed — header indicators restyled on every horizontal scroll frame
The sort indicator's inline style was rewritten (to the same value) for every visible header cell on every frame of a horizontal scroll. Header indicators are now written only when they change.
Fixed — runtimeLimitsClamped render counter
It was counted but always reported as 0.
Cutting a release
Before running pnpm --filter wit-grid-site docs:release:from-package (see Docs Architecture),
turn this page's next (unreleased) heading into ## <version> and start a fresh, empty next (unreleased) section —
the same rhythm as Migration Notes.
Last updated on