Skip to content

Conversation

@jzsfkzm
Copy link
Contributor

@jzsfkzm jzsfkzm commented Oct 2, 2025

closes #1982

Summary by CodeRabbit

  • New Features

    • Added multi-select capability with Shift+click range selection support for deployments.
  • Refactor

    • Improved selection state management to provide more consistent and efficient multi-item selection handling across the deployments interface.

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

coderabbitai bot commented Oct 2, 2025

Walkthrough

A new useListSelection hook manages multi-item selection with range-selection support using Shift-click. Deployment components are refactored to use this hook, replacing local state. Component prop signatures updated to pass selection metadata as structured objects.

Changes

Cohort / File(s) Summary
Multi-select Hook
apps/deploy-web/src/hooks/useListSelection/useListSelection.ts, apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts
Introduces new generic useListSelection<T> hook with UseListSelectionProps<T> type. Manages selectedItemIds, intervalSelectionAnchor, and lastIntervalSelectionIds state. Provides selectItem({ id, isShiftPressed }) for single/range selection, clearSelection(), and setSelectedItemIds(). Includes comprehensive test suite verifying single selection, multi-selection, and Shift-based range selection scenarios.
Deployment List Integration
apps/deploy-web/src/components/deployments/DeploymentList.tsx
Replaces local selectedDeploymentDseqs state with useListSelection hook. Updates action flows to use selectedItemIds for close deployments and message building. Adds clearSelection() calls after successful actions. UI references and selection checks updated to use hook-provided state and methods.
Deployment Row Component
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
Updates onSelectDeployment prop signature from (isChecked: boolean, dseq: string) to ({ id, isShiftPressed }: { id: string; isShiftPressed: boolean }). Refines refreshDeployments type from any to () => void. Checkbox interaction updated to call onSelectDeployment?.({ id: deployment.dseq, isShiftPressed: event.shiftKey }) on click.

Sequence Diagram

sequenceDiagram
    participant User
    participant DeploymentList
    participant useListSelection
    participant DeploymentListRow

    User->>DeploymentListRow: Click checkbox (no Shift)
    DeploymentListRow->>DeploymentList: onSelectDeployment({ id, isShiftPressed: false })
    DeploymentList->>useListSelection: selectItem({ id, isShiftPressed: false })
    useListSelection->>useListSelection: toggleSingleSelection(id)
    useListSelection-->>DeploymentList: selectedItemIds updated
    DeploymentList-->>DeploymentListRow: checked state updated

    User->>DeploymentListRow: Click checkbox (Shift held)
    DeploymentListRow->>DeploymentList: onSelectDeployment({ id, isShiftPressed: true })
    DeploymentList->>useListSelection: selectItem({ id, isShiftPressed: true })
    useListSelection->>useListSelection: changeMultipleSelection(id)
    Note over useListSelection: Select range from<br/>intervalSelectionAnchor to id
    useListSelection-->>DeploymentList: selectedItemIds includes range
    DeploymentList-->>DeploymentListRow: checked state for range updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Rationale: New hook implementation with non-trivial range-selection logic (interval anchors, deduplication, directional range handling); multiple interdependent component updates; prop signature changes affecting multiple call sites; comprehensive test suite adds verification scope; requires careful validation of edge cases in range selection and state management.

Suggested reviewers

  • baktun14

Poem

🐰 Hopping through selections,

Shift-click brings new connections,

Ranges bloom like clover patches,

Multi-select now catches,

Every deployment in batches!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: can multi-select deployments when pressing shift" is concise, clear, and directly summarizes the main change implemented in the code. It accurately reflects the primary feature added—the ability to select multiple deployments using the Shift key—which aligns with the refactoring from local state to the useListSelection hook and the updated selection handling across the affected components.
Linked Issues Check ✅ Passed The pull request addresses the core requirement from issue #1982 by implementing Shift-click multi-select functionality for deployments. The new useListSelection hook provides range selection capabilities by tracking an intervalSelectionAnchor and computing continuous ranges between two selected rows when Shift is pressed, allowing users to select multiple sequential deployments without clicking each row individually. The implementation replaces previous ranges correctly by tracking lastIntervalSelectionIds and clearing it when appropriate, which resolves the primary issue requirement. However, the PR objectives document open questions regarding normal (non-Shift) click behavior—specifically whether such clicks should toggle single items (current Option A implementation) or clear selection and select only the clicked row (Option B per the canonical spec)—though this represents a secondary UX refinement rather than a violation of the stated issue requirement.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 196f41d and ea823c5.

