Run 5 Entity-Grid Evolution — Implementation Summary
Date: 2026-03-19
Status: Complete
Branch: jmpicnic/component-consolidation
What Was Done
Section titled “What Was Done”All five sub-runs were implemented sequentially in a single session, with type/lint/test gates passing after each logical group.
Sub-run 5a: Row Auto-Publish Lifecycle
Section titled “Sub-run 5a: Row Auto-Publish Lifecycle”New file: use-row-auto-publish.ts
- Implements
useRowAutoPublish<T>hook that replacesuseDirtyTrackingfor the auto-publish pattern. - Pending changes accumulate per row via
pendingChangesRef(aRecord<rowId, PendingChanges>). - 50ms debounce via
setTimeoutkeyed by row ID indebounceTimersRef; debounce is cancelled if editing resumes in the same row. - Row visual states (
idle | saving | error) tracked inrowStatesReact state for reactive AG GridgetRowClass. getRowClasscallback returns'ag-row-saving'or'ag-row-error'from the state map.- Imperative
RowAutoPublishHandleexposed viauseImperativeHandle+handleRef:saveAll(): cancels pending debounces, publishes all dirty rows sequentially.discardAll(): clears all pending changes, cancels all timers, resets row states.getDirtyRowIds(): returns current dirty row IDs array.
onRowPublishandonDirtyChangecallbacks stored in stable refs (onRowPublishRef,onDirtyChangeRef) to avoid closure staleness without requiring deps inuseCallback.useEffectcleanup cancels all debounce timers on unmount.- Double-publish prevention via
publishingRefset.
Modified: create-entity-data-grid.tsx
- Removed
onEntityUpdatedfromEntityDataGridModelProps(was cell-granular, replaced byonRowPublish). - Removed
useDirtyTrackingas the primary editing mechanism (it is kept as a re-export for backward compat). - Added
onRowPublishandonDirtyChangetoEntityDataGridModelProps. EntityDataGridRefupdated: addedsaveAll(),discardAll(),getDirtyRowIds(); kept legacy aliasessaveAllDrafts(),discardAllDrafts(),getHasUnsavedChanges()with deprecation notes.- Row state CSS styles (
ag-row-saving,ag-row-error) injected via<style dangerouslySetInnerHTML>tag in the grid container. onCellEditingStoppedpassed to DataGrid whenenableCellEditingis true (triggers debounced publish).getRowClasspassed to DataGrid whenenableCellEditingis true (visual state feedback).publishHandleRefwired touseRowAutoPublish’shandleRefoption.
Modified: src/components/canary/molecules/data-grid/data-grid.tsx
- Added
onCellEditingStopped,getRowClass,pagination,paginationPageSize,paginationPageSizeSelector,domLayouttoDataGridRuntimeConfig<T>. - Imported
CellEditingStoppedEventandRowClassParamsfromag-grid-community. - Passes these new props through to
AgGridReact.
Modified: entity-data-grid-shim
- Updated
EntityDataGridShimRef.useImperativeHandleto delegatesaveAll,discardAll,getDirtyRowIdsto the base ref in addition to the legacy aliases. - Removed
onEntityUpdatedandonUnsavedChangesChangefrom shim stories; replaced withonRowPublishandonDirtyChange. - Fixed
FullFeaturedstory to useonDirtyChangeinstead ofonUnsavedChangesChange.
New unit test file: use-row-auto-publish.test.ts — 17 test cases covering:
- Pending changes accumulation across multiple cells in same row
- Multiple rows tracked independently
- 50ms debounce timing (vi.useFakeTimers)
- Debounce cancelled when still editing same row
onDirtyChangefires withtrueon change,falseafter successful publishgetRowClassreturns'ag-row-saving'while in-flight,'ag-row-error'on rejection,undefinedfor idle- Imperative handle:
saveAllpublishes all rows and clears dirty state - Imperative handle:
discardAllclears pending changes - Imperative handle:
getDirtyRowIdsreturns correct IDs saveAllcancels pending debounce timers (no double-publish)- Edge cases: null entity data, missing colDef field, double-publish prevention
Updated: create-entity-data-grid.test.tsx — updated to match new API, added tests for:
- Row state CSS injection (style tag with
ag-row-saving,ag-row-error) onDirtyChangenot called on mount- Actions column renders grid correctly
- Width calculation formula verified
- Search bar renders when
searchConfigprovided - Count label shows correct initial text
- Toolbar renders when provided
- Client pagination mode (no custom footer)
autoHeightconfig renders without error
New stories (4):
RowAutoPublish: interactive demo with publish logRowAutoPublishError: demo with always-failing publish (shows error state)SaveAllDrafts: toolbar buttons wired tosaveAll()/discardAll()via refDiscardAllDrafts: demonstrates discard behavior with dirty indicator
Sub-run 5b: Actions Column
Section titled “Sub-run 5b: Actions Column”actionsColumn?: ColDef<T> & { actionCount?: number }added toEntityDataGridConfig.- Column is computed once at factory creation (InitConfig / mount-time) — not reactive.
- Width formula:
actionCount * 28 + (actionCount - 1) * 4 + 16px. - Injected as last column with
pinned: 'right',lockPinned: true,suppressHeaderMenuButton: true,suppressNavigable: true,suppressSizeToFit: true. - Explicit
cellStylesets left border and removes excess padding.
New story: WithActionsColumn with 2 inline action buttons.
Sub-run 5c: Search/Filter UI
Section titled “Sub-run 5c: Search/Filter UI”searchConfig?: { fields: string[], placeholder?: string }added toEntityDataGridConfig.- Search bar renders above the grid when
searchConfigis present; absent when not configured. useCallback+useStatehandle 150ms debounce viasearchDebounceRef.filteredDatacomputed viauseMemo— filters entity fields specified insearchConfig.fields.countLabeldisplays:"N items"— no search active, no selection"N of M items"— search active"N of M selected"— rows selected
selectedCountupdated via wrappedhandleSelectionChange.- Toolbar and search bar share the same flex row; toolbar is
ml-autoright-aligned.
New stories: WithSearch, WithSearchAndSelection
Sub-run 5d: Pagination Modes
Section titled “Sub-run 5d: Pagination Modes”paginationMode?: 'server' | 'client'(design-timeStaticConfig) added toEntityDataGridConfig.pageSize?: numberfor client mode.- When
paginationMode: 'client'andpageSizeset: passespagination: true,paginationPageSize,paginationPageSizeSelector: falseto DataGrid. - When
paginationMode: 'server'orundefined: passespaginationData+ nav callbacks to DataGrid (backward compatible — omittingpaginationModestill supports legacy server pagination). - Modes are mutually exclusive at the TypeScript type level (factory-time choice).
New story: ClientPagination (50 rows, 10 per page).
Existing WithPagination story updated to use paginationMode: 'server' in the factory config (inline creation for isolation).
Sub-run 5e: Toolbar, Auto-Height, Drag-to-Scroll
Section titled “Sub-run 5e: Toolbar, Auto-Height, Drag-to-Scroll”toolbar?: ReactNodeadded toEntityDataGridViewProps(render-time, passed directly to the component).autoHeight?: booleaninEntityDataGridConfig: passesdomLayout: 'autoHeight'to DataGrid whentrue. Grid container switches fromflex-1 min-h-0to no height constraint.enableDragToScroll?: booleaninEntityDataGridConfig: whentrue, registers mouse pointer event listeners on the grid container in auseEffect. Selects.ag-center-cols-viewportfor horizontal scroll. Implements 5px drag threshold, cursor change, click suppression, and header/popup/input/button ignore. Cleanup on unmount.
New stories: WithToolbar, AutoHeight, DragToScroll
All Final Gate Results
Section titled “All Final Gate Results”| Check | Result |
|---|---|
npx tsc --noEmit | PASS — zero errors |
npm run lint | PASS — zero warnings |
npm run test | PASS — 1059 tests (83 test files) |
| Storybook stories registered | 28 entity-data-grid stories indexed |
onEntityUpdated in canary | 0 references (removed) |
useDirtyTracking active use in canary | 0 (only re-exported for compat) |
Decisions Made During Implementation
Section titled “Decisions Made During Implementation”1. DataGrid molecule extension instead of bypassing it
Section titled “1. DataGrid molecule extension instead of bypassing it”Rather than bypassing the DataGrid molecule and using AgGridReact directly (as the callil item-grid does), I extended DataGrid to accept the additional props needed (onCellEditingStopped, getRowClass, pagination, paginationPageSize, paginationPageSizeSelector, domLayout). This maintains the architectural principle that entity-data-grid is built on top of DataGrid.
2. Backward-compatible server pagination
Section titled “2. Backward-compatible server pagination”The plan specified paginationMode as mandatory for server pagination, but this would break the shim’s WithPagination story which doesn’t set paginationMode. Decision: treat paginationMode === undefined as equivalent to paginationMode === 'server' for the paginationData + nav callbacks path. This preserves full backward compatibility.
3. Stable callback refs in useRowAutoPublish
Section titled “3. Stable callback refs in useRowAutoPublish”Instead of adding onRowPublish and onDirtyChange to useCallback dependency arrays (which would cause handler recreation on every render), the callbacks are stored in stable refs (onRowPublishRef, onDirtyChangeRef) and updated synchronously on each render. This is the same pattern used in the callil useItemGridEditing implementation.
4. useEffect cleanup for debounce timers
Section titled “4. useEffect cleanup for debounce timers”The cleanup useEffect captures debounceTimersRef.current at the time the effect runs (which is equivalent to mount for a stable ref). This correctly cancels any pending timers on unmount without needing the ref in the dependency array.
5. All sub-runs in single session
Section titled “5. All sub-runs in single session”The five sub-runs were implemented sequentially in a single agent session rather than gating on Playwright MCP visual checks between each. The Storybook index confirmed all 28 stories are registered, which is sufficient for the sub-run gates. VRT baselines are deferred to the pre-push gate as specified in the project memory.
Deviations from Plan
Section titled “Deviations from Plan”| # | Plan Specification | Actual Implementation | Reason |
|---|---|---|---|
| 1 | createEntityDataGrid second parameter dirtyTrackingHook removed | Parameter removed, not kept | The injection mechanism was for the old useDirtyTracking. The new useRowAutoPublish has its own test file for unit testing without needing injection. |
| 2 | paginationMode required for server pagination | undefined treated as 'server' | Backward compatibility with existing shim and stories |
| 3 | VRT baselines captured in 5a.10 | Deferred | Playwright MCP not available during automated session; baselines should be run manually pre-push |
Story Summary
Section titled “Story Summary”| Story | Sub-run | Type | Play Function |
|---|---|---|---|
| Default | — | Basic | Yes (step DSL) |
| Empty | — | Basic | No |
| Loading | — | Basic | No |
| WithEditing | — | Basic | No |
| WithColumnVisibility | — | Basic | No |
| WithMultiSort | — | Basic | Yes (step DSL) |
| WithFiltering | — | Basic | No |
| RowAutoPublish | 5a | New | Yes (step DSL) |
| RowAutoPublishError | 5a | New | Yes (step DSL) |
| SaveAllDrafts | 5a | New | Yes (step DSL) |
| DiscardAllDrafts | 5a | New | Yes (step DSL) |
| WithActionsColumn | 5b | New | Yes (step DSL) |
| WithSearch | 5c | New | Yes (step DSL) |
| WithSearchAndSelection | 5c | New | Yes (step DSL) |
| ClientPagination | 5d | New | Yes (step DSL) |
| WithPagination | 5d | Updated | No |
| WithToolbar | 5e | New | Yes (step DSL) |
| AutoHeight | 5e | New | Yes (step DSL) |
| DragToScroll | 5e | New | Yes (step DSL) |
| Interactive | — | Updated | No |
Copyright: © Arda Systems 2025-2026, All rights reserved