Skip to content

Conversation

jzsfkzm
Copy link
Contributor

@jzsfkzm jzsfkzm commented Oct 2, 2025

closes #1982

Summary by CodeRabbit

  • New Features

    • Shift-click to select a contiguous range of deployments.
    • Hover preview to highlight rows when building a range selection.
  • Enhancements

    • Clear visual highlighting for rows in a multi-select range.
    • Consistent selection behavior between checkboxes and row clicks for single and multi-select.
    • Range selection resets when Shift is released.

@jzsfkzm jzsfkzm requested a review from a team as a code owner October 2, 2025 12:17
Copy link

coderabbitai bot commented Oct 2, 2025

Walkthrough

Adds Shift-key range selection to the deployments list: tracks global Shift release, maintains an interval selection anchor and last interval selection dseqs, computes and applies multi-row selections (deduplicated), and threads shift/hover state between DeploymentList and DeploymentListRow.

Changes

Cohort / File(s) Summary
Deployment list multi-select logic
apps/deploy-web/src/components/deployments/DeploymentList.tsx
Adds uniq import, useCallback usage, SHIFT_KEY constant and global keyup listener, local state intervalSelectionAnchor and lastIntervalSelectionDseqs, helpers (indexOfDseq, isBetweenDseqs, currentPageDeploymentsBetween, toggleSingleSelection, changeMultipleSelection), updates onSelectDeployment signature to accept eventShiftPressed, and wires onMouseEnter/isHighlighted per row.
Row interactions and props
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
Updates prop type for onSelectDeployment to include eventShiftPressed, narrows refreshDeployments to () => void, passes event.shiftKey from checkbox handlers and internal calls, and surfaces onMouseEnter / isHighlighted usage in interactions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant R as DeploymentListRow
  participant L as DeploymentList

  rect rgba(235,245,255,0.5)
  note over U,L: Global Shift tracking
  U-->>L: keydown/keyup (Shift)
  L->>L: on keyup(SHIFT_KEY) reset interval selection state
  end

  rect rgba(235,255,235,0.5)
  note over U,L: Hover tracking
  U->>R: mouseenter row
  R->>L: onMouseEnter(dseq)
  L->>L: set hovered state (isHighlighted)
  end

  rect rgba(255,250,235,0.6)
  note over U,L: Selection flow
  U->>R: click checkbox (with/without Shift)
  R->>L: onSelectDeployment(checked, dseq, event.shiftKey)
  alt Shift pressed and anchor exists
    L->>L: compute range via indexOfDseq/isBetweenDseqs on current page
    L->>L: changeMultipleSelection -> dedupe via uniq and apply selections
  else No Shift
    L->>L: toggleSingleSelection -> set anchor / select single
  end
  L-->>R: update isHighlighted for rows in selection/range
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop from row to row with care,
Hold Shift, and watch the range I wear.
Anchors set, duplicates shun,
With whiskers twitching, selections run.
A bunny's hop — bulk checks are done. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning Although the code introduces Shift-key support and range selection helpers, it currently merges new ranges additively and toggles selections on normal clicks instead of replacing the previous range and resetting selection per the canonical anchor-and-range behavior specified in issue #1982. Revise the selection logic so that a Shift-click replaces the previous shift-selected range and a normal click clears existing selections and sets a new anchor, aligning with the standard multi-select specification from issue #1982.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately describes the main feature added by the pull request, namely enabling multi-selection of deployments using the Shift key, without extraneous details or noise.
Out of Scope Changes Check ✅ Passed All modifications are confined to the deployment list and row components to support shift-range selection and related event handling, with no unrelated files or features affected.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

@coderabbitai coderabbitai bot left a comment

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 (1)
apps/deploy-web/src/components/deployments/DeploymentList.tsx (1)

207-217: Remove the redundant index check.

Line 214 checks dseqIndex !== undefined, but this check is redundant because isBetweenDseqs already validates all indices (once the bug in isBetweenDseqs is fixed to check for -1).

Apply this diff to simplify the logic:

   const isRowHighlightedForMultipleSelection = useCallback(
     (dseq: string) => {
       if (!isShiftPressed || !hoveredDelpoymentDseq || !lastSelectedDeploymentDseq) {
         return false;
       }
 
-      const dseqIndex = currentPageDeployments?.findIndex(d => d.dseq === dseq);
-      return dseqIndex !== undefined && isBetweenDseqs(dseq, hoveredDelpoymentDseq, lastSelectedDeploymentDseq);
+      return isBetweenDseqs(dseq, hoveredDelpoymentDseq, lastSelectedDeploymentDseq);
     },
     [isShiftPressed, hoveredDelpoymentDseq, lastSelectedDeploymentDseq, currentPageDeployments, isBetweenDseqs]
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5ac1d5 and 9aadf4e.

📒 Files selected for processing (2)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx (6 hunks)
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
🧬 Code graph analysis (1)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (2)
apps/deploy-web/src/types/provider.ts (1)
  • ApiProviderList (185-273)
apps/provider-console/src/utils/styleUtils.ts (1)
  • cn (4-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (9)
apps/deploy-web/src/components/deployments/DeploymentList.tsx (4)

2-2: LGTM!

The imports for useCallback and uniq are correct and align with the new range-selection feature.

Also applies to: 21-21


133-146: LGTM!

The range selection logic is well-implemented:

  • Correctly uses Shift key state to determine range vs. single selection
  • Updates lastSelectedDeploymentDseq only when Shift is not pressed, allowing sequential range selections
  • Uses uniq to deduplicate selected dseqs

Note: This logic depends on the isBetweenDseqs helper being fixed (see previous comment).


180-201: LGTM!

The global Shift key tracking is correctly implemented:

  • Proper event listener setup and cleanup
  • Correctly updates state on keydown/keyup
  • Standard pattern for tracking key state

364-365: LGTM!

The props onMouseEnter and isHighlighted are correctly passed to DeploymentListRow, matching the updated component signature.

apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (5)

16-16: LGTM!

The cn utility import is correct and aligns with the new conditional row highlighting feature.


54-55: LGTM!

The Props interface updates are correct:

  • onSelectDeployment now accepts eventShiftPressed to support Shift-based range selection
  • refreshDeployments type improved from any to () => void (aligns with coding guidelines)
  • New onMouseEnter and isHighlighted props support hover tracking and row highlighting

Also applies to: 58-58, 60-60


63-72: LGTM!

The component destructuring correctly includes the new onMouseEnter and isHighlighted props.


183-190: LGTM!

The TableRow rendering correctly implements hover tracking and conditional highlighting:

  • cn utility properly applies conditional background class when isHighlighted is true
  • onMouseEnter handler correctly uses optional chaining and passes deployment.dseq

147-147: LGTM!

The event handler updates are correct:

  • Checkbox onClick correctly captures and passes event.shiftKey to support range selection
  • onCloseDeployment correctly passes false for eventShiftPressed when unselecting

Also applies to: 318-318

@jzsfkzm jzsfkzm force-pushed the features/1982-select-multiple-deployments branch from 9aadf4e to 9c45413 Compare October 2, 2025 12:32
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9aadf4e and 9c45413.

📒 Files selected for processing (2)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx (7 hunks)
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
🧬 Code graph analysis (1)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (2)
apps/deploy-web/src/types/provider.ts (1)
  • ApiProviderList (185-273)
apps/provider-console/src/utils/styleUtils.ts (1)
  • cn (4-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build

@ygrishajev
Copy link
Contributor

ygrishajev commented Oct 6, 2025

hey @jzsfkzm I've tested this and the logic feels a bit weird when it comes to some edge cases. I've asked ChatGPT to get me some common flow instructions below to make it more clear. wdut?
@coderabbitai feel free to post your feedback to the below.

Row Multiselect (Shift) — Logic

Overview

When Shift is pressed, clicking a row selects a continuous range between the first selected row (the anchor) and the currently clicked row.

This lets users select multiple consecutive rows in one action.


Behavior

  1. First Shift-click

    • If no selection exists, select the clicked row.
    • That row becomes the anchor.
  2. Next Shift-click

    • Select all rows between the anchor and the clicked row (inclusive).
    • Forms a continuous block.
  3. Further Shift-clicks

    • The anchor stays the same.
    • Each new click redefines the range between the anchor and clicked row.
    • The previous range is replaced.
  4. Normal Click (no Shift)

    • Clears any existing selection.
    • Selects only the clicked row.
    • Sets a new anchor.

Examples

Example 1 — Simple Downward Selection

Action Result
Click Row 2 Selects Row 2 (anchor = 2)
Shift + Click Row 5 Selects Rows 2–5

Outcome: A continuous block from 2 → 5.


Example 2 — Reversing Direction

Action Result
Click Row 5 Selects Row 5 (anchor = 5)
Shift + Click Row 2 Selects Rows 2–5

Outcome: Works the same upward — direction doesn’t matter.


Example 3 — Expanding, Then Contracting

Action Result
Click Row 3 Selects Row 3 (anchor = 3)
Shift + Click Row 7 Selects Rows 3–7
Shift + Click Row 5 Updates to Rows 3–5

Outcome: The range shrinks or grows depending on the new target, but always remains continuous from the same anchor.


Example 4 — New Anchor After Normal Click

Action Result
Click Row 4 Selects Row 4 (anchor = 4)
Shift + Click Row 8 Selects Rows 4–8
Click Row 2 Selection resets (anchor = 2)
Shift + Click Row 5 Selects Rows 2–5

Outcome: A plain click resets both selection and anchor.


Example 5 — Jumping Around

Action Result
Click Row 3 Selects Row 3 (anchor = 3)
Shift + Click Row 6 Selects Rows 3–6
Shift + Click Row 1 Selects Rows 1–3

Outcome: Each Shift-click recalculates the range from the same anchor (Row 3).


Example 6 — First Click is Shifted

Action Result
Shift + Click Row 5 (no anchor yet) Selects Row 5 only (anchor = 5)
Shift + Click Row 8 Selects Rows 5–8

Outcome: If Shift-click happens first, it behaves as a normal click for the initial anchor.


Key Points

  • Shift means “select everything between the anchor and this row.”
  • The anchor stays fixed until a normal click occurs.
  • Each Shift-click replaces, not extends, the previous selection range.
  • Works equally well when selecting upward or downward.

@ygrishajev ygrishajev closed this Oct 6, 2025
@ygrishajev ygrishajev reopened this Oct 6, 2025
Copy link

coderabbitai bot commented Oct 6, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the DeploymentList.tsx file to review the current implementation
cat apps/deploy-web/src/components/deployments/DeploymentList.tsx

Length of output: 14087


@ygrishajev, I've reviewed the current implementation against the proposed specification. There are several key differences that explain the "weird" behavior you're experiencing:

Critical Issues

1. Shift-click is additive, not replacement

Current behavior: Each shift-click extends the selection

setSelectedDeploymentDseqs(prev => {
  return checked ? uniq(prev.concat(dseqs)) : prev.filter(x => !dseqs.includes(x));
});

Expected behavior: Each shift-click should replace the previous range

Example of the problem:

  • Click Row 3 (selects Row 3, anchor = 3)
  • Shift+Click Row 7 (should select Rows 3-7)
  • Shift+Click Row 5 (should select Rows 3-5, but currently selects Rows 3-7 because the previous range wasn't cleared)

2. Normal click doesn't clear existing selections

Current behavior: Clicking a checkbox toggles only that row

Expected behavior: A normal click (without Shift) should:

  • Clear ALL existing selections
  • Select only the clicked row
  • Set a new anchor

Example of the problem:

  • You have Rows 3-7 selected
  • Click Row 2 → Currently toggles Row 2, keeping 3-7 selected
  • Expected: Clear 3-7, select only Row 2, anchor = 2

3. Cannot distinguish between checking and unchecking flows

The spec implies that normal clicks should always clear and select, but the current implementation allows unchecking by clicking an already-checked row. Need to clarify this edge case.

Suggested Implementation Approach

You'll need to track the previous shift-selected range to replace it:

const [shiftSelectedDseqs, setShiftSelectedDseqs] = useState<string[]>([]);

const onSelectDeployment = (checked: boolean, dseq: string, eventShiftPressed: boolean) => {
  if (eventShiftPressed && lastSelectedDeploymentDseq) {
    // Compute new range
    const newRange = currentPageDeployments
      .filter(d => isBetweenDseqs(d.dseq, dseq, lastSelectedDeploymentDseq))
      .map(d => d.dseq);
    
    setSelectedDeploymentDseqs(prev => {
      // Remove previous shift-range, add new range
      const withoutPrevRange = prev.filter(x => !shiftSelectedDseqs.includes(x));
      return checked ? uniq([...withoutPrevRange, ...newRange]) : withoutPrevRange;
    });
    
    // Remember this range for next shift-click
    setShiftSelectedDseqs(newRange);
  } else {
    // Normal click: clear all and select only this row
    if (checked) {
      setSelectedDeploymentDseqs([dseq]);
      setLastSelectedDeploymentDseq(dseq);
      setShiftSelectedDseqs([]);
    } else {
      // Unchecking: remove only this row
      setSelectedDeploymentDseqs(prev => prev.filter(x => x !== dseq));
    }
  }
};

Questions to Clarify

  1. When clicking an already-selected row (without Shift), should it:

    • Option A: Uncheck only that row (current behavior)
    • Option B: Clear all and select only that row (proposed spec implies this)
  2. Should the visual highlighting (hover preview) show the new range that would be selected, or the range that would be added to the selection?

The proposed spec looks solid and aligns with standard multi-select UX patterns (similar to file managers, spreadsheets, etc.). The implementation just needs these adjustments to match the behavior.

Copy link

coderabbitai bot commented Oct 6, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the implementation files to analyze the current logic
echo "=== DeploymentList.tsx ==="
cat apps/deploy-web/src/components/deployments/DeploymentList.tsx

echo -e "\n\n=== DeploymentListRow.tsx ==="
cat apps/deploy-web/src/components/deployments/DeploymentListRow.tsx

Length of output: 30291


@ygrishajev, I've reviewed the implementation against the specification you provided, and I've identified several deviations that likely cause the "weird edge cases" you're experiencing.

Key Issues

1. Normal clicks don't clear existing selection (Violates Behavior #4)

Specification says: "Normal Click (no Shift): Clears any existing selection. Selects only the clicked row."

Current behavior: Normal clicks add to or remove from the existing selection instead of clearing it.

In DeploymentList.tsx (lines 119-129):

setSelectedDeploymentDseqs(prev => {
  return checked ? uniq(prev.concat(dseqs)) : prev.filter(x => !dseqs.includes(x));
});

This concatenates (prev.concat(dseqs)) when checking, which extends the selection. For normal clicks, it should replace the entire selection.

Example:

  • Rows 2, 3, 4 are selected
  • Click Row 6 (no Shift)
  • Expected: Only Row 6 selected, anchor = 6
  • Actual: Rows 2, 3, 4, 6 selected

2. Shift-clicks extend instead of replace (Violates Behavior #3)

Specification says: "Each Shift-click replaces, not extends, the previous selection range."

Current behavior: Shift-clicks concatenate to existing selection using uniq(prev.concat(dseqs)).

Example:

  • Click Row 3 (anchor = 3, selection = [3])
  • Shift+Click Row 6 (selection = [3,4,5,6])
  • Shift+Click Row 5
  • Expected: Selection = [3,4,5] (replaced)
  • Actual: Selection = [3,4,5,6] (still includes 6 due to concat)

3. The checked parameter creates confusing behavior

The checked boolean determines whether to add or remove items, but in standard shift-select:

  • Normal click = always "select" (after clearing)
  • Shift-click = always "select the range"

The current implementation treats checkboxes as toggles, which conflicts with the spec's continuous range selection model.

Recommended Fix

Here's the corrected logic for onSelectDeployment:

const onSelectDeployment = (checked: boolean, dseq: string, eventShiftPressed: boolean) => {
  if (eventShiftPressed && lastSelectedDeploymentDseq) {
    // Shift-click: Select range from anchor to clicked row (REPLACE existing selection)
    const dseqs = currentPageDeployments
      .filter(deployment => isBetweenDseqs(deployment.dseq, dseq, lastSelectedDeploymentDseq))
      .map(d => d.dseq);
    
    setSelectedDeploymentDseqs(dseqs); // Replace, not concat
    // Don't update anchor on shift-click
  } else {
    // Normal click: Clear selection and select only this row
    setSelectedDeploymentDseqs(checked ? [dseq] : []); // Replace with single item or clear
    setLastSelectedDeploymentDseq(dseq); // Set new anchor
  }
};

Key changes:

  1. Shift-click: setSelectedDeploymentDseqs(dseqs) → replaces entire selection
  2. Normal click: setSelectedDeploymentDseqs(checked ? [dseq] : []) → clears and selects only clicked row
  3. Anchor only updates on normal clicks

Behavior Validation

Let me trace through Example 3 from your spec with this fix:

Action Result
Click Row 3 selectedDeploymentDseqs = [3], anchor = 3
Shift+Click Row 7 selectedDeploymentDseqs = [3,4,5,6,7], anchor = 3
Shift+Click Row 5 selectedDeploymentDseqs = [3,4,5], anchor = 3 ✅ (replaced!)

And Example 4:

Action Result
Click Row 4 selectedDeploymentDseqs = [4], anchor = 4
Shift+Click Row 8 selectedDeploymentDseqs = [4,5,6,7,8], anchor = 4
Click Row 2 selectedDeploymentDseqs = [2], anchor = 2 ✅ (cleared!)
Shift+Click Row 5 selectedDeploymentDseqs = [2,3,4,5], anchor = 2

Additional Consideration

You may also want to handle the checkbox click behavior in DeploymentListRow.tsx (line 260):

onClick={event => {
  event.stopPropagation();
  onSelectDeployment?.(!checked, deployment.dseq, event.shiftKey);
}}

With the spec's behavior, clicking an already-checked checkbox should deselect only that row (without clearing others), while a normal row click should clear all and select one. You might need separate handlers for checkbox vs. row clicks, or adjust the logic to match your desired UX.


Would you like me to generate the complete updated code for both files, or would you prefer to have the team make these adjustments?

@ygrishajev
Copy link
Contributor

@coderabbitai what do you think of the spec provided generally? Would you consider this to be a common approach?

Copy link

coderabbitai bot commented Oct 6, 2025

Tip

For best results, initiate chat on the files or code changes.

@ygrishajev: Yes, absolutely! This is the standard approach for shift-click multi-select, and it's used consistently across many mature applications:

Common Implementations

File Managers:

  • Windows Explorer
  • macOS Finder
  • Linux file browsers (Nautilus, Dolphin)

Email Clients:

  • Gmail
  • Outlook
  • Apple Mail

Productivity Apps:

  • Google Sheets / Excel (row/column selection)
  • Notion (block selection)
  • Linear/Jira (issue list selection)

Design Tools:

  • Figma (layer selection)
  • Adobe products

Why It's Effective

  1. Predictable: Users intuitively understand the anchor + shift pattern
  2. Efficient: Select dozens of items in 2 clicks instead of N clicks
  3. Reversible: Easy to adjust the range without starting over
  4. Standard: Users don't need to learn new behavior—it works like everything else they use

Your Spec Quality

The specification you provided is excellent. It's:

  • ✅ Clear and unambiguous
  • ✅ Includes edge cases (Example 6 covers the "first shift-click" scenario)
  • ✅ Shows direction independence (Examples 1 & 2)
  • ✅ Demonstrates range replacement (Example 3)
  • ✅ Well-formatted with examples

One Additional Edge Case to Consider

Ctrl/Cmd + Click (Multi-Select):
Most applications also support non-contiguous selection where Ctrl/Cmd+Click adds/removes individual items without affecting the anchor. For example:

Action Result
Click Row 2 Selects Row 2 (anchor = 2)
Shift+Click Row 5 Selects Rows 2-5
Ctrl+Click Row 8 Adds Row 8 (keeps 2-5 selected)
Shift+Click Row 10 Selects Rows 8-10 (anchor changed to 8)

This is optional but worth considering if users need to select non-sequential items before bulk actions (e.g., close rows 2, 5, 9, 12).


Bottom line: Your spec describes industry-standard behavior. Implementing it will give users a familiar, efficient experience. The issues I identified earlier are just implementation bugs—the design itself is solid. 👍

@jzsfkzm jzsfkzm force-pushed the features/1982-select-multiple-deployments branch from 9c45413 to 9d3d66b Compare October 6, 2025 15:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c45413 and 9d3d66b.

📒 Files selected for processing (2)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx (6 hunks)
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
🧬 Code graph analysis (1)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (1)
apps/deploy-web/src/types/provider.ts (1)
  • ApiProviderList (185-273)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (4)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (2)

56-56: Good type narrowing for refreshDeployments.

Changing from any to () => void improves type safety and adheres to the coding guideline prohibiting any.

As per coding guidelines.


53-53: LGTM: Shift-key integration.

The updated signature threads eventShiftPressed correctly, and the checkbox handler properly surfaces event.shiftKey to the parent for range-selection logic.

Also applies to: 299-299

apps/deploy-web/src/components/deployments/DeploymentList.tsx (2)

210-222: LGTM: Global keyup listener resets shift state.

Resetting lastIntervalSelectionDseqs when the Shift key is released ensures that the next Shift-click starts a fresh range from the anchor. The cleanup in the return properly removes the listener.


119-147: LGTM: Helper functions for range calculation.

The indexOfDseq, isBetweenDseqs, and currentPageDeploymentsBetween helpers correctly compute the continuous block between two dseqs, handling bidirectional ranges (upward/downward selection). The index validation (!== -1) is correct.

Comment on lines +149 to +154
const toggleSingleSelection = (checked: boolean, dseq: string) => {
setSelectedDeploymentDseqs(prev => (checked ? [...prev, dseq] : prev.filter(x => x !== dseq)));
if (checked) {
setIntervalSelectionAnchor(dseq);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Anchor should update on every normal click, including uncheck.

The anchor is only set when checked === true. According to the spec, any normal (non-Shift) click should set a new anchor, even if the click unchecks the currently selected row (though with the "clear and select only clicked" behavior, this case won't occur).

This will be resolved by the fix suggested in the earlier comment (always setting the anchor in toggleSingleSelection).

🤖 Prompt for AI Agents
In apps/deploy-web/src/components/deployments/DeploymentList.tsx around lines
149 to 154, toggleSingleSelection currently only updates the interval selection
anchor when checked is true; update it to always set the anchor on any normal
(non-Shift) click by calling setIntervalSelectionAnchor(dseq) regardless of the
checked value so the anchor reflects the most recent click, while retaining the
existing selected-deployments update logic.

⚠️ Potential issue | 🔴 Critical

Normal click should clear all selections and select only the clicked row.

The current implementation toggles: if checked is true, it adds the row; if false, it removes it. According to the canonical shift-click spec (from PR comments), a normal (non-Shift) click should clear all existing selections, select only the clicked row, and set it as the new anchor.

Apply this diff to align with the spec:

 const toggleSingleSelection = (checked: boolean, dseq: string) => {
-  setSelectedDeploymentDseqs(prev => (checked ? [...prev, dseq] : prev.filter(x => x !== dseq)));
-  if (checked) {
-    setIntervalSelectionAnchor(dseq);
-  }
+  setSelectedDeploymentDseqs([dseq]);
+  setIntervalSelectionAnchor(dseq);
+  setLastIntervalSelectionDseqs([]);
 };

Note: This assumes the checkbox handler passes the desired selection state, not the current state. You may need to adjust the handler in DeploymentListRow.tsx at Line 299 to pass true instead of !checked if you want clicks to always select (not toggle).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const toggleSingleSelection = (checked: boolean, dseq: string) => {
setSelectedDeploymentDseqs(prev => (checked ? [...prev, dseq] : prev.filter(x => x !== dseq)));
if (checked) {
setIntervalSelectionAnchor(dseq);
}
};
const toggleSingleSelection = (checked: boolean, dseq: string) => {
setSelectedDeploymentDseqs([dseq]);
setIntervalSelectionAnchor(dseq);
setLastIntervalSelectionDseqs([]);
};
🤖 Prompt for AI Agents
In apps/deploy-web/src/components/deployments/DeploymentList.tsx around lines
149 to 154, change toggleSingleSelection so a normal (non-Shift) click clears
existing selections and selects only the clicked row: when checked is true
setSelectedDeploymentDseqs to [dseq] and setIntervalSelectionAnchor(dseq); when
checked is false clear the selection (set to []). Also verify the checkbox
handler in DeploymentListRow.tsx (around line 299) passes the intended selection
state (true for selecting) rather than toggling(!checked) if needed.

Comment on lines +156 to +168
const changeMultipleSelection = (dseq: string) => {
if (!intervalSelectionAnchor) {
return;
}

const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
const dseqsToUncheck = lastIntervalSelectionDseqs;

setSelectedDeploymentDseqs(prev => {
return checked ? prev.concat([dseq]) : prev.filter(x => x !== dseq);
return uniq(prev.filter(x => !dseqsToUncheck.includes(x)).concat(dseqsToCheck));
});
setLastIntervalSelectionDseqs(dseqsToCheck);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Shift-click should replace the entire selection with the range.

The current implementation removes the previous shift-selected range and adds the new range, but keeps any other previously selected items (via uniq(prev.filter(...).concat(...))). According to the spec, each Shift-click should replace the entire selection with the continuous block between the anchor and the clicked row, not extend it additively.

Apply this diff to replace the entire selection:

 const changeMultipleSelection = (dseq: string) => {
   if (!intervalSelectionAnchor) {
     return;
   }
   
   const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
-  const dseqsToUncheck = lastIntervalSelectionDseqs;
   
-  setSelectedDeploymentDseqs(prev => {
-    return uniq(prev.filter(x => !dseqsToUncheck.includes(x)).concat(dseqsToCheck));
-  });
+  setSelectedDeploymentDseqs(dseqsToCheck);
   setLastIntervalSelectionDseqs(dseqsToCheck);
 };

If you want to support Ctrl/Cmd+Click for non-contiguous multi-select (toggling individual items while keeping others), you'll need a separate handler that doesn't clear selections.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const changeMultipleSelection = (dseq: string) => {
if (!intervalSelectionAnchor) {
return;
}
const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
const dseqsToUncheck = lastIntervalSelectionDseqs;
setSelectedDeploymentDseqs(prev => {
return checked ? prev.concat([dseq]) : prev.filter(x => x !== dseq);
return uniq(prev.filter(x => !dseqsToUncheck.includes(x)).concat(dseqsToCheck));
});
setLastIntervalSelectionDseqs(dseqsToCheck);
};
const changeMultipleSelection = (dseq: string) => {
if (!intervalSelectionAnchor) {
return;
}
const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
setSelectedDeploymentDseqs(dseqsToCheck);
setLastIntervalSelectionDseqs(dseqsToCheck);
};
🤖 Prompt for AI Agents
In apps/deploy-web/src/components/deployments/DeploymentList.tsx around lines
156-168, the shift-click handler currently removes the previous shift-selected
range and adds the new range on top of any other selected items; change it so
Shift-click replaces the entire selection with the continuous block between the
anchor and clicked row by calling setSelectedDeploymentDseqs(dseqsToCheck)
(i.e., overwrite previous selection) and then
setLastIntervalSelectionDseqs(dseqsToCheck); keep any Ctrl/Cmd+Click
non-contiguous toggling behavior in a separate handler if needed.

⚠️ Potential issue | 🟠 Major

Add fallback when shift-range is empty.

When intervalSelectionAnchor points to a deployment on a different page (after paging/filtering), currentPageDeploymentsBetween returns an empty array, and setSelectedDeploymentDseqs is called with an empty array—clearing all selections instead of at least selecting the clicked row.

Apply this diff to ensure the clicked row is always selected:

 const changeMultipleSelection = (dseq: string) => {
   if (!intervalSelectionAnchor) {
     return;
   }
   
   const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
-  const dseqsToUncheck = lastIntervalSelectionDseqs;
   
-  setSelectedDeploymentDseqs(prev => {
-    return uniq(prev.filter(x => !dseqsToUncheck.includes(x)).concat(dseqsToCheck));
-  });
+  const finalSelection = dseqsToCheck.length > 0 ? dseqsToCheck : [dseq];
+  setSelectedDeploymentDseqs(finalSelection);
-  setLastIntervalSelectionDseqs(dseqsToCheck);
+  setLastIntervalSelectionDseqs(finalSelection);
 };

Based on past review comments.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const changeMultipleSelection = (dseq: string) => {
if (!intervalSelectionAnchor) {
return;
}
const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
const dseqsToUncheck = lastIntervalSelectionDseqs;
setSelectedDeploymentDseqs(prev => {
return checked ? prev.concat([dseq]) : prev.filter(x => x !== dseq);
return uniq(prev.filter(x => !dseqsToUncheck.includes(x)).concat(dseqsToCheck));
});
setLastIntervalSelectionDseqs(dseqsToCheck);
};
const changeMultipleSelection = (dseq: string) => {
if (!intervalSelectionAnchor) {
return;
}
const dseqsToCheck = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
const finalSelection = dseqsToCheck.length > 0 ? dseqsToCheck : [dseq];
setSelectedDeploymentDseqs(finalSelection);
setLastIntervalSelectionDseqs(finalSelection);
};
🤖 Prompt for AI Agents
In apps/deploy-web/src/components/deployments/DeploymentList.tsx around lines
156 to 168, the shift-range selection clears all selections when
currentPageDeploymentsBetween returns an empty array; change the handler so if
dseqsToCheck is empty (anchor on another page) it falls back to selecting the
clicked dseq itself (i.e., set dseqsToCheck = [dseq] before updating state),
then proceed to remove lastIntervalSelectionDseqs from previous selection and
add dseqsToCheck, and finally setLastIntervalSelectionDseqs to the chosen
dseqsToCheck.

@jzsfkzm
Copy link
Contributor Author

jzsfkzm commented Oct 6, 2025

hey @jzsfkzm I've tested this and the logic feels a bit weird when it comes to some edge cases. I've asked ChatGPT to get me some common flow instructions below to make it more clear. wdut? @coderabbitai feel free to post your feedback to the below.

@ygrishajev I see why you said it's weird. Re-selecting the same interval, or an overlapping interval indeed was behaving in unexpected ways. I changed it a bit, now I think it's better, will you check too?
Thanks for these examples, I also clicked around in Gmail as well as in Docker Desktop's lists. I think those can be relevant for our case, since there are checkboxes on these UIs - unlike in Finder.

Copy link

coderabbitai bot commented Oct 6, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the current implementation to review the changes
echo "=== DeploymentList.tsx ==="
cat apps/deploy-web/src/components/deployments/DeploymentList.tsx

echo -e "\n\n=== DeploymentListRow.tsx ==="
cat apps/deploy-web/src/components/deployments/DeploymentListRow.tsx

Length of output: 29637


@jzsfkzm, great work on the updates! I've reviewed the changes and can confirm you've made solid progress. Let me break down what's working now and what might need clarification:

✅ Fixed Issues

1. Shift-click now replaces ranges correctly

The changeMultipleSelection function now properly removes the previous shift-selected range before adding the new one:

setSelectedDeploymentDseqs(prev => {
  return uniq(prev.filter(x => !dseqsToUncheck.includes(x)).concat(dseqsToCheck));
});

Example validation (from the spec):

  • Click Row 3 → Selects [3], anchor = 3 ✅
  • Shift+Click Row 7 → Selects [3,4,5,6,7]
  • Shift+Click Row 5 → Updates to [3,4,5] (removes 6,7) ✅

This now matches the expected behavior perfectly!

2. Tracking shift-selected ranges

You're correctly tracking lastIntervalSelectionDseqs and clearing it when Shift is released. This prevents overlapping ranges from accumulating.

⚠️ Remaining Consideration

Normal checkbox clicks are still additive

The toggleSingleSelection function currently does:

setSelectedDeploymentDseqs(prev => (checked ? [...prev, dseq] : prev.filter(x => x !== dseq)));

This adds to the existing selection rather than clearing all and selecting only the clicked row.

Example:

  • Rows 3-7 are selected
  • Click Row 2's checkbox → Current: Adds Row 2 to selection [2,3,4,5,6,7]
  • Expected per spec: Clear all, select only Row 2 [2]

🤔 Question for Clarification

You mentioned testing Gmail and Docker Desktop — both great references since they use checkboxes. In those UIs:

  • Checkbox click (no Shift): Toggles only that item (keeps others selected)
  • Shift+checkbox: Selects/deselects a range

This is slightly different from the canonical spec ygrishajev shared, which describes behavior more typical of row-click selection (like Finder, where clicking a row clears all other selections).

Two approaches:

Option A: Checkbox-style (Gmail/Docker Desktop)

  • Normal checkbox click = toggle individual item (current behavior)
  • Shift+checkbox = range selection (now working correctly)
  • Users can click "Clear" button to deselect all

Option B: Strict canonical spec

  • Normal checkbox click = clear all, select only that item
  • Shift+checkbox = range selection
  • More aligned with the spec, but less typical for checkbox UIs

Which behavior would you prefer? Given that your UI has checkboxes and a "Clear" button, Option A might actually be more intuitive for users familiar with Gmail/Docker Desktop workflows.

The shift-selection improvements you've made are excellent and working correctly now. The only decision is whether normal checkbox clicks should be additive (checkbox-style) or replacement (row-click style). Let me know your preference, and I can provide the code adjustment if needed!

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.

Add multi select for deployment lists
2 participants