Skip to content

Support multiple ranges per conditional formatting rule#57

Merged
hackerwins merged 3 commits into
mainfrom
feat/cf-multi-range
Mar 20, 2026
Merged

Support multiple ranges per conditional formatting rule#57
hackerwins merged 3 commits into
mainfrom
feat/cf-multi-range

Conversation

@hackerwins

@hackerwins hackerwins commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • 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
  • Add Yorkie migration script (migrate:yorkie:cf-ranges) to convert existing documents
  • Backward-compat normalization handles un-migrated documents seamlessly

Test plan

  • Unit tests updated and new multi-range tests added (1091 sheet tests pass)
  • Frontend worksheet-structure tests updated (97 frontend tests pass)
  • Backend migration helper updated (107 backend tests pass)
  • pnpm verify:fast passes
  • pnpm verify:self passes (all lanes green)
  • Manual test: create conditional format with comma-separated ranges in UI
  • Manual test: run pnpm --filter @wafflebase/backend migrate:yorkie:cf-ranges --document <id> on a test document

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Conditional formatting now supports multiple ranges per rule, entered as comma-separated ranges.
  • Documentation

    • Added design, implementation, and rollout docs for multi-range conditional formatting.
  • Tools

    • Added a migration script to convert legacy single-range rules to the new multi-range shape.
  • UI

    • Panel input, placeholders, and sheet selection display updated to accept and show comma-separated ranges.
  • Tests

    • Updated and added tests for multi-range matching, normalization, shifting/moving, and migration.

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]>
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: enabling multiple ranges per conditional formatting rule, which is the core feature across all code, test, and documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cf-multi-range

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 92.6s

Lane Status Duration
sheet:build ✅ pass 12.5s
verify:fast ✅ pass 45.7s
frontend:build ✅ pass 13.4s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.2s
cli:build ✅ pass 1.7s
verify:entropy ✅ pass 14.8s

Verification: verify:integration

Result: ✅ PASS

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 calling doc.update().

The current pattern calls doc.update() unconditionally and determines changed inside the callback. This differs from the reference migration in migrate-yorkie-worksheet-shape.ts, which parses the snapshot first and only calls doc.update() when changes are needed.

While functionally correct (the sync is skipped when changed is false), calling doc.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

📥 Commits

Reviewing files that changed from the base of the PR and between ff57a5e and 40321f2.

📒 Files selected for processing (12)
  • docs/design/conditional-format-multi-range.md
  • docs/tasks/active/20260321-cf-multi-range-lessons.md
  • docs/tasks/active/20260321-cf-multi-range-todo.md
  • packages/backend/package.json
  • packages/backend/scripts/migrate-yorkie-cf-ranges.ts
  • packages/backend/src/yorkie/worksheet-shape-migration.ts
  • packages/frontend/src/app/spreadsheet/conditional-format-panel.tsx
  • packages/frontend/tests/app/spreadsheet/yorkie-worksheet-structure.test.ts
  • packages/sheet/src/model/core/types.ts
  • packages/sheet/src/model/worksheet/conditional-format.ts
  • packages/sheet/test/model/conditional-format.test.ts
  • packages/sheet/test/sheet/formatting.test.ts

Comment thread packages/sheet/src/model/worksheet/conditional-format.ts Outdated
Comment thread packages/sheet/src/model/worksheet/conditional-format.ts Outdated
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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/backend/scripts/migrate-yorkie-cf-ranges.ts (2)

81-89: --limit is silently ignored when used with --document.

The --limit option is only effective with --all (used at line 119), but the parser accepts --limit with --document without warning. Users may expect --limit to cap the number of --document arguments processed.

Consider adding validation to reject --limit without --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 from parseArgs.

🤖 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-xxx and --document xxx, the same key would be processed twice. While harmless (second pass finds ranges already 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

📥 Commits

Reviewing files that changed from the base of the PR and between 40321f2 and cd1d04a.

📒 Files selected for processing (2)
  • packages/backend/scripts/migrate-yorkie-cf-ranges.ts
  • packages/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]>
@hackerwins
hackerwins merged commit 53cc2dc into main Mar 20, 2026
3 checks passed
@hackerwins
hackerwins deleted the feat/cf-multi-range branch March 20, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant