docs(base): add working plans for recent base feature work

This commit is contained in:
Philipinho
2026-04-19 02:05:34 +01:00
parent cac4774641
commit 4c0348e46a
12 changed files with 4348 additions and 0 deletions
@@ -0,0 +1,197 @@
# New Property Not In View 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:** When a user creates a new property, it lands in the grid's local column state immediately (appended to the end, visible by default) so the grid can size and scroll to reveal it.
**Architecture:** Single-file fix in [`apps/client/src/features/base/hooks/use-base-table.ts`](apps/client/src/features/base/hooks/use-base-table.ts). Extend the existing gated re-seed effect so that on property add / remove (within the same view) it reconciles local `columnOrder` and `columnVisibility` instead of ignoring the change. Existing per-column user toggles (hide, reorder) are preserved; new columns are appended; deleted columns are dropped.
**Tech Stack:** React 18, TanStack Table v8, TanStack Query v5.
---
## Background
Earlier in this branch (commit `c6f993b6`) the sync effect that copied `derivedColumnOrder` / `derivedColumnVisibility` into local state was gated behind a view-id ref to stop ws-driven base refetches from stomping the user's pending hide-column toggles. Current code:
```ts
// apps/client/src/features/base/hooks/use-base-table.ts:267-281
const lastSyncedViewIdRef = useRef<string | undefined>(activeView?.id);
useEffect(() => {
const currentViewId = activeView?.id;
if (currentViewId !== lastSyncedViewIdRef.current) {
lastSyncedViewIdRef.current = currentViewId;
setColumnOrder(derivedColumnOrder);
setColumnVisibility(derivedColumnVisibility);
}
}, [activeView?.id, derivedColumnOrder, derivedColumnVisibility]);
```
**What goes wrong with a new property in the same view:**
1. User opens `CreatePropertyPopover`, submits → `useCreatePropertyMutation.onSuccess` appends the new property to `["bases", baseId].properties`.
2. `base.properties` has a new reference → `properties` useMemo re-runs → new array reference.
3. `derivedColumnOrder` recomputes — includes the new property id.
4. The gated effect sees `currentViewId === lastSyncedViewIdRef.current`, so it **does nothing**. Local `columnOrder` state still lists the OLD column ids.
5. `columns` prop to `useReactTable` is rebuilt (it memos on `[properties]`), so react-table does know the new column def exists.
6. But state passed via `state={{columnOrder}}` still references only the old columns → `table.getState().columnOrder` → stale → `gridTemplateColumns` (which depends on `table.getVisibleLeafColumns()` + `table.getState().columnOrder` in [`grid-container.tsx:149`](apps/client/src/features/base/components/grid/grid-container.tsx:149)) doesn't get a track for the new column.
7. Result: the new column renders in the DOM (react-table still yields it as visible), but the grid wrapper's `scrollWidth` doesn't extend to contain it, so `handlePropertyCreated`'s [`scrollRef.current.scrollTo({left: scrollWidth})`](apps/client/src/features/base/components/grid/grid-container.tsx:183-192) ends at the OLD scrollWidth — the new column stays clipped at the edge.
The same mechanism also breaks property **deletion** within the same view (local state keeps a dead id) — not the filed symptom, but worth fixing in the same patch.
The same mechanism does NOT break **rename**, because rename changes `property.name` but not property IDs; order + visibility maps key on id, so they stay correct. Rename is already working after the earlier memo-prop-threading fix.
---
## File Structure
**Modified files:**
- `apps/client/src/features/base/hooks/use-base-table.ts` — extend the sync effect.
No new files, no new deps, nothing server-side.
---
## Task 1: Reconcile local column state on property add / remove
**Files:**
- Modify: `apps/client/src/features/base/hooks/use-base-table.ts:260-281`
- [ ] **Step 1: Replace the effect**
Before (current):
```ts
const lastSyncedViewIdRef = useRef<string | undefined>(activeView?.id);
useEffect(() => {
const currentViewId = activeView?.id;
if (currentViewId !== lastSyncedViewIdRef.current) {
lastSyncedViewIdRef.current = currentViewId;
setColumnOrder(derivedColumnOrder);
setColumnVisibility(derivedColumnVisibility);
}
}, [activeView?.id, derivedColumnOrder, derivedColumnVisibility]);
```
After:
```ts
const lastSyncedViewIdRef = useRef<string | undefined>(activeView?.id);
useEffect(() => {
const currentViewId = activeView?.id;
// View switch → full re-seed from the server's stored config.
if (currentViewId !== lastSyncedViewIdRef.current) {
lastSyncedViewIdRef.current = currentViewId;
setColumnOrder(derivedColumnOrder);
setColumnVisibility(derivedColumnVisibility);
return;
}
// Same view — preserve user toggles, but reconcile the id set:
// append properties that were just created, drop properties that
// were deleted. Without this, creating a new column leaves it
// invisible to `table.getState().columnOrder` / `gridTemplateColumns`,
// and the grid's scrollWidth never grows to include it.
const validIds = new Set<string>(["__row_number"]);
for (const p of properties) validIds.add(p.id);
setColumnOrder((prev) => {
const prevSet = new Set(prev);
const kept = prev.filter((id) => validIds.has(id));
const appended = derivedColumnOrder.filter(
(id) => !prevSet.has(id) && validIds.has(id),
);
if (appended.length === 0 && kept.length === prev.length) return prev;
return [...kept, ...appended];
});
setColumnVisibility((prev) => {
let changed = false;
const next: VisibilityState = {};
for (const [id, visible] of Object.entries(prev)) {
if (validIds.has(id)) {
next[id] = visible;
} else {
changed = true;
}
}
for (const id of derivedColumnOrder) {
if (!(id in next)) {
next[id] = derivedColumnVisibility[id] ?? true;
changed = true;
}
}
return changed ? next : prev;
});
}, [
activeView?.id,
derivedColumnOrder,
derivedColumnVisibility,
properties,
]);
```
Notes for the implementer:
- `VisibilityState` is already imported from `@tanstack/react-table` — no new import needed.
- `properties` is already declared as a memoized value at the top of this hook, so adding it to the dep list is safe.
- The two `setX((prev) => ...)` updaters both short-circuit (return `prev`) when nothing actually changed, which matters because `derivedColumnOrder` / `derivedColumnVisibility` have a new identity every time `properties` does — without the short-circuit the set would fire every render in the same view and blow away user toggles.
- `kept.length === prev.length` is a proxy for "no deletions" and is safe because `prev` can't contain duplicates (react-table enforces id uniqueness, and our own `derivedColumnOrder` is also unique).
- [ ] **Step 2: Build**
```bash
pnpm nx run client:build
```
Expected: success.
- [ ] **Step 3: Commit**
```bash
git add apps/client/src/features/base/hooks/use-base-table.ts
git commit -m "fix(base): include new properties in local column state so the grid can scroll to them"
```
---
## Task 2: USER smoke test
> ⚠️ **Do not run `pnpm dev` as an agent.** Hand off to the user.
After a hard reload:
- [ ] **Create a new property — grid sizes for it and scroll reaches it.**
1. Open a base with enough columns that horizontal scrolling is already active.
2. Click the "+" / create-property button, pick a type, submit.
3. The grid should scroll right automatically; the new column is fully visible; the horizontal scrollbar extends.
- [ ] **New property is visible in the Hide fields popover.**
1. Open the eye icon (Hide fields).
2. The new property appears in the list, toggle ON.
- [ ] **Existing toggles are preserved.**
1. Hide column X.
2. Create a new column Y. Column X stays hidden; Y appears at the end, visible.
- [ ] **Delete a property.**
1. From a property's menu, click Delete.
2. Column disappears from the grid; grid scrollWidth contracts. No stale column left.
- [ ] **View switch still works cleanly.**
1. Switch to a different view; then switch back.
2. Hidden / reordered state for that view loads correctly.
- [ ] **Rename still works (regression check).**
1. Rename a property; the header text updates without reload.
- [ ] **Hide + concurrent sort mutation (regression for the original hide bug).**
1. Hide a column, then add a sort within 300 ms. Column stays hidden; sort applies.
If any step fails, report back with the specific case.
---
## Out of scope
- Scrolling behavior on row add (orthogonal, not broken).
- The two `rAF` delay in `handlePropertyCreated` — it already waits long enough once state reconciliation happens in the same render cycle.
- `columnSizing` reconciliation — a new column uses its defined size automatically via react-table's `initialState`, and a deleted column's entry in the sizing state is harmless.