📒 Files selected for processing (4)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx (5 hunks)
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (3 hunks)
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts (1 hunks)
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{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/hooks/useListSelection/useListSelection.spec.ts
**/*.{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/hooks/useListSelection/useListSelection.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: jzsfkzm
PR: akash-network/console#1994
File: apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts:58-64
Timestamp: 2025-10-17T10:04:00.909Z
Learning: In the Akash Console deployment list multi-select feature (PR #1994, apps/deploy-web/src/hooks/useListSelection), the requirement is for additive Shift+click behavior: subsequent Shift+clicks should expand/add to the existing selection rather than replacing the previous Shift range. This differs from canonical Gmail/Finder-style replacement behavior.
📚 Learning: 2025-10-17T10:04:00.909Z
Learnt from: jzsfkzm
PR: akash-network/console#1994
File: apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts:58-64
Timestamp: 2025-10-17T10:04:00.909Z
Learning: In the Akash Console deployment list multi-select feature (PR #1994, apps/deploy-web/src/hooks/useListSelection), the requirement is for additive Shift+click behavior: subsequent Shift+clicks should expand/add to the existing selection rather than replacing the previous Shift range. This differs from canonical Gmail/Finder-style replacement behavior.

Applied to files:

  • apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts
🧬 Code graph analysis (2)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (1)
apps/deploy-web/src/types/provider.ts (1)
  • ApiProviderList (185-273)
apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts (1)
apps/deploy-web/src/hooks/useListSelection/useListSelection.ts (2)
  • useListSelection (8-94)
  • UseListSelectionProps (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 (7)
apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts (4)

158-161: LGTM! Setup function follows all coding guidelines.

The setup function is correctly positioned at the bottom of the root describe block, uses an inline type definition for its parameter, avoids shared state, and doesn't specify a return type. Well done!


23-35: Test helper is well-structured and covers the use case.

The testSelection helper efficiently simulates multiple interactions and validates the final state. The use of forEach with act followed by waitFor is appropriate for this hook's synchronous state updates.


8-11: Helper function is correct and follows past feedback.

The combination of length check and expect.arrayContaining properly validates that both arrays contain the same set of items, as confirmed in previous discussions.


43-155: Comprehensive test coverage for additive shift-selection behavior.

The test suite thoroughly covers single selection, toggle behavior, shift-range selection in both directions, range expansion, and range deselection. The expectations align with the additive shift-click requirement confirmed in previous discussions.

Based on learnings

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

53-56: Excellent type safety improvements!

The prop signatures have been properly updated:

  • onSelectDeployment now uses a structured object with explicit types, improving clarity and enabling shift-key detection.
  • refreshDeployments changed from any to () => void, eliminating the unsafe type.

These changes align with TypeScript best practices and the coding guidelines.


297-300: Correct shift-key detection in checkbox handler.

The checkbox onClick handler properly:

  • Stops event propagation to prevent row click interference
  • Detects shift key state via event.shiftKey
  • Passes the structured object to onSelectDeployment

This integrates correctly with the useListSelection hook's API.


134-136: Proper programmatic deselection after closure.

Passing isShiftPressed: false when closing a deployment ensures the item is deselected without triggering range-selection logic. This is the correct approach for programmatic state updates.


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
Contributor

@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
Contributor

@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
@coderabbitai
Copy link
Contributor

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.

@coderabbitai
Copy link
Contributor

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?

@coderabbitai
Copy link
Contributor

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
Contributor

@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.

@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.

@coderabbitai
Copy link
Contributor

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!

@jzsfkzm jzsfkzm force-pushed the features/1982-select-multiple-deployments branch from 9d3d66b to 06e2c0f Compare October 13, 2025 08:37
Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
apps/deploy-web/src/components/deployments/DeploymentList.tsx (2)

149-154: Anchor should update on every normal click (and consider reset-on-click spec).

Currently anchor updates only when checked is true. Update anchor on any normal click, or adopt the “normal click clears and selects only clicked row” behavior as discussed.

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

156-168: Shift-click should replace selection and needs empty-range fallback.

Per spec, Shift-click replaces the whole selection with the continuous range. Also, when the anchor isn’t on the current page, fall back to selecting the clicked row. Proposed fix:

 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));
-  });
-  setLastIntervalSelectionDseqs(dseqsToCheck);
+  const range = currentPageDeploymentsBetween(dseq, intervalSelectionAnchor);
+  const finalSelection = range.length > 0 ? range : [dseq];
+  setSelectedDeploymentDseqs(finalSelection);
+  setLastIntervalSelectionDseqs(finalSelection);
 };

If you adopt this, uniq becomes unnecessary here.

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

21-21: Use modular lodash import (or drop it if no longer needed).

Prefer importing only uniq to avoid pulling the full lodash bundle; if you apply the Shift-click replacement above, uniq may be removable.

-import { uniq } from "lodash";
+import uniq from "lodash/uniq";

Based on learnings.


170-176: Optional: support Ctrl/Cmd+Click for non-contiguous selection.

If desired, detect metaKey/ctrlKey here to toggle individual rows without clearing selection, alongside the Shift range behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 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/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 (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 (3)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (2)

296-301: Confirm UX: normal click toggle vs. reset-and-select.

Row forwards Shift state and toggles via !checked. If you adopt the “reset-and-select on normal click” spec, this handler should always pass true and let the parent replace selection.


53-57: All onSelectDeployment call sites updated. No stale usages remain.

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

210-222: Good: keyup cleanup for Shift-range tracking.

Listener registration/cleanup looks correct.

@jzsfkzm jzsfkzm force-pushed the features/1982-select-multiple-deployments branch from 29da9d2 to 196f41d Compare October 17, 2025 09:41
Copy link
Contributor

@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

🧹 Nitpick comments (2)
apps/deploy-web/src/hooks/useListSelection/useListSelection.ts (2)

67-76: Consider implementing canonical single-click behavior.

Per the PR objectives, the canonical spec expects a normal (non-Shift) click to clear all existing selections and select only the clicked item, then set it as the new anchor. The current implementation toggles instead.

If the team decides on Option B (canonical spec), update toggleSingleSelection to clear-and-select:

-const toggleSingleSelection = useCallback(
+const selectSingleItem = useCallback(
   (id: T) => {
-    setSelectedItemIds(prev => {
-      const isAdding = !prev.includes(id);
-      if (isAdding) {
-        setIntervalSelectionAnchor(id);
-      }
-      return isAdding ? [...prev, id] : prev.filter(x => x !== id);
-    });
+    setSelectedItemIds([id]);
+    setIntervalSelectionAnchor(id);
     setLastIntervalSelectionIds([]);
   },
   []
 );

Then rename the callback in onSelectItem accordingly. If Option A (toggle) is preferred, keep the current approach but apply the fixes from the previous comment.


1-2: Consider replacing lodash uniq with native Set.

Using uniq from lodash works correctly, but for minimal bundle size you could use native JS.

-import { uniq } from "lodash";

Then at line 61:

-return uniq([...withoutLast, ...newRange]);
+return Array.from(new Set([...withoutLast, ...newRange]));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06e2c0f and 196f41d.

📒 Files selected for processing (4)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx (5 hunks)
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (3 hunks)
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts (1 hunks)
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/deploy-web/src/components/deployments/DeploymentList.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts
**/*.{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/hooks/useListSelection/useListSelection.spec.ts
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.ts
  • 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/hooks/useListSelection/useListSelection.spec.ts
  • apps/deploy-web/src/hooks/useListSelection/useListSelection.ts
  • apps/deploy-web/src/components/deployments/DeploymentListRow.tsx
🧬 Code graph analysis (2)
apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts (1)
apps/deploy-web/src/hooks/useListSelection/useListSelection.ts (2)
  • useListSelection (8-91)
  • UseListSelectionProps (4-6)
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 (3)
apps/deploy-web/src/components/deployments/DeploymentListRow.tsx (2)

56-56: Good type fix!

Replacing any with () => void improves type safety.

As per coding guidelines.


53-53: Prop signature updates look correct.

The changes correctly adopt the new useListSelection hook API, passing { id, isShiftPressed } objects instead of the old (isChecked, dseq) parameters.

Also applies to: 135-135, 297-300

apps/deploy-web/src/hooks/useListSelection/useListSelection.spec.ts (1)

27-34: Review comment is incorrect—test validates the implemented behavior (Option A, additive toggle).

The implementation clearly uses Option A (additive toggle) behavior: toggleSingleSelection checks whether an item exists and adds or removes it accordingly (line 43 in useListSelection.ts). The test case correctly validates this—the sequence of clicks [2, 3, 4, 3] produces [2, 4] because clicking item 3 twice toggles it off. All shift-click tests confirm the additive approach via changeMultipleSelection using uniq([...prev, ...newRange]).

The suggested changes would break the test while the implementation remains unchanged. There is no evidence in the codebase supporting a decision to implement Option B.

Likely an incorrect or invalid review comment.

ygrishajev
ygrishajev previously approved these changes Oct 17, 2025
stalniy
stalniy previously approved these changes Oct 17, 2025
@jzsfkzm jzsfkzm force-pushed the features/1982-select-multiple-deployments branch from 196f41d to ea823c5 Compare October 17, 2025 11:32
@ygrishajev ygrishajev merged commit 4b3e777 into akash-network:main Oct 17, 2025
62 checks passed
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

3 participants