13 KiB
Property Rename Not Reflecting Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make property (column) rename reflect immediately in the grid header and the hide-fields popover, both for the editing user and other clients in the same base room — without a tab reload.
Architecture: Three small frontend changes. The server path is already correct (rename persists, emits base:property:updated ws event, and useBaseSocket invalidates ["bases", baseId]). The cache updates too. The bug is purely client-side memoization: GridHeader, GridHeaderCell, and ViewFieldVisibility memo on [table] / their prop object — and the table reference returned by useReactTable is STABLE across renders. So when properties change under the hood (new column defs, new meta.property objects, new header strings), memo'd consumers never re-render. Fix: thread properties / property down as explicit props so shallow-compare catches the change.
Tech Stack: React 18, TanStack Table v8, TanStack Query v5.
Background — the trace that explains the bug
- User renames property "Email" → "Mail" from the header's property menu.
updatePropertyMutation.mutatefires. Server persists, returns{property: {...name: "Mail"}}.onSuccessinbase-property-query.ts:52-65callsqueryClient.setQueryData(["bases", baseId], old => ({...old, properties: old.properties.map(p => p.id === result.property.id ? result.property : p)})). The renamed property is a new object reference; the rest of the array reuses old refs.useBaseQueryreturns a newIBaseobject with a newpropertiesarray.- In
use-base-table.ts,properties = useMemo(() => base?.properties ?? [], [base?.properties])picks up the new array.columns = useMemo(() => buildColumns(properties), [properties])rebuilds all column defs, including newmeta.propertyobjects and newheader: property.namevalues. useReactTable({columns, ...})receives the new columns array. Internally TanStack Table updates its column state.- The
tableinstance returned byuseReactTableis the SAME reference it was before — it's memoized for stability. <GridHeader table={table} columnOrder={...} />is wrapped inReact.memo. Its props:table(stable),columnOrder(unchanged — rename doesn't reorder),loadedRowIds(unchanged). Memo shallow-compare says "no change" → no re-render.- Even if
GridHeaderdid re-render, each<GridHeaderCell key={header.id} header={header} />is alsomemo'd.headeris reused by TanStack Table across renders for the same column id, so same ref → memo skips → no re-render. view-field-visibility.tsx:27-31:const columns = useMemo(() => table.getAllLeafColumns().filter(...), [table]).tableis stable → memo never invalidates → shows stale names.
The rename only becomes visible after a full mount (tab reload), which recomputes everything from scratch.
Files
Modified:
apps/client/src/features/base/components/grid/grid-header.tsx— acceptproperties: IBaseProperty[]prop; passproperty={...}to eachGridHeaderCell. Memo picks up the change.apps/client/src/features/base/components/grid/grid-header-cell.tsx— acceptpropertyas an explicit prop instead of deriving fromheader.column.columnDef.meta?.property. Use it for header rendering (and anywhere else in this file that currently reads it through the meta).apps/client/src/features/base/components/grid/grid-container.tsx— acceptpropertiesprop; pass to<GridHeader>.apps/client/src/features/base/components/base-table.tsx— passbase?.propertiesto<GridContainer>.apps/client/src/features/base/components/base-toolbar.tsx— passproperties={base.properties}to<ViewFieldVisibility>.apps/client/src/features/base/components/views/view-field-visibility.tsx— acceptpropertiesprop; include it in theuseMemo([table, properties])dep list.
No new files. No server changes. No new deps.
Task 1: GridHeaderCell — accept property as a prop
Files:
-
Modify:
apps/client/src/features/base/components/grid/grid-header-cell.tsx -
Step 1: Add
propertytoGridHeaderCellProps
type GridHeaderCellProps = {
header: Header<IBaseRow, unknown>;
property: IBaseProperty | undefined;
loadedRowIds: string[];
};
- Step 2: Replace the internal property derivation with the prop
Find the line near the top of the component:
const property = header.column.columnDef.meta?.property as
| IBaseProperty
| undefined;
Remove it. Add property to the function parameter destructuring:
export const GridHeaderCell = memo(function GridHeaderCell({
header,
property,
loadedRowIds,
}: GridHeaderCellProps) {
Everything else in the file continues to reference the same property variable, now a prop. No further changes needed in this file.
- Step 3: Build
pnpm nx run client:build
Build will FAIL because GridHeader doesn't yet pass property. That's fine — fixed in the next task. Do not commit yet.
Task 2: GridHeader — thread properties / property through
Files:
-
Modify:
apps/client/src/features/base/components/grid/grid-header.tsx -
Step 1: Add
propertiesprop and use it to look up per-cell property
Before:
type GridHeaderProps = {
table: Table<IBaseRow>;
baseId?: string;
// Passed explicitly to break memo when columns change
// (table ref is stable from useReactTable, so memo won't fire without this)
columnOrder: ColumnOrderState;
loadedRowIds: string[];
onPropertyCreated?: () => void;
};
export const GridHeader = memo(function GridHeader({
table,
baseId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
columnOrder: _columnOrder,
loadedRowIds,
onPropertyCreated,
}: GridHeaderProps) {
const headerGroups = table.getHeaderGroups();
return (
<div className={classes.headerRow} role="row">
{headerGroups[0]?.headers.map((header) => (
<GridHeaderCell key={header.id} header={header} loadedRowIds={loadedRowIds} />
))}
...
After:
import { IBaseProperty, IBaseRow } from "@/features/base/types/base.types";
type GridHeaderProps = {
table: Table<IBaseRow>;
baseId?: string;
// Passed explicitly to break memo when columns change
// (table ref is stable from useReactTable, so memo won't fire without these)
columnOrder: ColumnOrderState;
properties: IBaseProperty[];
loadedRowIds: string[];
onPropertyCreated?: () => void;
};
export const GridHeader = memo(function GridHeader({
table,
baseId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
columnOrder: _columnOrder,
properties,
loadedRowIds,
onPropertyCreated,
}: GridHeaderProps) {
const headerGroups = table.getHeaderGroups();
const propertyById = useMemo(() => {
const map = new Map<string, IBaseProperty>();
for (const p of properties) map.set(p.id, p);
return map;
}, [properties]);
return (
<div className={classes.headerRow} role="row">
{headerGroups[0]?.headers.map((header) => (
<GridHeaderCell
key={header.id}
header={header}
property={propertyById.get(header.column.id)}
loadedRowIds={loadedRowIds}
/>
))}
...
Make sure useMemo is imported from react (it already imports memo; add useMemo alongside).
- Step 2: Build
pnpm nx run client:build
Still expected to fail — GridContainer doesn't yet pass properties. Next task.
Task 3: GridContainer — accept and forward properties
Files:
-
Modify:
apps/client/src/features/base/components/grid/grid-container.tsx -
Step 1: Add
propertiesto props
Near the existing GridContainerProps type (look near the top of the file), add:
properties: IBaseProperty[];
Add IBaseProperty to the existing @/features/base/types/base.types import at the top of the file if it's not already imported.
Destructure it in the component signature.
- Step 2: Pass it to
<GridHeader>
Find the <GridHeader ... /> JSX at roughly line 239-245 and add:
<GridHeader
table={table}
baseId={baseId}
columnOrder={table.getState().columnOrder}
properties={properties}
loadedRowIds={rowIds}
onPropertyCreated={handlePropertyCreated}
/>
- Step 3: Build
Still expected to fail — BaseTable doesn't yet pass properties to GridContainer. Next task.
Task 4: BaseTable — pass base.properties to GridContainer
Files:
-
Modify:
apps/client/src/features/base/components/base-table.tsx -
Step 1: Add the prop
Find the <GridContainer ... /> JSX at line 187. Add:
<GridContainer
table={table}
properties={base.properties}
onCellUpdate={handleCellUpdate}
...
/>
base is already guaranteed non-null at this point — line 174 has if (!base) return null;.
- Step 2: Build
pnpm nx run client:build
Should succeed now — grid path is complete.
- Step 3: Commit the grid-side changes as one unit
git add \
apps/client/src/features/base/components/grid/grid-header-cell.tsx \
apps/client/src/features/base/components/grid/grid-header.tsx \
apps/client/src/features/base/components/grid/grid-container.tsx \
apps/client/src/features/base/components/base-table.tsx
git commit -m "fix(base): refresh grid headers when a property is renamed"
The four files have to land together or the build is broken — one commit.
Task 5: ViewFieldVisibility — accept properties and include it in the memo
Files:
-
Modify:
apps/client/src/features/base/components/views/view-field-visibility.tsx -
Step 1: Add
propertiesto the props type
type ViewFieldVisibilityProps = {
opened: boolean;
onClose: () => void;
table: Table<IBaseRow>;
properties: IBaseProperty[];
onPersist: () => void;
children: React.ReactNode;
};
- Step 2: Add
propertiesto theuseMemodep list
Change:
const columns = useMemo(() => {
return table
.getAllLeafColumns()
.filter((col) => col.id !== "__row_number");
}, [table]);
To:
const columns = useMemo(() => {
return table
.getAllLeafColumns()
.filter((col) => col.id !== "__row_number");
}, [table, properties]);
We still derive columns from table (that's where col.getIsVisible() / col.getCanHide() live), but properties is added as a dep so the memo invalidates whenever properties change — forcing a re-read of table.getAllLeafColumns() which by then reflects the renamed metadata.
Also destructure properties in the function signature.
- Step 3: Build
Expected to fail until the toolbar passes properties.
Task 6: BaseToolbar — pass base.properties to ViewFieldVisibility
Files:
-
Modify:
apps/client/src/features/base/components/base-toolbar.tsx -
Step 1: Pass the prop
Find <ViewFieldVisibility ... /> (near the bottom of the file). Add:
<ViewFieldVisibility
opened={fieldsOpened}
onClose={() => setFieldsOpened(false)}
table={table}
properties={base.properties}
onPersist={onPersistViewConfig}
>
base is already a prop on BaseToolbar — no other plumbing needed.
- Step 2: Build
pnpm nx run client:build
Should succeed.
- Step 3: Commit the popover-side changes
git add \
apps/client/src/features/base/components/views/view-field-visibility.tsx \
apps/client/src/features/base/components/base-toolbar.tsx
git commit -m "fix(base): refresh hide-fields popover when a property is renamed"
Task 7: USER smoke test
⚠️ Do not run
pnpm devas an agent. Hand off to the user.
Ask the user to run through:
-
Local rename updates the grid header instantly.
- Open a base.
- Click a column header → Rename → type a new name → press Enter.
- The column header text updates immediately — no reload.
-
Local rename updates the hide-fields popover instantly.
- After renaming, open the Hide fields popover.
- The property's entry in the list shows the new name.
-
Remote rename (other client) updates without reload.
- Open the same base in two browsers / tabs (A and B).
- In A, rename a property.
- In B, within a second, the header text (and hide-fields popover) show the new name.
-
Regression: column resize, reorder, hide, sort, filter all still work.
If all pass, the fix is complete. Otherwise report back with which case failed.
Out of scope
ViewSortConfigPopover/ViewFilterConfigPopoveralso show property names, but they read frombase.propertiesdirectly (they already getbaseas a prop and re-render when it changes), so they weren't broken by this bug. Not touching them.- Property icons (
property.typechange). A type change already bumpsschemaVersionon the base, which invalidates and refetches — that path works. Out of scope here. - Server-side — rename already persists + broadcasts correctly.