Add pivot table feature (Phase 1)#21
Conversation
📝 WalkthroughWalkthroughAdds a Pivot Table feature: design docs and tasks; new pivot engine modules (parse → filter → group → aggregate → calculate → materialize) and types; store/Yorkie persistence APIs; sheet engine pivot guards; frontend hook, editor panel and UI wiring (addPivotTab, context menu, tab icon); mobile-handle refactor; and tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User as User (UI)
participant FE as Frontend (SheetView / PivotEditor)
participant Store as YorkieStore (CRDT)
participant Engine as Sheet Engine (calculatePivot / materialize)
participant Sheet as Pivot Sheet (Grid)
User->>FE: create/edit pivot definition (addPivotTab / editor save)
FE->>Store: setPivotDefinition(def)
Store-->>FE: ack
FE->>Store: read source sheet range (sourceGrid)
Store-->>FE: sourceGrid
FE->>Engine: calculatePivot(sourceGrid, def)
Engine-->>FE: PivotResult
FE->>Engine: materialize(PivotResult)
Engine-->>FE: Grid(cells)
FE->>Store: write pivot sheet cells (CRDT updates)
Store-->>Sheet: sync updates to connected clients
Sheet-->>User: rendered pivot sheet
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Verification: verify:selfResult: ✅ PASS in 88.2s
Verification: verify:integrationResult: ✅ PASS |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/sheet/test/store/store.test.ts (1)
263-266: Strengthen clone-safety check to validate clone-on-read as well.Mutating
defonly proves clone-on-write. Mutate the fetched object to ensuregetPivotDefinition()also returns a defensive clone.✅ Suggested test adjustment
await store.setPivotDefinition(def); const result = await store.getPivotDefinition(); expect(result).toEqual(def); - // Verify deep clone (mutation safety) - def.rowFields[0].label = 'Changed'; + // Verify deep clone on read (mutation safety) + result!.rowFields[0].label = 'Changed'; const result2 = await store.getPivotDefinition(); expect(result2!.rowFields[0].label).toBe('Category');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/test/store/store.test.ts` around lines 263 - 266, The test currently only mutates the local `def` to prove clone-on-write; extend it to also verify clone-on-read by mutating the object returned from `store.getPivotDefinition()` and asserting the subsequent call still returns the original values. Specifically, after `const result2 = await store.getPivotDefinition()`, change `result2.rowFields[0].label` (or another fetched property) and then call `await store.getPivotDefinition()` again and assert its `rowFields[0].label` remains `'Category'`; this ensures `getPivotDefinition()` returns a defensive clone rather than a live reference.design/pivot-table.md (1)
46-59: Consider adding language identifiers to fenced code blocks.Static analysis flagged several fenced code blocks without language specifiers (lines 46, 166, 311, 329, 370). For ASCII diagrams, use
textorplaintextas the language identifier to satisfy markdown linters.📝 Example fix for the architecture diagram
-``` +```text ┌──────────────────────────────────────────────────┐ │ Frontend (React) │🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@design/pivot-table.md` around lines 46 - 59, Add a language identifier (e.g., text or plaintext) to the fenced code blocks in design/pivot-table.md—specifically update the ASCII architecture diagram block (the three-backtick fenced block containing the box diagram with Frontend/Sheet Engine/Store) and the other unnamed fenced blocks flagged by the linter (lines noted in the review) so they become ```text (or ```plaintext); this satisfies markdown linters while preserving the diagram content and formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/sheet/src/model/pivot/aggregate.ts`:
- Around line 48-74: The switch in aggregateValues currently only handles
'SUM','AVERAGE','MIN','MAX' and can fall through returning undefined for
unexpected aggregation values; add an explicit default branch (or a final guard
after the switch) that throws a descriptive Error (including the invalid
aggregation value and relevant context like nums) or returns a safe fallback
string, so the function never returns undefined for malformed input; update the
switch handling in aggregateValues to include this default/error path and ensure
the thrown message references the aggregation identifier to aid debugging.
In `@packages/sheet/src/model/pivot/calculate.ts`:
- Around line 150-155: The branch that calls aggregateValues(valueFields[0]) can
crash when valueFields is empty; add a guard in the same conditional that checks
valueFields.length === 0 and handle it deterministically (e.g., push a
placeholder cell like { value: null, type: 'value' } or skip/return early)
instead of dereferencing valueFields[0]. Update the block around hasColumns /
the single-value-field branch so functions aggregateValues and code that
consumes the cell always receive a valid cell object or an early exit when
valueFields is empty.
In `@packages/sheet/src/model/sheet.ts`:
- Around line 897-898: The structural mutation methods insertRows, deleteRows,
insertColumns, and deleteColumns currently lack the pivot-sheet guard present in
setData/removeData/paste; update each of these methods in class Sheet
(insertRows, deleteRows, insertColumns, deleteColumns) to early-return (or
throw) when this.pivotDefinition is set—mirroring the check in setData—or
alternatively add inline documentation and tests explaining and asserting that
structural edits are allowed on pivot sheets; also add unit tests covering
pivotBehavior for these four methods to make the intended behavior explicit.
---
Nitpick comments:
In `@design/pivot-table.md`:
- Around line 46-59: Add a language identifier (e.g., text or plaintext) to the
fenced code blocks in design/pivot-table.md—specifically update the ASCII
architecture diagram block (the three-backtick fenced block containing the box
diagram with Frontend/Sheet Engine/Store) and the other unnamed fenced blocks
flagged by the linter (lines noted in the review) so they become ```text (or
```plaintext); this satisfies markdown linters while preserving the diagram
content and formatting.
In `@packages/sheet/test/store/store.test.ts`:
- Around line 263-266: The test currently only mutates the local `def` to prove
clone-on-write; extend it to also verify clone-on-read by mutating the object
returned from `store.getPivotDefinition()` and asserting the subsequent call
still returns the original values. Specifically, after `const result2 = await
store.getPivotDefinition()`, change `result2.rowFields[0].label` (or another
fetched property) and then call `await store.getPivotDefinition()` again and
assert its `rowFields[0].label` remains `'Category'`; this ensures
`getPivotDefinition()` returns a defensive clone rather than a live reference.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2a165ef1-706e-47cb-85b5-b3cfe0605c1f
📒 Files selected for processing (31)
design/README.mddesign/pivot-table.mdpackages/frontend/src/app/documents/document-detail.tsxpackages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsxpackages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/frontend/src/app/spreadsheet/sheet-view.tsxpackages/frontend/src/app/spreadsheet/yorkie-store.tspackages/frontend/src/components/sheet-context-menu.tsxpackages/frontend/src/components/tab-bar.tsxpackages/frontend/src/types/worksheet.tspackages/sheet/src/index.tspackages/sheet/src/model/pivot/aggregate.tspackages/sheet/src/model/pivot/calculate.tspackages/sheet/src/model/pivot/group.tspackages/sheet/src/model/pivot/index.tspackages/sheet/src/model/pivot/materialize.tspackages/sheet/src/model/pivot/parse.tspackages/sheet/src/model/sheet.tspackages/sheet/src/model/types.tspackages/sheet/src/store/memory.tspackages/sheet/src/store/readonly.tspackages/sheet/src/store/store.tspackages/sheet/test/sheet/pivot-aggregate.test.tspackages/sheet/test/sheet/pivot-calculate.test.tspackages/sheet/test/sheet/pivot-group.test.tspackages/sheet/test/sheet/pivot-materialize.test.tspackages/sheet/test/sheet/pivot-parse.test.tspackages/sheet/test/sheet/pivot-protection.test.tspackages/sheet/test/store/store.test.tstasks/active/20260304-pivot-table-lessons.mdtasks/active/20260304-pivot-table-todo.md
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts (1)
122-137: KeepremoveFieldstrongly typed instead of casting tounknown[].The
unknown[]cast on Line 133 weakens type safety in a mutation path. A typed branch keeps compile-time guarantees for each field list.💡 Suggested refactor
const removeField = useCallback( ( section: | "rowFields" | "columnFields" | "valueFields" | "filterFields", index: number, ) => { - updateDefinition((def) => ({ - ...def, - [section]: (def[section] as unknown[]).filter( - (_: unknown, i: number) => i !== index, - ), - })); + updateDefinition((def) => { + switch (section) { + case "rowFields": + return { ...def, rowFields: def.rowFields.filter((_, i) => i !== index) }; + case "columnFields": + return { + ...def, + columnFields: def.columnFields.filter((_, i) => i !== index), + }; + case "valueFields": + return { ...def, valueFields: def.valueFields.filter((_, i) => i !== index) }; + case "filterFields": + return { + ...def, + filterFields: def.filterFields.filter((_, i) => i !== index), + }; + } + }); }, [updateDefinition], );As per coding guidelines, "Use type annotations in TypeScript to improve code clarity and catch errors early."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines 122 - 137, The removeField callback weakens type safety by casting def[section] to unknown[]; update it to index into the definition with the actual typed key instead of an unknown cast — make section be the keyof the pivot definition type (the same type used by updateDefinition, e.g., PivotDefinition keys "rowFields"|"columnFields"|"valueFields"|"filterFields") and read const list = def[section] (typed as the correct array type) then return { ...def, [section]: list.filter((_, i) => i !== index) } so TypeScript preserves element types; adjust the removeField signature and body (referencing removeField and updateDefinition) to use the real PivotDefinition key type instead of unknown[].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 47-62: The effect in useEffect that reads the pivot definition
(using doc, tabId, root, ws and ws.pivotTable) only sets definition when pivot
metadata exists but never clears it; update the effect to call
setDefinition(undefined) (or reset to an empty/default PivotTableDefinition)
whenever doc or tabId change and there is no ws.pivotTable present so stale
pivot state is cleared when switching to a non-pivot sheet or when pivot
metadata is removed; keep the existing branch that JSON-parses and sets the
definition for the positive case (including
rowFields/columnFields/valueFields/filterFields/showTotals) and add an explicit
else that clears/resets via setDefinition.
- Around line 64-77: The updater currently runs doc.update(...) inside the
setDefinition functional updater (updateDefinition -> setDefinition), causing
side effects inside a React state updater; instead, compute the new
PivotTableDefinition outside the state updater (e.g., const next =
updater(definition) after guarding definition and doc), then call
setDefinition(() => next) and finally call doc.update(root => { const ws =
root.sheets[tabId]; if (ws) ws.pivotTable = next; }); Ensure you null-check doc
and definition/PivotTableDefinition before computing next and performing
doc.update.
---
Nitpick comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 122-137: The removeField callback weakens type safety by casting
def[section] to unknown[]; update it to index into the definition with the
actual typed key instead of an unknown cast — make section be the keyof the
pivot definition type (the same type used by updateDefinition, e.g.,
PivotDefinition keys "rowFields"|"columnFields"|"valueFields"|"filterFields")
and read const list = def[section] (typed as the correct array type) then return
{ ...def, [section]: list.filter((_, i) => i !== index) } so TypeScript
preserves element types; adjust the removeField signature and body (referencing
removeField and updateDefinition) to use the real PivotDefinition key type
instead of unknown[].
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bb56e1ce-0602-48ff-9897-b30c7de99093
📒 Files selected for processing (1)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/sheet/src/model/pivot/aggregate.ts (1)
49-57: Optional: deduplicate sum computation forSUMandAVERAGE.Both branches recompute the same sum. Precomputing once will reduce duplication and keep this block easier to maintain.
♻️ Proposed refactor
switch (aggregation) { + case 'SUM': + case 'AVERAGE': { + let sum = 0; + for (const n of nums) sum += n; + if (aggregation === 'SUM') return String(sum); + return String(sum / nums.length); + } - case 'SUM': { - let sum = 0; - for (const n of nums) sum += n; - return String(sum); - } - case 'AVERAGE': { - let sum = 0; - for (const n of nums) sum += n; - return String(sum / nums.length); - } case 'MIN': {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/model/pivot/aggregate.ts` around lines 49 - 57, The SUM and AVERAGE branches in the pivot aggregation (cases 'SUM' and 'AVERAGE' in aggregate.ts) both compute the sum by iterating nums; refactor by computing the sum once before the switch (or in a shared helper) and then use that value in the 'SUM' return and in the 'AVERAGE' return (divide by nums.length); keep the same return format (String(...)) and ensure you still handle empty nums appropriately if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 69-80: The effect can write a stale definition when doc or tabId
change because isInitialLoad is computed from definitionRef before updating it;
fix by reading and updating the ref in the correct order: capture the previous
value (e.g., const prevDef = definitionRef.current), then immediately set
definitionRef.current = definition, then compute isInitialLoad as prevDef ===
null && definition !== null, and only then return early if isInitialLoad || !doc
|| !definition; this ensures the doc.update call (and ws.pivotTable =
definition) always uses the latest definition for the current tabId and
document.
---
Nitpick comments:
In `@packages/sheet/src/model/pivot/aggregate.ts`:
- Around line 49-57: The SUM and AVERAGE branches in the pivot aggregation
(cases 'SUM' and 'AVERAGE' in aggregate.ts) both compute the sum by iterating
nums; refactor by computing the sum once before the switch (or in a shared
helper) and then use that value in the 'SUM' return and in the 'AVERAGE' return
(divide by nums.length); keep the same return format (String(...)) and ensure
you still handle empty nums appropriately if needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 751cef43-37c8-4ea0-8746-cb7d8d968046
📒 Files selected for processing (5)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/sheet/src/model/pivot/aggregate.tspackages/sheet/src/model/pivot/calculate.tspackages/sheet/src/model/sheet.tspackages/sheet/test/sheet/pivot-protection.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/sheet/test/sheet/pivot-protection.test.ts
- packages/sheet/src/model/pivot/calculate.ts
- packages/sheet/src/model/sheet.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/sheet/src/index.ts (1)
108-110: Consolidate duplicatecoordinatesimports.Lines 37–45 import from
./model/coordinates, and line 110 importsparseRangefrom the same module. Consolidate by addingparseRangeto the existing import block to keep the module boundary cleaner and avoid duplicate-import lint issues.Suggested diff
import { toColumnLabel, toSref, toSrefs, parseRef, inRange, isCrossSheetRef, parseCrossSheetRef, + parseRange, } from './model/coordinates'; @@ -import { parseRange } from './model/coordinates';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/index.ts` around lines 108 - 110, The file currently imports parseRange separately from './model/coordinates'; consolidate that into the existing import block that already imports from './model/coordinates' (add parseRange to that import) and remove the standalone import of parseRange so there's a single import statement for './model/coordinates' (refer to the parseRange symbol and the existing coordinates import block to locate and update the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/sheet/src/index.ts`:
- Around line 108-110: The file currently imports parseRange separately from
'./model/coordinates'; consolidate that into the existing import block that
already imports from './model/coordinates' (add parseRange to that import) and
remove the standalone import of parseRange so there's a single import statement
for './model/coordinates' (refer to the parseRange symbol and the existing
coordinates import block to locate and update the code).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c1a1a4a4-a07f-43b9-adf8-d0428973d7d2
📒 Files selected for processing (2)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/sheet/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts (1)
58-69:⚠️ Potential issue | 🔴 CriticalRace condition persists when switching tabs — stale definition can overwrite the new tab's pivot metadata.
The past review flagged this issue. The current fix using
isInitialLoadonly guards against the first load (null → non-null), but does not prevent the effect from running with a staledefinitionvalue whendocortabIdchanges.When switching from Tab A to Tab B:
- React updates
tabIddependency- The sync effect (lines 60-69) runs immediately with Tab A's definition still in state
- It writes Tab A's definition to Tab B's
ws.pivotTable- Only then does the load effect (lines 36-56) run and load Tab B's correct definition
Proposed fix: Track previous source and skip sync when source changes
// Sync definition changes to Yorkie document const definitionRef = useRef(definition); + const prevSourceRef = useRef<{ doc: typeof doc; tabId: string }>({ + doc, + tabId, + }); + useEffect(() => { + const sourceChanged = + prevSourceRef.current.doc !== doc || + prevSourceRef.current.tabId !== tabId; + prevSourceRef.current = { doc, tabId }; + // Skip the initial load (when definitionRef.current transitions from null) const isInitialLoad = definitionRef.current === null && definition !== null; definitionRef.current = definition; - if (isInitialLoad || !doc || !definition) return; + if (sourceChanged || isInitialLoad || !doc || !definition) return; doc.update((root) => { const ws = root.sheets[tabId]; if (ws) ws.pivotTable = definition; }); }, [doc, tabId, definition]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines 58 - 69, The sync effect still uses a stale definition when tabId or doc changes; fix by tracking the source of the cached definition and skipping sync if the source changed: keep a ref (e.g., definitionRef with an associated sourceRef or previousTabIdRef) that stores both the definition and the tabId it came from, and in the useEffect that calls doc.update compare sourceRef.currentTabId to the current tabId and return early if they differ (also keep the isInitialLoad guard). Update the ref values only after verifying a safe sync is permitted so doc.update never writes a definition that was captured for a different tabId; reference definitionRef, sourceRef/previousTabIdRef, tabId, definition, and doc.update.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 58-69: The sync effect still uses a stale definition when tabId or
doc changes; fix by tracking the source of the cached definition and skipping
sync if the source changed: keep a ref (e.g., definitionRef with an associated
sourceRef or previousTabIdRef) that stores both the definition and the tabId it
came from, and in the useEffect that calls doc.update compare
sourceRef.currentTabId to the current tabId and return early if they differ
(also keep the isInitialLoad guard). Update the ref values only after verifying
a safe sync is permitted so doc.update never writes a definition that was
captured for a different tabId; reference definitionRef,
sourceRef/previousTabIdRef, tabId, definition, and doc.update.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2a7deafd-63c7-458d-8621-7ac85ecaebcb
📒 Files selected for processing (3)
packages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsxpackages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/frontend/src/app/spreadsheet/sheet-view.tsx
Implement a complete pivot table feature that lets users select a data range, create a pivot sheet, and configure row/column/value/filter fields via a sidebar editor. The calculation pipeline includes source parsing, filtering, grouping, aggregation (SUM, COUNT, COUNTA, AVERAGE, MIN, MAX), and materialization to styled cells. Key changes: - Add pivot types, calculation modules (parse, group, aggregate, calculate, materialize) to the sheet package - Add pivot editor panel with state management hook - Wire pivot creation into context menu and document tabs - Add Store.invalidate() for external write support - Fix Yorkie CRDT proxy issues: use direct property access instead of JSON.parse(JSON.stringify()) which double-encodes - Add pivot sheet protection (block direct cell edits) - Add comprehensive unit and e2e tests Co-Authored-By: Claude Opus 4.6 <[email protected]>
a2e52e4 to
0e92491
Compare
|
@coderabbitai Could you review this PR again? |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/sheet/src/view/cellinput.ts (1)
137-159:⚠️ Potential issue | 🟠 Major
prime()now forces focus on every priming path.Line 158 turns a primed editor into an actual focus event unconditionally. If
prime()is used for selection/tap flows, this will blur external controls and can pop the mobile keyboard before the user has actually entered edit mode. Keep the old opt-out or make focusing an explicit editing-only choice.⌨️ Proposed fix
public prime( left: number, top: number, width?: number, height?: number, maxWidth?: number, maxHeight?: number, + focus: boolean = true, ): void { this.primed = true; this.updateFrame( left, top, @@ this.renderInput(); this.adjustSize(); this.applyPrimedAppearance(); - this.input.focus(); + if (focus) { + this.input.focus(); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/view/cellinput.ts` around lines 137 - 159, The prime() method currently always calls this.input.focus(), causing unwanted focus/keyboard on non-edit selection flows; change prime to accept an optional boolean parameter (e.g., focus?: boolean = false) or a second "editing" flag and only call this.input.focus() when that flag is true, leaving applyPrimedAppearance(), renderInput(), adjustSize() behavior unchanged; update any callers that expect immediate focus to pass true (or call a new enterEdit()/focusEditor() helper that focuses) and ensure existing non-edit selection/tap flows keep the opt-out by not passing the focus flag.
♻️ Duplicate comments (1)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts (1)
78-86:⚠️ Potential issue | 🔴 CriticalSkip the sync effect when
docortabIdchanges.This still has the cross-tab overwrite race from the earlier review: on the first render after a tab switch,
definitionis still the previous tab’s state, so this effect writes it into the new sheet before the loader effect finishes. That’s destructive. Track the previous(doc, tabId)and suppress sync during source transitions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines 78 - 86, The effect using definitionRef currently writes stale `definition` into the new sheet on tab switches; modify the useEffect that calls doc.update so it tracks previous `doc` and `tabId` (e.g., via refs like prevDocRef and prevTabIdRef) and returns early when either `doc` or `tabId` has changed since the last render to suppress syncing during a source transition; keep the existing initial-load guard (definitionRef) and only perform doc.update((root) => { const ws = root.sheets[tabId]; if (ws) ws.pivotTable = definition; }) when doc and tabId are stable and not in a transition.
🧹 Nitpick comments (6)
packages/frontend/src/components/datasource-selector.tsx (2)
36-44: Prefer async/await over .then() chains.The promise chain could be refactored to use async/await for consistency with coding guidelines.
♻️ Proposed async/await refactor
useEffect(() => { + const loadDataSources = async () => { + setLoading(true); + try { + const data = await fetchDataSources(); + setDatasources(data); + } catch { + setDatasources([]); + } finally { + setLoading(false); + } + }; + if (open) { - setLoading(true); - fetchDataSources() - .then(setDatasources) - .catch(() => setDatasources([])) - .finally(() => setLoading(false)); + loadDataSources(); } }, [open]);As per coding guidelines: "Use async/await instead of .then() chains for promise handling".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/datasource-selector.tsx` around lines 36 - 44, Refactor the useEffect callback in the datasource-selector component to use async/await instead of .then() chains: inside the effect when open is true, create an async function (e.g., loadDatasources) that sets setLoading(true), awaits fetchDataSources(), calls setDatasources with the result, catches errors to call setDatasources([]), and ensures setLoading(false) in a finally block; invoke that async function from useEffect and keep the dependency on open.
128-132: Same async/await refactor applies here.The refresh logic after dialog close also uses a .then() chain.
♻️ Proposed async/await refactor
onOpenChange={(v) => { setShowCreate(v); if (!v) { // Refresh list after creating - fetchDataSources() - .then(setDatasources) - .catch(() => {}); + (async () => { + try { + const data = await fetchDataSources(); + setDatasources(data); + } catch { + // ignore + } + })(); } }}As per coding guidelines: "Use async/await instead of .then() chains for promise handling".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/components/datasource-selector.tsx` around lines 128 - 132, Replace the .then()/.catch() chain used to refresh datasources with an async/await pattern: mark the surrounding handler that runs after the dialog closes as async, call const updated = await fetchDataSources() inside a try block, then call setDatasources(updated), and handle errors in the catch block (instead of .catch(()=>{})). This change targets the fetchDataSources and setDatasources calls so the refresh uses explicit await and proper error handling.packages/sheet/src/model/pivot/parse.ts (1)
20-23: Consider explicit string conversion for cell values.Cell values (
cell.v) may be numbers or booleans, not just strings. While JavaScript's implicit coercion handles this when pushed to a string array, explicit conversion would be more robust and self-documenting.♻️ Proposed fix for explicit string conversion
for (let c = from.c; c <= to.c; c++) { const cell = grid.get(toSref({ r: from.r, c })); - headers.push(cell?.v ?? ''); + headers.push(cell?.v != null ? String(cell.v) : ''); }Apply the same pattern at line 31:
const cell = grid.get(toSref({ r, c })); - row.push(cell?.v ?? ''); + row.push(cell?.v != null ? String(cell.v) : '');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/model/pivot/parse.ts` around lines 20 - 23, The loop that builds headers uses headers.push(cell?.v ?? '') but cell.v can be non-string; change to explicitly convert to string (e.g., headers.push(String(cell?.v ?? ''))) to make types explicit and robust, and apply the same explicit String(...) conversion to the other analogous push in this module where cell?.v is added (the other header/value push that mirrors this loop using grid.get and toSref).packages/sheet/src/model/pivot/materialize.ts (1)
23-31: Consider consolidating identical switch cases.The
rowHeader,colHeader,empty, andtotalcases currently apply identical styling. If the intent is for them to remain the same, consolidating reduces duplication:♻️ Optional consolidation
switch (pivotCell.type) { case 'rowHeader': case 'colHeader': case 'empty': - cell = { v: pivotCell.value, s: { b: true } }; - break; case 'total': cell = { v: pivotCell.value, s: { b: true } }; break;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/model/pivot/materialize.ts` around lines 23 - 31, The switch in materializePivot (handling pivotCell.type) duplicates the same assignment for 'rowHeader', 'colHeader', 'empty', and 'total'; consolidate these into a single case group (e.g., case 'rowHeader': case 'colHeader': case 'empty': case 'total':) that assigns cell = { v: pivotCell.value, s: { b: true } } to remove duplication while preserving behavior.packages/sheet/test/sheet/pivot-calculate.test.ts (1)
27-45: Cover the totals rows/columns explicitly.These cases flip
showTotals, but the assertions never touch the extra total cells. A regression that drops the grand-total row/column would still pass. Please assert the totals when enabled and their absence when disabled. As per coding guidelines,**/*.{test,spec}.{js,ts,jsx,tsx}: Write tests for critical business logic and edge cases.Also applies to: 47-62
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/test/sheet/pivot-calculate.test.ts` around lines 27 - 45, The test is not asserting the totals cells for the pivot so regressions removing grand-total row/column would go unnoticed; update the test that constructs the PivotTableDefinition (using calculatePivot and showTotals) to explicitly assert the values of the totals cells when showTotals.rows/columns are true (e.g., check the bottom-row grand total and right-column grand total cells) and add a complementary test case (or extend the existing one at the other block) that sets showTotals.rows/columns to false and asserts those total cells are absent or empty; locate usages of calculatePivot and the PivotTableDefinition in the failing test blocks to add these assertions.packages/sheet/src/model/pivot/calculate.ts (1)
141-146: HoistrowSetout of the inner loop.This rebuilds the same
Setonce per column leaf, which adds avoidable work and allocations on wide pivots.♻️ Proposed refactor
if (hasColumns) { + const rowSet = new Set(rowLeaf.indices); for (const colLeaf of colLeaves) { // Find intersection: record indices that belong to both row and column. - const rowSet = new Set(rowLeaf.indices); const intersection = colLeaf.indices.filter((i) => rowSet.has(i));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sheet/src/model/pivot/calculate.ts` around lines 141 - 146, The inner loop recreates the same Set for each colLeaf: move the creation of rowSet = new Set(rowLeaf.indices) out of the colLeaves loop so it is built once per rowLeaf (i.e., compute rowSet before iterating over colLeaves in the block guarded by hasColumns), then use that single rowSet when computing intersection = colLeaf.indices.filter(i => rowSet.has(i)); update the references in calculate.ts accordingly to avoid per-column allocations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@design/pivot-table.md`:
- Around line 297-307: The design doc's pivot-sheet protection section is out of
sync: implementation now detects pivots with the TabMeta.kind === "pivot" check
(used in Sheet.setData()) instead of reading the pivot definition via
isPivotSheet() (which previously read from the store), and internal
materialization bypasses the guard via a private method; update the text to
state that pivot detection is based on TabMeta.kind === "pivot", describe that
Sheet.setData() early-returns for TabMeta.kind === "pivot" to block edits, and
note the internal materialization path uses a private/internal method that
bypasses this guard rather than reading pivot definitions from the store.
- Around line 46-59: The fenced ASCII diagram blocks (the ones containing
strings like "Frontend (React)", "PivotEditorPanel", "Sheet Engine
(packages/sheet)", "PivotCalculator", and "Store / Yorkie
CRDT"/"PivotTableDefinition") need language identifiers on their triple-backtick
fences to satisfy markdownlint; edit each fence to use a neutral identifier such
as ```text (or ```text/plain) so the blocks render and lint cleanly. Update the
primary diagram fence and the other similar fenced blocks referenced in the
comment (the ones containing the same unique labels like PivotEditorPanel,
PivotCalculator, PivotMaterializer, PivotTableDefinition) to use the same
language tag for consistency.
In `@packages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsx`:
- Around line 104-121: The editor currently prevents adding the same source
column to multiple valueFields because usedColumns and availableColumns exclude
any column once used and keys like `val-${field.sourceColumn}` collide; change
the logic so usedColumns is built only from rowFields, columnFields and
filterFields (exclude valueFields) so valueFields may reference the same
sourceColumn, and update any element keys that use just `field.sourceColumn`
(e.g., `val-${field.sourceColumn}`) to include a unique per-field identifier or
the field array index (e.g., `val-${field.sourceColumn}-${i}` or a field.id) to
avoid key collisions; ensure calculatePivot usage remains unchanged since it
already supports duplicate valueFields.
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 91-120: The sourceGrid computed by the useMemo hook (sourceGrid)
becomes stale because it only depends on [doc, definition] and Yorkie document
content changes don’t change those refs; fix by either (A) adding a
document-change signal as a memo dependency — subscribe to doc changes
(doc.subscribe) and increment a local version/state (e.g., sourceVersion) then
include sourceVersion in the useMemo deps so sourceGrid recomputes on edits, or
(B) remove the useMemo and move the grid-building logic into the existing
refresh() and getSourceHeaders() call sites so they always build a fresh grid
from doc.getRoot()/sourceWs when invoked; apply the change around the sourceGrid
creation code and update any callers that expect sourceGrid to remain valid
(refresh(), getSourceHeaders()) accordingly.
- Around line 235-237: The bulk clear loop iterates Object.keys directly on the
Yorkie CRDT proxy (ws.sheet), which can throw "ownKeys duplicate" errors; change
it to first take a plain snapshot of the sheet and iterate that snapshot's keys
(e.g., use the { ...ws.sheet } / snapshotObject pattern used in yorkie-store.ts)
and then delete each key from ws.sheet, or alternatively delete cells
individually by building keysToDelete = Object.keys({ ...ws.sheet }) and then
running delete ws.sheet[key] for each key; update the loop that references
ws.sheet accordingly to avoid reading keys from the proxy directly.
In `@packages/frontend/src/app/spreadsheet/sheet-view.tsx`:
- Around line 876-893: The PivotEditorPanel is being mounted even when the sheet
is read-only; update the conditional that renders PivotEditorPanel so it also
checks the readOnly flag (e.g., only render when doc && isPivotTab &&
pivotEditorOpen && !readOnly). Ensure the onClose/onRefresh handlers remain the
same (references: PivotEditorPanel, setPivotEditorOpen, sheetRef,
setSheetRenderVersion) so the panel is never mounted or usable in read-only
mode.
In `@packages/frontend/src/app/spreadsheet/yorkie-store.ts`:
- Around line 1205-1208: The getPivotDefinition method currently returns
structuredClone(ws.pivotTable) which fails for Yorkie CRDT proxies; replace the
structuredClone usage in getPivotDefinition with manual extraction of each pivot
property (e.g., use the same pattern as in use-pivot-table.ts: use
readField(...) for scalar fields and Array.from(...) for arrays or iterables) to
build and return a plain PivotTableDefinition object detached from the Yorkie
proxy so loadPivotDefinition and pivot guards won't throw; ensure you reference
ws.pivotTable and populate the exact PivotTableDefinition fields when returning.
In `@packages/frontend/src/components/mobile-selection-handles.tsx`:
- Around line 30-35: The touchmove handler currently re-reads the selection via
spreadsheet.getSelectionRangeOrActiveCell(), causing the anchor to drift;
instead capture the opposite corner once in onTouchStart (e.g., store the
bottom-right cell in a ref like fixedAnchorRef) and reuse that stored anchor
inside onTouchMove when calling spreadsheet.selectStart/selectEnd so the
opposite corner stays fixed for the whole gesture; also clear the ref on
onTouchEnd/onTouchCancel. Ensure you update the handlers that reference
spreadsheet.getSelectionRangeOrActiveCell()
(onTouchStart/onTouchMove/onTouchEnd) to use the captured anchor ref.
In `@packages/sheet/src/model/pivot/aggregate.ts`:
- Around line 24-31: The COUNTA branch counts undefined as non-empty; update the
loop in the aggregation === 'COUNTA' branch to treat missing cells as empty by
first reading const val = records[i][sourceColumn] and only incrementing count
when val is neither undefined nor the empty string (e.g., if (val !== undefined
&& val !== '') { count++; }), mirroring the guard used in the numeric
aggregation branches so out-of-range/stale sourceColumn values don't inflate the
count.
In `@packages/sheet/src/model/pivot/materialize.ts`:
- Around line 4-12: The docstring for materialize is inconsistent with the
implementation: update the comment in the materialize function (the block
describing PivotResult -> Grid and cell styling) to remove "gray background" and
state that total cells receive bold styling only (matching the implementation
that sets { b: true } for totals); reference materialize, PivotResult, Grid and
Cell so reviewers can find and reconcile the docstring with the code.
In `@packages/sheet/src/model/sheet.ts`:
- Around line 446-449: Initialize the pivotDefinition cache in the Sheet
constructor (set this.pivotDefinition to undefined or call
this.loadPivotDefinition() during bootstrap) and add calls to await
this.loadPivotDefinition() inside the existing cache-reload sequences in both
undo() and redo() so pivotDefinition is refreshed alongside the other cached
state; update references to pivotDefinition in guards to rely on the freshly
loaded cache from loadPivotDefinition (function names: constructor,
loadPivotDefinition, undo, redo, pivotDefinition, store.getPivotDefinition).
In `@packages/sheet/src/model/shifting.ts`:
- Around line 80-88: The rewrite drops leading '$' markers because
parseRef/toSref lose absolute flags; fix by preserving and reapplying absolute
markers around moved refs: before calling parseRef on each token (both single
and range branches in the blocks using parseRef/moveRef/toSref), detect and
store the original column/row absolute flags (presence of '$' in startStr/endStr
or text), pass those flags through or attach them to the Ref object (or extend
moveRef/toSref to accept an absolute mask), then after moveRef reserialize using
a toSref variant that reintroduces the stored absolute markers; apply the same
change for all other occurrences of parseRef→moveRef→toSref (the other blocks
you noted).
In `@packages/sheet/test/sheet/pivot-aggregate.test.ts`:
- Around line 19-25: The test currently treats aggregation: 'COUNT' as counting
all rows including blanks and "N/A", which is incorrect for spreadsheet
semantics; update the test in pivot-aggregate.test.ts so that when using
PivotValueField with aggregation: 'COUNT' (in the it('COUNT counts all values
including empty') case referencing sumField, aggregateValues, and records) the
expected value is '3' (numeric-only count) while leaving the COUNTA test
expecting '4'; alternatively, if you intended row-count semantics, rename the
aggregation to a distinct name and adjust the assertion accordingly.
---
Outside diff comments:
In `@packages/sheet/src/view/cellinput.ts`:
- Around line 137-159: The prime() method currently always calls
this.input.focus(), causing unwanted focus/keyboard on non-edit selection flows;
change prime to accept an optional boolean parameter (e.g., focus?: boolean =
false) or a second "editing" flag and only call this.input.focus() when that
flag is true, leaving applyPrimedAppearance(), renderInput(), adjustSize()
behavior unchanged; update any callers that expect immediate focus to pass true
(or call a new enterEdit()/focusEditor() helper that focuses) and ensure
existing non-edit selection/tap flows keep the opt-out by not passing the focus
flag.
---
Duplicate comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 78-86: The effect using definitionRef currently writes stale
`definition` into the new sheet on tab switches; modify the useEffect that calls
doc.update so it tracks previous `doc` and `tabId` (e.g., via refs like
prevDocRef and prevTabIdRef) and returns early when either `doc` or `tabId` has
changed since the last render to suppress syncing during a source transition;
keep the existing initial-load guard (definitionRef) and only perform
doc.update((root) => { const ws = root.sheets[tabId]; if (ws) ws.pivotTable =
definition; }) when doc and tabId are stable and not in a transition.
---
Nitpick comments:
In `@packages/frontend/src/components/datasource-selector.tsx`:
- Around line 36-44: Refactor the useEffect callback in the datasource-selector
component to use async/await instead of .then() chains: inside the effect when
open is true, create an async function (e.g., loadDatasources) that sets
setLoading(true), awaits fetchDataSources(), calls setDatasources with the
result, catches errors to call setDatasources([]), and ensures setLoading(false)
in a finally block; invoke that async function from useEffect and keep the
dependency on open.
- Around line 128-132: Replace the .then()/.catch() chain used to refresh
datasources with an async/await pattern: mark the surrounding handler that runs
after the dialog closes as async, call const updated = await fetchDataSources()
inside a try block, then call setDatasources(updated), and handle errors in the
catch block (instead of .catch(()=>{})). This change targets the
fetchDataSources and setDatasources calls so the refresh uses explicit await and
proper error handling.
In `@packages/sheet/src/model/pivot/calculate.ts`:
- Around line 141-146: The inner loop recreates the same Set for each colLeaf:
move the creation of rowSet = new Set(rowLeaf.indices) out of the colLeaves loop
so it is built once per rowLeaf (i.e., compute rowSet before iterating over
colLeaves in the block guarded by hasColumns), then use that single rowSet when
computing intersection = colLeaf.indices.filter(i => rowSet.has(i)); update the
references in calculate.ts accordingly to avoid per-column allocations.
In `@packages/sheet/src/model/pivot/materialize.ts`:
- Around line 23-31: The switch in materializePivot (handling pivotCell.type)
duplicates the same assignment for 'rowHeader', 'colHeader', 'empty', and
'total'; consolidate these into a single case group (e.g., case 'rowHeader':
case 'colHeader': case 'empty': case 'total':) that assigns cell = { v:
pivotCell.value, s: { b: true } } to remove duplication while preserving
behavior.
In `@packages/sheet/src/model/pivot/parse.ts`:
- Around line 20-23: The loop that builds headers uses headers.push(cell?.v ??
'') but cell.v can be non-string; change to explicitly convert to string (e.g.,
headers.push(String(cell?.v ?? ''))) to make types explicit and robust, and
apply the same explicit String(...) conversion to the other analogous push in
this module where cell?.v is added (the other header/value push that mirrors
this loop using grid.get and toSref).
In `@packages/sheet/test/sheet/pivot-calculate.test.ts`:
- Around line 27-45: The test is not asserting the totals cells for the pivot so
regressions removing grand-total row/column would go unnoticed; update the test
that constructs the PivotTableDefinition (using calculatePivot and showTotals)
to explicitly assert the values of the totals cells when showTotals.rows/columns
are true (e.g., check the bottom-row grand total and right-column grand total
cells) and add a complementary test case (or extend the existing one at the
other block) that sets showTotals.rows/columns to false and asserts those total
cells are absent or empty; locate usages of calculatePivot and the
PivotTableDefinition in the failing test blocks to add these assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6f8a2597-3127-47a0-ae21-1a497dd2dd18
📒 Files selected for processing (52)
design/README.mddesign/pivot-table.mdpackages/backend/test/database.e2e-spec.tspackages/frontend/src/app/datasources/datasource-list.tsxpackages/frontend/src/app/documents/document-detail.tsxpackages/frontend/src/app/harness/visual/sheet-scenarios.tsxpackages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsxpackages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/frontend/src/app/spreadsheet/sheet-view.tsxpackages/frontend/src/app/spreadsheet/yorkie-store.tspackages/frontend/src/app/workspaces/workspace-datasources.tsxpackages/frontend/src/components/datasource-dialog.tsxpackages/frontend/src/components/datasource-selector.tsxpackages/frontend/src/components/mobile-selection-handles.tsxpackages/frontend/src/components/nav-main.tsxpackages/frontend/src/components/sheet-context-menu.tsxpackages/frontend/src/components/tab-bar.tsxpackages/frontend/src/hooks/use-mobile-sheet-gestures.tspackages/frontend/src/types/datasource.tspackages/frontend/src/types/worksheet.tspackages/sheet/src/index.tspackages/sheet/src/model/coordinates.tspackages/sheet/src/model/pivot/aggregate.tspackages/sheet/src/model/pivot/calculate.tspackages/sheet/src/model/pivot/group.tspackages/sheet/src/model/pivot/index.tspackages/sheet/src/model/pivot/materialize.tspackages/sheet/src/model/pivot/parse.tspackages/sheet/src/model/sheet.tspackages/sheet/src/model/shifting.tspackages/sheet/src/model/types.tspackages/sheet/src/store/memory.tspackages/sheet/src/store/readonly.tspackages/sheet/src/store/store.tspackages/sheet/src/view/cellinput.tspackages/sheet/src/view/gridcanvas.tspackages/sheet/src/view/layout.tspackages/sheet/src/view/overlay.tspackages/sheet/src/view/spreadsheet.tspackages/sheet/src/view/worksheet.tspackages/sheet/test/sheet/pivot-aggregate.test.tspackages/sheet/test/sheet/pivot-calculate.test.tspackages/sheet/test/sheet/pivot-e2e.test.tspackages/sheet/test/sheet/pivot-group.test.tspackages/sheet/test/sheet/pivot-materialize.test.tspackages/sheet/test/sheet/pivot-parse.test.tspackages/sheet/test/sheet/pivot-protection.test.tspackages/sheet/test/sheet/shifting.test.tspackages/sheet/test/store/store.test.tstasks/active/20260304-pivot-table-lessons.mdtasks/active/20260304-pivot-table-todo.mdtasks/active/20260307-pivot-test-scenarios.md
💤 Files with no reviewable changes (5)
- packages/backend/test/database.e2e-spec.ts
- packages/frontend/src/hooks/use-mobile-sheet-gestures.ts
- packages/frontend/src/types/datasource.ts
- packages/sheet/src/view/layout.ts
- packages/sheet/test/sheet/shifting.test.ts
| async loadPivotDefinition(): Promise<void> { | ||
| const def = await this.store.getPivotDefinition(); | ||
| this.pivotDefinition = def ? structuredClone(def) : undefined; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== loadPivotDefinition call sites ==="
rg -n "\bloadPivotDefinition\s*\(" packages --type ts --type tsx
echo
echo "=== Cache reload paths in Sheet ==="
rg -n "loadDimensions|loadStyles|loadMerges|loadFreezePane|loadHiddenState|loadFilterState|loadPivotDefinition" packages/sheet/src/model/sheet.tsRepository: wafflebase/wafflebase
Length of output: 1534
🏁 Script executed:
#!/bin/bash
# Check lines around 3758-3793 to identify the methods
sed -n '3750,3800p' packages/sheet/src/model/sheet.ts | head -60
echo
echo "=== Constructor initialization of pivotDefinition ==="
rg -n "constructor\s*\(" packages/sheet/src/model/sheet.ts | head -5
# Get constructor content
sed -n '/constructor\s*(/,/^\s*}/p' packages/sheet/src/model/sheet.ts | head -50Repository: wafflebase/wafflebase
Length of output: 1781
Add loadPivotDefinition() call to undo() and redo() cache reload sequences, and initialize pivotDefinition in the constructor.
The pivot mutation guards depend entirely on the pivotDefinition cache, but it is never initialized in the constructor and is never reloaded in undo() or redo(). This leaves pivot sheets vulnerable: the guards check an undefined or stale cache value and can be bypassed. Reload pivotDefinition alongside the other cached state in both methods, and ensure it is initialized during sheet bootstrap.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/sheet/src/model/sheet.ts` around lines 446 - 449, Initialize the
pivotDefinition cache in the Sheet constructor (set this.pivotDefinition to
undefined or call this.loadPivotDefinition() during bootstrap) and add calls to
await this.loadPivotDefinition() inside the existing cache-reload sequences in
both undo() and redo() so pivotDefinition is refreshed alongside the other
cached state; update references to pivotDefinition in guards to rely on the
freshly loaded cache from loadPivotDefinition (function names: constructor,
loadPivotDefinition, undo, redo, pivotDefinition, store.getPivotDefinition).
| const startRef = parseRef(startStr.toUpperCase()); | ||
| const endRef = parseRef(endStr.toUpperCase()); | ||
| const newStart = moveRef(startRef, axis, src, count, dst); | ||
| const newEnd = moveRef(endRef, axis, src, count, dst); | ||
| result += toSref(newStart) + ':' + toSref(newEnd); | ||
| } else { | ||
| const aref = parseARef(text.toUpperCase()); | ||
| const moved = moveRef(aref, axis, src, count, dst); | ||
| result += toASref({ ...moved, ...(aref.absCol ? { absCol: true } : {}), ...(aref.absRow ? { absRow: true } : {}) }); | ||
| const ref = parseRef(text.toUpperCase()); | ||
| const moved = moveRef(ref, axis, src, count, dst); | ||
| result += toSref(moved); |
There was a problem hiding this comment.
Preserve absolute reference markers when rewriting formulas.
These paths now parse with parseRef() and serialize with toSref(), which drops $ markers. That changes formulas like =$A$1+B1 into relative refs during move/shift/relocate/redirect, so copied or shifted formulas can start pointing at the wrong cells. This needs an absolute-aware rewrite path before release.
Also applies to: 172-179, 182-187, 221-233, 325-341
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/sheet/src/model/shifting.ts` around lines 80 - 88, The rewrite drops
leading '$' markers because parseRef/toSref lose absolute flags; fix by
preserving and reapplying absolute markers around moved refs: before calling
parseRef on each token (both single and range branches in the blocks using
parseRef/moveRef/toSref), detect and store the original column/row absolute
flags (presence of '$' in startStr/endStr or text), pass those flags through or
attach them to the Ref object (or extend moveRef/toSref to accept an absolute
mask), then after moveRef reserialize using a toSref variant that reintroduces
the stored absolute markers; apply the same change for all other occurrences of
parseRef→moveRef→toSref (the other blocks you noted).
- Fix COUNT to count only numeric values (spreadsheet semantics) - Fix COUNTA to treat undefined/missing cells as empty - Fix stale sourceGrid by building on-demand in refresh() - Fix Yorkie CRDT proxy issues: cell clearing fallback, manual property extraction in getPivotDefinition - Add loadPivotDefinition to undo/redo and sheet initialization - Guard pivot editor from mounting in read-only mode - Allow same source column in multiple value fields - Fix mobile selection handle anchor drift on drag - Update design doc: code fence languages, pivot protection section, total cell styling description - Fix materialize docstring to match implementation Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
design/pivot-table.md (1)
297-307:⚠️ Potential issue | 🟡 MinorThe protection section still describes the old pivot guard.
This section and the mitigation row still talk in terms of cached
pivotDefinition/isPivotSheet-style checks, but the shipped protection moved toTabMeta.kind === "pivot". Please update both spots so the doc matches the actual detection path. Based on learnings: Update design documentation indesign/when changes alter architecture, data flow, or public API behavior described in design docs.Also applies to: 422-428
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@design/pivot-table.md` around lines 297 - 307, Update the "protection" and mitigation text to reflect that pivot detection now uses TabMeta.kind === "pivot" at the tab level (not the cached Sheet.pivotDefinition / isPivotSheet guard). Replace any guidance that says to check or rely on Sheet.setData early-returns like "if (this.pivotDefinition) return" with a description that editors should check TabMeta.kind === "pivot" and note that internal materialization writes go through doc.update (which bypasses Sheet mutation methods). Also update the mitigation row to instruct consumers/guard code to consult TabMeta.kind when preventing edits or showing UI state, and remove references to the outdated cached pivotDefinition behavior (including mentions of loadPivotDefinition).packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts (1)
76-87:⚠️ Potential issue | 🔴 CriticalPrevent stale pivot metadata from leaking across tab switches.
On a source switch, this effect still runs once with the previous
definitionfrom the render that triggered the switch, so Line 83 can write the old pivot metadata into the new sheet before the load effect clears or reloads state.💡 Minimal guard
+ const prevSourceRef = useRef<{ + doc: YorkieDoc<SpreadsheetDocument> | null; + tabId: string; + } | null>(null); const definitionRef = useRef(definition); useEffect(() => { + const sourceChanged = + prevSourceRef.current?.doc !== doc || + prevSourceRef.current?.tabId !== tabId; + prevSourceRef.current = { doc, tabId }; + // Skip the initial load (when definitionRef.current transitions from null) const isInitialLoad = definitionRef.current === null && definition !== null; definitionRef.current = definition; - if (isInitialLoad || !doc || !definition) return; + if (sourceChanged || isInitialLoad || !doc || !definition) return; doc.update((root) => { const ws = root.sheets[tabId]; if (ws) ws.pivotTable = definition;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines 76 - 87, The effect can run with a stale `definition` when `tabId` changes and write old pivot metadata into the new sheet; add a lightweight guard by tracking the last-seen tab and only applying `doc.update` when the effect's `tabId` matches that last-seen value. Concretely, introduce a ref like `lastTabRef = useRef(tabId)`, check `if (lastTabRef.current !== tabId) { lastTabRef.current = tabId; definitionRef.current = definition; return; }` (keep the existing `isInitialLoad` logic), and only call `doc.update` when `lastTabRef.current === tabId`; update the ref as needed so stale `definition` from the previous tab never gets written to the new sheet in `doc.update` (references: `definitionRef`, `lastTabRef`, `useEffect`, `doc.update`, `tabId`, `definition`).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@design/pivot-table.md`:
- Around line 223-225: Update the documentation text in the "Type coercion" /
pivot table section to reflect the implemented COUNT semantics: change the
sentence that currently reads "COUNT counts all values, COUNTA counts non-empty
values" to state that "COUNT counts numeric values only (non-numeric values are
ignored), while COUNTA counts non-empty values." Keep the existing notes about
parsing cell strings to numbers and skipping non-numeric values for
SUM/AVERAGE/MIN/MAX, and ensure the terms COUNT and COUNTA are used exactly so
readers can map them to the implemented functions.
In `@packages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsx`:
- Around line 152-159: The inline Close button (IconX) in pivot-editor-panel.tsx
currently calls onClose which hides the editor with no in-tab way to re-open it;
change this to a toggle-style API and surface a reopen control in the sheet
chrome: replace the onClick handler to call a new prop like onToggleEditor (or
emit a "pivotEditorToggle" event) instead of onClose, update the parent to
implement onToggleEditor and render a persistent reopen action in the sheet
chrome (e.g., a toolbar button) that toggles the editor visibility, or
alternatively remove the IconX button entirely if you prefer only the chrome
reopen path.
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 36-74: The effect in use-pivot-table.ts only runs on doc or tabId
change and misses remote updates; inside the existing useEffect that reads the
pivot definition, subscribe to the Yorkie document remote changes via
doc.subscribe((e) => { if (e.type === "remote-change") { /* re-read pivot for
current tabId and call setDefinition like the existing pt logic */ } }) and
store/return the unsubscribe function to cleanup on unmount or dependency
change; ensure the subscription callback re-uses the same read logic (the pt
handling / readField, Array.from(...) and setDefinition) to update when
root.sheets[tabId].pivotTable mutates and only act when the affected tabId
matches the current tabId.
- Around line 225-259: The refresh callback currently mutates the Yorkie
document directly via doc.update() (in the refresh function that calls
buildSourceGrid(), calculatePivot(), materialize()) which bypasses the Store and
leaves YorkieStore.getGrid()'s cached CellIndex stale; change the implementation
to route writes through the Store API (call store.setGrid(tabId, grid) or
equivalent) or accept the store in this hook and after performing the update
call store.invalidate(tabId) so the cache is updated; replace direct writes to
root.sheets[tabId].sheet inside refresh with a call to the store method (or call
store.invalidate() immediately after the doc.update()) and keep buildSourceGrid,
calculatePivot and materialize usage intact.
---
Duplicate comments:
In `@design/pivot-table.md`:
- Around line 297-307: Update the "protection" and mitigation text to reflect
that pivot detection now uses TabMeta.kind === "pivot" at the tab level (not the
cached Sheet.pivotDefinition / isPivotSheet guard). Replace any guidance that
says to check or rely on Sheet.setData early-returns like "if
(this.pivotDefinition) return" with a description that editors should check
TabMeta.kind === "pivot" and note that internal materialization writes go
through doc.update (which bypasses Sheet mutation methods). Also update the
mitigation row to instruct consumers/guard code to consult TabMeta.kind when
preventing edits or showing UI state, and remove references to the outdated
cached pivotDefinition behavior (including mentions of loadPivotDefinition).
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 76-87: The effect can run with a stale `definition` when `tabId`
changes and write old pivot metadata into the new sheet; add a lightweight guard
by tracking the last-seen tab and only applying `doc.update` when the effect's
`tabId` matches that last-seen value. Concretely, introduce a ref like
`lastTabRef = useRef(tabId)`, check `if (lastTabRef.current !== tabId) {
lastTabRef.current = tabId; definitionRef.current = definition; return; }` (keep
the existing `isInitialLoad` logic), and only call `doc.update` when
`lastTabRef.current === tabId`; update the ref as needed so stale `definition`
from the previous tab never gets written to the new sheet in `doc.update`
(references: `definitionRef`, `lastTabRef`, `useEffect`, `doc.update`, `tabId`,
`definition`).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d9f46431-cd04-4cf8-ae4a-89e68b2cf909
📒 Files selected for processing (11)
design/pivot-table.mdpackages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsxpackages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/frontend/src/app/spreadsheet/sheet-view.tsxpackages/frontend/src/app/spreadsheet/yorkie-store.tspackages/frontend/src/components/mobile-selection-handles.tsxpackages/sheet/src/model/pivot/aggregate.tspackages/sheet/src/model/pivot/materialize.tspackages/sheet/src/model/sheet.tspackages/sheet/src/view/worksheet.tspackages/sheet/test/sheet/pivot-aggregate.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/sheet/src/model/pivot/aggregate.ts
- packages/sheet/test/sheet/pivot-aggregate.test.ts
- packages/sheet/src/model/sheet.ts
- packages/frontend/src/components/mobile-selection-handles.tsx
| <button | ||
| type="button" | ||
| className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground" | ||
| onClick={onClose} | ||
| aria-label="Close pivot editor" | ||
| > | ||
| <IconX size={16} /> | ||
| </button> |
There was a problem hiding this comment.
Closing this panel is effectively one-way on the current tab.
onClose hides the editor, but the current parent wiring only reopens it when the user switches tabs or lands on a pivot tab again. That leaves no in-tab path back to Filters / Rows / Columns / Values after an accidental close. Either remove this close affordance or add a dedicated reopen action in the sheet chrome.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/frontend/src/app/spreadsheet/pivot/pivot-editor-panel.tsx` around
lines 152 - 159, The inline Close button (IconX) in pivot-editor-panel.tsx
currently calls onClose which hides the editor with no in-tab way to re-open it;
change this to a toggle-style API and surface a reopen control in the sheet
chrome: replace the onClick handler to call a new prop like onToggleEditor (or
emit a "pivotEditorToggle" event) instead of onClose, update the parent to
implement onToggleEditor and render a persistent reopen action in the sheet
chrome (e.g., a toolbar button) that toggles the editor visibility, or
alternatively remove the IconX button entirely if you prefer only the chrome
reopen path.
| const refresh = useCallback(() => { | ||
| if (!doc || !definition) return; | ||
|
|
||
| const sourceGrid = buildSourceGrid(); | ||
| if (!sourceGrid) return; | ||
|
|
||
| const pivotResult = calculatePivot(sourceGrid, definition); | ||
| const grid = materialize(pivotResult); | ||
|
|
||
| // Write materialized cells to the pivot sheet | ||
| doc.update((root) => { | ||
| const ws = root.sheets[tabId]; | ||
| if (!ws) return; | ||
|
|
||
| // Clear existing cells. Use toJSON() to snapshot keys from the Yorkie | ||
| // CRDT proxy, which avoids "ownKeys duplicate" errors. | ||
| let keys: string[]; | ||
| try { | ||
| keys = Object.keys(ws.sheet); | ||
| } catch { | ||
| const maybeToJSON = (ws.sheet as { toJSON?: () => string }).toJSON; | ||
| keys = typeof maybeToJSON === 'function' | ||
| ? Object.keys(JSON.parse(maybeToJSON.call(ws.sheet))) | ||
| : []; | ||
| } | ||
| for (const key of keys) { | ||
| delete ws.sheet[key]; | ||
| } | ||
|
|
||
| // Write new cells | ||
| for (const [sref, cell] of grid) { | ||
| ws.sheet[sref] = cell; | ||
| } | ||
| }); | ||
| }, [doc, tabId, definition, buildSourceGrid]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file at the specified lines
wc -l packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tsRepository: wafflebase/wafflebase
Length of output: 131
🏁 Script executed:
# Look at the actual code around lines 225-259
sed -n '220,265p' packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tsRepository: wafflebase/wafflebase
Length of output: 1410
🏁 Script executed:
# Search for YorkieStore implementation to understand caching
fd -t f "yorkie-store\|YorkieStore" packages/frontend/srcRepository: wafflebase/wafflebase
Length of output: 47
🏁 Script executed:
# Look for how invalidate() is called in similar contexts
rg "invalidate\(\)" packages/frontend/src/app/spreadsheet --type ts --type tsx -B 2 -A 2Repository: wafflebase/wafflebase
Length of output: 92
🏁 Script executed:
# Search for YorkieStore definition broadly
rg "YorkieStore\|getGrid" packages/frontend/src --type ts -lRepository: wafflebase/wafflebase
Length of output: 47
🏁 Script executed:
# Look for Store-related files
fd -t f "store" packages/frontend/src | head -20Repository: wafflebase/wafflebase
Length of output: 118
🏁 Script executed:
# Search for invalidate pattern correctly
rg "invalidate" packages/frontend/src --type ts -B 2 -A 2Repository: wafflebase/wafflebase
Length of output: 7288
🏁 Script executed:
# Look at the hook imports in the pivot file to understand store context
head -50 packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tsRepository: wafflebase/wafflebase
Length of output: 1471
🏁 Script executed:
# Examine the yorkie-store implementation
cat -n packages/frontend/src/app/spreadsheet/yorkie-store.tsRepository: wafflebase/wafflebase
Length of output: 50377
🏁 Script executed:
# Look at how sheet-view invalidates the store
rg "invalidateStore|invalidate\(\)" packages/frontend/src/app/spreadsheet/sheet-view.tsx -B 5 -A 5Repository: wafflebase/wafflebase
Length of output: 490
🏁 Script executed:
# Search for how use-pivot-table is used and if store is passed
rg "usePivotTable" packages/frontend/src --type ts -B 3 -A 3Repository: wafflebase/wafflebase
Length of output: 2034
🏁 Script executed:
# Check what sheet.invalidateStore() does - look in sheet-view context
rg "invalidateStore" packages/frontend/src/app/spreadsheet --type ts -A 3 -B 3Repository: wafflebase/wafflebase
Length of output: 737
🏁 Script executed:
# Look for how the store is instantiated and passed around
rg "new YorkieStore|YorkieStore\(" packages/frontend/src --type ts -B 2 -A 2Repository: wafflebase/wafflebase
Length of output: 471
🏁 Script executed:
# Check if there's a hook that provides the store
rg "useStore|Store" packages/frontend/src/app/spreadsheet --type ts -l | head -10Repository: wafflebase/wafflebase
Length of output: 229
Route pivot writes through the Store to maintain cache consistency.
The refresh callback writes directly to root.sheets[tabId].sheet via doc.update(), but YorkieStore.getGrid() relies on a cached CellIndex. Local mutations bypass the store's dirty flag, which only updates on remote changes. This can leave the store's cache stale until the next remote update or explicit invalidation.
Use store.setGrid() instead of direct document mutation, or pass the store instance to the hook so it can call store.invalidate() after the update.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines
225 - 259, The refresh callback currently mutates the Yorkie document directly
via doc.update() (in the refresh function that calls buildSourceGrid(),
calculatePivot(), materialize()) which bypasses the Store and leaves
YorkieStore.getGrid()'s cached CellIndex stale; change the implementation to
route writes through the Store API (call store.setGrid(tabId, grid) or
equivalent) or accept the store in this hook and after performing the update
call store.invalidate(tabId) so the cache is updated; replace direct writes to
root.sheets[tabId].sheet inside refresh with a call to the store method (or call
store.invalidate() immediately after the doc.update()) and keep buildSourceGrid,
calculatePivot and materialize usage intact.
- Fix race condition: track loaded tabId to prevent stale definition from overwriting a newly switched tab's pivot config - Subscribe to remote-change events to reload pivot definition when another user modifies the configuration - Extract readPivotProxy helper to deduplicate Yorkie proxy reading - Add "Edit pivot table" button to reopen closed pivot editor panel - Update design doc COUNT semantics to say numeric-only Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts (2)
31-61: Consider consolidating proxy-reading logic withYorkieStore.getPivotDefinition.This helper duplicates the field extraction logic from
yorkie-store.ts:getPivotDefinition(lines 1205-1235 in the context snippet). While the duplication enables synchronous access within effects, consider extracting a shared utility or having the hook callstore.getPivotDefinition()where possible to reduce maintenance burden.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines 31 - 61, readPivotProxy duplicates the field-extraction logic from YorkieStore.getPivotDefinition; remove the duplication by extracting the common mapping into a shared utility (e.g., export a function like normalizePivotDefinition or mapPivotFields) and have both readPivotProxy and YorkieStore.getPivotDefinition call that utility, or else have the hook call YorkieStore.getPivotDefinition directly where synchronous access permits; update references to PivotTableDefinition, readField, and the value/filter field mappings so the single shared implementation handles rowFields, columnFields, valueFields (with aggregation), filterFields (with hiddenValues) and showTotals.
237-249: Consider handling undefined sort state explicitly.When
sortisundefined(the initial state), clicking toggle sets it to"desc". Users might expect the first toggle to produce"asc"(the default sort order per the design doc line 115). Consider:- ? { ...f, sort: f.sort === "desc" ? "asc" : "desc" } + ? { ...f, sort: f.sort === "asc" ? "desc" : "asc" }This makes the cycle:
undefined → "asc" → "desc" → "asc", matching the design doc's default'asc'behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines 237 - 249, The toggleSort handler currently maps sort with f.sort === "desc" ? "asc" : "desc", which turns undefined -> "desc"; change the toggle logic in toggleSort so an undefined sort becomes "asc" and the cycle is undefined → "asc" → "desc" → "asc" (e.g., use f.sort === "asc" ? "desc" : "asc"), updating the mapping for (def[section] as PivotField[]).map(...) inside toggleSort to set the correct initial "asc" state while keeping updateDefinition and PivotField intact.packages/frontend/src/app/spreadsheet/sheet-view.tsx (1)
885-903: Consider using async/await instead of .then() chain.The
onRefreshcallback uses a.then()chain (lines 895-898). As per coding guidelines: "Use async/await instead of .then() chains for promise handling."♻️ Proposed refactor
- onRefresh={() => { + onRefresh={async () => { const sheet = sheetRef.current; if (sheet) { sheet.invalidateStore(); - sheet.reloadDimensions().then(() => { - sheet.render(); - setSheetRenderVersion((v) => v + 1); - }); + await sheet.reloadDimensions(); + sheet.render(); + setSheetRenderVersion((v) => v + 1); } }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/src/app/spreadsheet/sheet-view.tsx` around lines 885 - 903, The onRefresh callback on the PivotEditorPanel currently uses a .then() chain; change it to an async function and use await to handle the promise from sheet.reloadDimensions() instead. Locate the onRefresh prop passed to PivotEditorPanel and modify its handler (the inline function that references sheetRef, sheet.invalidateStore(), sheet.reloadDimensions(), sheet.render(), and setSheetRenderVersion) to be async, call await sheet.reloadDimensions(), then run sheet.render() and setSheetRenderVersion((v) => v + 1); keep the existing null checks for sheet and preserve behavior otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 92-106: The subscription currently only updates when
ws?.pivotTable exists; change the doc.subscribe callback (in the useEffect that
references doc, tabId, doc.subscribe, loadedTabIdRef, setDefinition,
readPivotProxy) to also handle the case where ws?.pivotTable is missing by
clearing local state: when event.type === "remote-change" and ws?.pivotTable is
falsy, setDefinition(undefined) (or the appropriate empty value) and reset
loadedTabIdRef.current (e.g., to undefined/null) so the UI immediately reflects
a remote deletion of the pivot definition.
---
Nitpick comments:
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts`:
- Around line 31-61: readPivotProxy duplicates the field-extraction logic from
YorkieStore.getPivotDefinition; remove the duplication by extracting the common
mapping into a shared utility (e.g., export a function like
normalizePivotDefinition or mapPivotFields) and have both readPivotProxy and
YorkieStore.getPivotDefinition call that utility, or else have the hook call
YorkieStore.getPivotDefinition directly where synchronous access permits; update
references to PivotTableDefinition, readField, and the value/filter field
mappings so the single shared implementation handles rowFields, columnFields,
valueFields (with aggregation), filterFields (with hiddenValues) and showTotals.
- Around line 237-249: The toggleSort handler currently maps sort with f.sort
=== "desc" ? "asc" : "desc", which turns undefined -> "desc"; change the toggle
logic in toggleSort so an undefined sort becomes "asc" and the cycle is
undefined → "asc" → "desc" → "asc" (e.g., use f.sort === "asc" ? "desc" :
"asc"), updating the mapping for (def[section] as PivotField[]).map(...) inside
toggleSort to set the correct initial "asc" state while keeping updateDefinition
and PivotField intact.
In `@packages/frontend/src/app/spreadsheet/sheet-view.tsx`:
- Around line 885-903: The onRefresh callback on the PivotEditorPanel currently
uses a .then() chain; change it to an async function and use await to handle the
promise from sheet.reloadDimensions() instead. Locate the onRefresh prop passed
to PivotEditorPanel and modify its handler (the inline function that references
sheetRef, sheet.invalidateStore(), sheet.reloadDimensions(), sheet.render(), and
setSheetRenderVersion) to be async, call await sheet.reloadDimensions(), then
run sheet.render() and setSheetRenderVersion((v) => v + 1); keep the existing
null checks for sheet and preserve behavior otherwise.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7569aebd-79ec-4f3b-b687-dbb910e1e3ff
📒 Files selected for processing (3)
design/pivot-table.mdpackages/frontend/src/app/spreadsheet/pivot/use-pivot-table.tspackages/frontend/src/app/spreadsheet/sheet-view.tsx
| // Reload definition when remote changes arrive (e.g. another user | ||
| // modifies the pivot configuration). | ||
| useEffect(() => { | ||
| if (!doc) return; | ||
| const unsub = doc.subscribe((event) => { | ||
| if (event.type !== "remote-change") return; | ||
| const root = doc.getRoot(); | ||
| const ws = root.sheets?.[tabId]; | ||
| if (ws?.pivotTable) { | ||
| loadedTabIdRef.current = tabId; | ||
| setDefinition(readPivotProxy(ws.pivotTable)); | ||
| } | ||
| }); | ||
| return unsub; | ||
| }, [doc, tabId]); |
There was a problem hiding this comment.
Handle remote deletion of pivot definition.
The subscription only re-reads when ws?.pivotTable exists. If another user deletes the pivot table (sets pivotTable to undefined), the local state won't be cleared until the tab is switched.
🐛 Proposed fix
useEffect(() => {
if (!doc) return;
const unsub = doc.subscribe((event) => {
if (event.type !== "remote-change") return;
const root = doc.getRoot();
const ws = root.sheets?.[tabId];
if (ws?.pivotTable) {
loadedTabIdRef.current = tabId;
setDefinition(readPivotProxy(ws.pivotTable));
+ } else {
+ loadedTabIdRef.current = tabId;
+ setDefinition(null);
}
});
return unsub;
}, [doc, tabId]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/frontend/src/app/spreadsheet/pivot/use-pivot-table.ts` around lines
92 - 106, The subscription currently only updates when ws?.pivotTable exists;
change the doc.subscribe callback (in the useEffect that references doc, tabId,
doc.subscribe, loadedTabIdRef, setDefinition, readPivotProxy) to also handle the
case where ws?.pivotTable is missing by clearing local state: when event.type
=== "remote-change" and ws?.pivotTable is falsy, setDefinition(undefined) (or
the appropriate empty value) and reset loadedTabIdRef.current (e.g., to
undefined/null) so the UI immediately reflects a remote deletion of the pivot
definition.
Summary
packages/sheet/src/model/pivot/) — full pipeline: parse source data → filter → group → aggregate → materialize as real cells on a dedicated sheetsetPivotDefinition/getPivotDefinitionon all Store implementations (MemStore, YorkieStore, ReadOnlyStore) with Yorkie CRDT persistencesetData,removeData,paste) on pivot sheetsTabMeta.kindsubtype for pivot identification, distinct pivot icon, pivot tab creation from context menuDesign doc:
design/pivot-table.mdTest plan
pnpm verify:fastpasses (948 sheet + 36 frontend + 47 backend tests)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Behavior
Documentation
Tests