Support multiple ranges per conditional formatting rule#57
Conversation
Change ConditionalFormatRule from single `range: Range` to `ranges: Range[]` to match Google Sheets behavior. Users can now specify comma-separated ranges like "A1:B10, D1:E10" in the conditional formatting panel. Includes backward-compat normalization for legacy single-range documents, a Yorkie migration script (`migrate:yorkie:cf-ranges`), and updated shift/move logic for multi-range rules. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
Caution Review failedPull request was closed or merged during review 📝 Walkthrough🚥 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 docstrings
🧪 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 92.6s
Verification: verify:integrationResult: ✅ PASS |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
docs/design/conditional-format-multi-range.md (1)
1-156: Well-structured design document covering the multi-range migration comprehensively.The document clearly outlines goals, data model changes, affected modules, migration strategy with backward compatibility, and risk mitigation. This provides excellent context for the implementation.
Minor nit at Line 115: "CLI interface" is redundant since CLI already stands for "Command Line Interface". Consider using just "CLI" or "Command-line interface".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/design/conditional-format-multi-range.md` around lines 1 - 156, Change the redundant heading "CLI interface" to either "CLI" or "Command-line interface" in the design doc near the migration script section (the block that shows usage for migrate-yorkie-cf-ranges.ts and the pnpm commands); update the heading text only (leave the example commands and script name migrate-yorkie-cf-ranges.ts unchanged).packages/backend/scripts/migrate-yorkie-cf-ranges.ts (1)
140-165: Consider checking for changes before callingdoc.update().The current pattern calls
doc.update()unconditionally and determineschangedinside the callback. This differs from the reference migration inmigrate-yorkie-worksheet-shape.ts, which parses the snapshot first and only callsdoc.update()when changes are needed.While functionally correct (the sync is skipped when
changedis false), callingdoc.update()without changes may still create an empty checkpoint in Yorkie's history. Consider restructuring to check for migration necessity before mutating:♻️ Suggested restructure to avoid unnecessary update calls
async function migrateDocument( client: Client, documentId: string, ): Promise<{ changed: boolean }> { const doc = new yorkie.Document<Record<string, unknown>>( `sheet-${documentId}`, ); await client.attach(doc, { syncMode: SyncMode.Manual }); try { - let changed = false; - doc.update((root) => { - changed = migrateConditionalFormatRanges( - root as Record<string, unknown>, - ); - }, 'Migrate conditional format range to ranges array'); + // Check if migration is needed by inspecting current state + const root = doc.getRoot() as Record<string, unknown>; + const needsMigration = checkNeedsMigration(root); + + if (!needsMigration) { + return { changed: false }; + } - if (changed) { - await client.sync(doc); - } + doc.update((root) => { + migrateConditionalFormatRanges(root as Record<string, unknown>); + }, 'Migrate conditional format range to ranges array'); + await client.sync(doc); - return { changed }; + return { changed: true }; } finally { await client.detach(doc); } } + +function checkNeedsMigration(root: Record<string, unknown>): boolean { + const sheets = root.sheets as Record<string, Record<string, unknown>> | undefined; + if (!sheets) return false; + + for (const worksheet of Object.values(sheets)) { + const rules = worksheet.conditionalFormats as Array<Record<string, unknown>> | undefined; + if (!rules) continue; + for (const rule of rules) { + if (!rule.ranges && rule.range) return true; + } + } + return false; +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/scripts/migrate-yorkie-cf-ranges.ts` around lines 140 - 165, The migrateDocument function currently calls doc.update() unconditionally which can create empty Yorkie checkpoints; instead, read the document state first (e.g., via doc.getSnapshot() or equivalent) and run migrateConditionalFormatRanges on the parsed root to determine if changes are necessary, then only call doc.update(...) and client.sync(doc) when that pre-check returns true; keep the existing client.attach/doc.detach and error flow but move the migration check outside the doc.update callback and invoke migrateConditionalFormatRanges again inside doc.update only when 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/sheet/src/model/worksheet/conditional-format.ts`:
- Around line 433-466: In moveConditionalFormatRules, after computing
movedRanges (the mapped + clampRange results for normalized.ranges) filter out
any ranges that collapsed (empty) during the remap before pushing the rule;
i.e., replace the unconditional next.push of
cloneConditionalFormatRule(normalized) with logic that builds
movedRangesFiltered = movedRanges.filter(r => r && r[0].r <= r[1].r && r[0].c <=
r[1].c) (or use the same helper used by shiftConditionalFormatRules) and only
push the cloned rule with ranges: movedRangesFiltered when
movedRangesFiltered.length > 0.
- Around line 378-411: The current map over normalized.ranges produces
shiftedRanges but never removes ranges that collapsed to zero size, so the check
shiftedRanges.length === 0 is ineffective; update the logic after mapping to
filter out collapsed ranges (where the clamped range has start > end or
otherwise degenerate) — e.g. replace the direct mapping with mapping + filtering
of clampRange results (using toRange/shiftBoundary as currently done) and then
only push the cloned rule (cloneConditionalFormatRule(normalized)) with ranges:
shiftedRanges if the filtered shiftedRanges is non-empty; use the same helper
functions (shiftBoundary, toRange, clampRange) and keep the existing next.push
structure.
---
Nitpick comments:
In `@docs/design/conditional-format-multi-range.md`:
- Around line 1-156: Change the redundant heading "CLI interface" to either
"CLI" or "Command-line interface" in the design doc near the migration script
section (the block that shows usage for migrate-yorkie-cf-ranges.ts and the pnpm
commands); update the heading text only (leave the example commands and script
name migrate-yorkie-cf-ranges.ts unchanged).
In `@packages/backend/scripts/migrate-yorkie-cf-ranges.ts`:
- Around line 140-165: The migrateDocument function currently calls doc.update()
unconditionally which can create empty Yorkie checkpoints; instead, read the
document state first (e.g., via doc.getSnapshot() or equivalent) and run
migrateConditionalFormatRanges on the parsed root to determine if changes are
necessary, then only call doc.update(...) and client.sync(doc) when that
pre-check returns true; keep the existing client.attach/doc.detach and error
flow but move the migration check outside the doc.update callback and invoke
migrateConditionalFormatRanges again inside doc.update only when needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7df22045-83a4-4b26-92d6-f64f8b407846
📒 Files selected for processing (12)
docs/design/conditional-format-multi-range.mddocs/tasks/active/20260321-cf-multi-range-lessons.mddocs/tasks/active/20260321-cf-multi-range-todo.mdpackages/backend/package.jsonpackages/backend/scripts/migrate-yorkie-cf-ranges.tspackages/backend/src/yorkie/worksheet-shape-migration.tspackages/frontend/src/app/spreadsheet/conditional-format-panel.tsxpackages/frontend/tests/app/spreadsheet/yorkie-worksheet-structure.test.tspackages/sheet/src/model/core/types.tspackages/sheet/src/model/worksheet/conditional-format.tspackages/sheet/test/model/conditional-format.test.tspackages/sheet/test/sheet/formatting.test.ts
Yorkie CRDT proxy objects don't pass Array.isArray() and require JSON serialization before assignment. Also add --yorkie-key option for documents not in the database, and use getSelectionRanges() so the CF panel populates all selected ranges on Add/Use selection. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/backend/scripts/migrate-yorkie-cf-ranges.ts (2)
81-89:--limitis silently ignored when used with--document.The
--limitoption is only effective with--all(used at line 119), but the parser accepts--limitwith--documentwithout warning. Users may expect--limitto cap the number of--documentarguments processed.Consider adding validation to reject
--limitwithout--all, or document this behavior more clearly.💡 Optional: Add validation for --limit usage
return options; } + +function validateOptions(options: CliOptions): void { + if (options.limit !== undefined && !options.processAll) { + throw new Error('--limit can only be used with --all'); + } +}Then call
validateOptions(options)before returning fromparseArgs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/scripts/migrate-yorkie-cf-ranges.ts` around lines 81 - 89, The parser currently accepts --limit into options.limit even when --document mode is used (only --all honors limit), so update parseArgs to validate combinations: after parsing arguments (before returning) call validateOptions(options) or explicitly check if options.limit is set while options.all is false and throw a clear Error (e.g. " --limit can only be used with --all") so users are rejected when using --limit with --document; reference options.limit, --all, --document, parseArgs, and validateOptions when making the change.
226-247: Consider deduplicating Yorkie keys.If a user provides both
--yorkie-key sheet-xxxand--document xxx, the same key would be processed twice. While harmless (second pass findsrangesalready set), it's redundant and could confuse the operator reviewing logs.💡 Optional: Use a Set to deduplicate keys
- const yorkieKeys: string[] = [...options.yorkieKeys]; + const yorkieKeySet = new Set<string>(options.yorkieKeys); if (options.processAll || options.documentIds.length > 0) { // ... existing code ... for (const document of documents) { - yorkieKeys.push(`sheet-${document.id}`); + yorkieKeySet.add(`sheet-${document.id}`); } } + const yorkieKeys = [...yorkieKeySet];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/backend/scripts/migrate-yorkie-cf-ranges.ts` around lines 226 - 247, The yorkieKeys array can contain duplicates when both --yorkie-key and --document are used; update the logic that builds yorkieKeys (the yorkieKeys variable populated from options.yorkieKeys and the loop that pushes `sheet-${document.id}` for each document returned by loadDocuments) to deduplicate entries before further processing (e.g., convert to a Set and back or use a Set throughout) so each yorkie key is processed only once and logs won't show redundant work.
🤖 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/backend/scripts/migrate-yorkie-cf-ranges.ts`:
- Around line 81-89: The parser currently accepts --limit into options.limit
even when --document mode is used (only --all honors limit), so update parseArgs
to validate combinations: after parsing arguments (before returning) call
validateOptions(options) or explicitly check if options.limit is set while
options.all is false and throw a clear Error (e.g. " --limit can only be used
with --all") so users are rejected when using --limit with --document; reference
options.limit, --all, --document, parseArgs, and validateOptions when making the
change.
- Around line 226-247: The yorkieKeys array can contain duplicates when both
--yorkie-key and --document are used; update the logic that builds yorkieKeys
(the yorkieKeys variable populated from options.yorkieKeys and the loop that
pushes `sheet-${document.id}` for each document returned by loadDocuments) to
deduplicate entries before further processing (e.g., convert to a Set and back
or use a Set throughout) so each yorkie key is processed only once and logs
won't show redundant work.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e2187248-b402-4de9-b80b-e5e81642355b
📒 Files selected for processing (2)
packages/backend/scripts/migrate-yorkie-cf-ranges.tspackages/frontend/src/app/spreadsheet/sheet-view.tsx
After shifting or moving, ranges that collapse to zero size should be dropped. The previous .map() preserved array length so the emptiness check was unreachable. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Summary
ConditionalFormatRulefrom singlerange: Rangetoranges: Range[]to match Google Sheets behaviorA1:B10, D1:E10in the conditional formatting panelmigrate:yorkie:cf-ranges) to convert existing documentsTest plan
pnpm verify:fastpassespnpm verify:selfpasses (all lanes green)pnpm --filter @wafflebase/backend migrate:yorkie:cf-ranges --document <id>on a test document🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tools
UI
Tests