Skip to content

feat(web): add group resource pickers and improve drag UX#736

Open
ksong008 wants to merge 1 commit intodaeuniverse:mainfrom
ksong008:feat/group-picker-drag-ux
Open

feat(web): add group resource pickers and improve drag UX#736
ksong008 wants to merge 1 commit intodaeuniverse:mainfrom
ksong008:feat/group-picker-drag-ux

Conversation

@ksong008
Copy link
Copy Markdown

@ksong008 ksong008 commented Apr 16, 2026

Upstream PR Prep for daeuniverse/daed

Prepared against upstream main at commit 70e413c9ceb0ad40c33a3f2da55ec8e2c6be4ef5.

Source reference:

1. Upstream requirements from CONTRIBUTING

  • PR should target main.
  • Branch should be created from main.
  • PR should stay focused and atomic.
  • Commits should follow Conventional Commits.
  • Before opening PR, run:
    • pnpm test
    • pnpm lint
  • UI changes should include screenshots or recordings.

2. Current upstream-ready branch

Prepared branch:

  • feat/group-picker-drag-ux

Current HEAD:

  • e09d515 feat(web): add group resource pickers and improve drag UX

Current diff vs upstream/main:

  • 1 commit ahead
  • 10 changed files
  • 1,102 insertions / 182 deletions

Changed files:

  • apps/web/src/components/DraggableResourceBadge.tsx
  • apps/web/src/components/DroppableGroupCard.tsx
  • apps/web/src/components/GroupResourcePickerModal.tsx
  • apps/web/src/components/SortableGroupContent.tsx
  • apps/web/src/components/SortableResourceBadge.tsx
  • apps/web/src/i18n/locales/en.json
  • apps/web/src/i18n/locales/zh-Hans.json
  • apps/web/src/pages/Orchestrate/Group.tsx
  • apps/web/src/pages/Orchestrate/Subscription.tsx
  • apps/web/src/pages/Orchestrate/index.tsx

Notes:

  • The temporary branch-only workflow file has already been excluded.
  • This branch is much cleaner for upstream review than ci/linux-amd64v3-test.

3. Verification results run locally

Environment used:

  • local workspace Node v22.22.2
  • local workspace pnpm 10.24.0

Results:

  • pnpm install: passed
  • pnpm test: passed
  • pnpm lint: failed
  • pnpm check-types: failed

Failure details

pnpm lint

This failed before reaching project lint violations.

Observed error:

ConfigError: Config "antfu/react/setup": Key "plugins": Key "react-dom": Expected an object.

Interpretation:

  • This looks like an ESLint config / dependency compatibility issue in the current repository toolchain.
  • It does not look like a failure caused directly by this feature branch's application code.

Relevant files:

  • [eslint.config.js](/Users/Shaka/Documents/New project/daed/eslint.config.js)
  • [package.json](/Users/Shaka/Documents/New project/daed/package.json)

pnpm check-types

Observed error:

Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0.
Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.

Interpretation:

  • This is a repository TypeScript configuration compatibility issue with TypeScript 6.0.2.
  • It is not specific to the UI changes in this branch.

Relevant file:

  • [apps/web/tsconfig.json](/Users/Shaka/Documents/New project/daed/apps/web/tsconfig.json)

pnpm test

Status:

  • passed successfully

Summary:

  • workspace package tests passed
  • apps/web vitest suite passed

4. PR blockers before opening upstream PR

Remaining blockers

  1. Attach the prepared UI screenshots

This PR changes visible orchestrate behavior. Upstream guidance strongly suggests screenshots or a short recording.

Recommendation:

  • Attach the real product screenshots already prepared:
    • [2026-04-16 19 39 01)
    • [ 2026-04-16 19 39 06)
    • [2026-04-16 19 39 12)
  • Do not use the mock SVGs in tmp/ as PR attachments.
  1. Lint / type-check are not green yet

Even though these failures appear to be repository toolchain issues rather than branch-specific logic regressions, they are still part of the expected pre-PR checks in CONTRIBUTING.

Recommendation:

  • Mention the exact failures in the PR description if you proceed anyway.
  • Preferably confirm whether maintainers are already aware of these toolchain breakages on main.

No longer blockers

  • temporary workflow file
  • bad branch naming for upstream PR
  • mixed feature/testing history on the CI branch

Those have already been cleaned up in feat/group-picker-drag-ux.

5. Recommended PR scope

Recommended upstream scope:

  • add group-level node picker
  • add group-level subscription-group picker
  • compact picker layout for denser scanning
  • rename group-side subscription actions to "subscription groups"
  • make group cards collapsed by default
  • add sortable groups
  • improve drag-and-drop UX for:
    • dragging subscription groups into groups
    • dragging nodes from subscription groups into groups
    • dragging into collapsed groups
    • edge auto-scroll while dragging

6. Suggested PR title

feat(orchestrate): add group resource pickers and improve group drag-and-drop UX

Alternative:

feat(web): add group pickers, sortable groups, and better drag UX

7. Draft PR description

## Summary

This PR improves the Orchestrate group management experience by adding group-level resource pickers and tightening the drag-and-drop workflow.

## What changed

- add a group resource picker modal for nodes
- add a group resource picker modal for subscription groups
- compact the picker layout to show more items at once
- rename group-side subscription actions to "subscription groups"
- make group cards collapsed by default to reduce page height
- allow sorting groups directly in the group column
- improve drag-and-drop behavior when moving subscription groups or nodes into a group
- improve page edge auto-scrolling while dragging
- add/update i18n strings for the new group picker and group summary UI

## Verification

- [x] `pnpm install`
- [x] `pnpm test`
- [ ] `pnpm lint`
- [ ] `pnpm check-types`

### Current known verification issues

`pnpm lint` currently fails in repo config loading with:

```text
Config "antfu/react/setup": Key "plugins": Key "react-dom": Expected an object.

pnpm check-types currently fails in repo TypeScript config with:

Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0.
Specify compilerOption '"ignoreDeprecations": "6.0"' to silence this error.

These look like existing toolchain/config compatibility issues rather than failures introduced by this branch's application logic.

Manual testing

  • add nodes to a group from the picker
  • add subscription groups to a group from the picker
  • drag a subscription group into a collapsed group
  • drag a node from a subscription group into a collapsed group
  • reorder groups
  • verify edge auto-scroll while dragging

Screenshots

Suggested screenshot set:

  • [2026-04-16 19 39 01)
    • collapsed group card with reorder affordance visible
    • [ 2026-04-16 19 39 06)
    • collapsed group card with expand affordance visible
    • [2026-04-16 19 39 12)
    • expanded group card with node/subscription-group sections and add actions

## 8. Suggested execution steps

```bash
git fetch upstream
git checkout feat/group-picker-drag-ux

# re-run checks if needed
pnpm install
pnpm lint
pnpm test
pnpm check-types

Open PR from:

  • ksong008:feat/group-picker-drag-ux

Into:

  • daeuniverse:main

9. Final recommendation

Status: technically prepared, but not fully green

Meaning:

  • branch content is clean and focused enough for upstream review
  • tests passed
  • lint and type-check currently fail due to repository toolchain/config compatibility issues
  • screenshots are available

If you are okay opening the PR with explicit notes about the current lint/type-check failures, this branch is ready enough to proceed.

@ksong008 ksong008 requested a review from a team as a code owner April 16, 2026 12:06
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

This PR enhances drag-and-drop interactions for group resource management by isolating drag handles to dedicated wrapper elements, introducing an expand/collapse UI for groups, adding a reusable group resource picker modal, implementing automatic group expansion during drag operations with edge auto-scrolling, enabling group reordering, and refactoring group content rendering with custom collapse controls.

Changes

Cohort / File(s) Summary
Drag Handle Refinements
apps/web/src/components/DraggableResourceBadge.tsx, apps/web/src/components/SortableResourceBadge.tsx
Moved dragHandleProps from root badge elements to dedicated wrapper divs around GripVertical icons, isolating drag-handle behavior. Updated badge sizing (min-height, gap, padding) and simplified dragging state styling to apply opacity/shadow only when actively dragging. Increased grip icon sizes and added touch-interaction classes to handle wrappers.
Group Card Components
apps/web/src/components/DroppableGroupCard.tsx, apps/web/src/components/SortableGroupContent.tsx
Enhanced DroppableGroupCard with new props for summary display, collapse state, drag handle, and collapse toggle callback. Refactored header layout with truncation and conditionally rendered collapse/expand toggle and drag-handle UI. Replaced SortableGroupContent's Accordion with custom GroupDropZone wrapper supporting internal collapse state, keyboard interaction (Enter/Space), expand callbacks, and add-resource buttons with conditional "click-to-open" behavior when collapsed.
Group Resource Picker
apps/web/src/components/GroupResourcePickerModal.tsx
Added new reusable modal component for selecting group resources (nodes or subscriptions). Exports GroupPickerItem interface and two configured wrappers: GroupAddNodesModal and GroupAddSubscriptionsModal. Features internal query filtering, multi-selection state management, keyboard support (Enter/Space), configurable layout (node-card or subscription-chip), and async submission with reset-on-open behavior.
Internationalization
apps/web/src/i18n/locales/en.json, apps/web/src/i18n/locales/zh-Hans.json
Added actions.expand translation key and new groupPicker namespace with localized strings for picker UI (selected counts, empty states, search placeholders, action labels, and title templates with interpolation variables).
Orchestrate Page Updates
apps/web/src/pages/Orchestrate/Group.tsx
Added group drag-and-drop reordering, group expansion state tracking, auto-expand logic on hover/drag-over, group sorting support via sortedGroupIds, new picker modals for adding nodes/subscriptions to groups, refactored group card rendering into renderGroupCard() helper, and extended GroupResource component signature with dragDestinationDroppableId and hoveredGroupId props.
Subscription and DnD Context Updates
apps/web/src/pages/Orchestrate/Subscription.tsx, apps/web/src/pages/Orchestrate/index.tsx
Removed default accordion expansion in SubscriptionResource. Enhanced DragDropContext with onDragUpdate handler, added drag UI state tracking (isDragging, dragDestinationDroppableId, hoveredGroupId), implemented edge auto-scrolling via pointer listeners, added group sorting to onDragEnd, and fallback group detection using hoveredGroupIdRef when drop destination is unavailable.

Sequence Diagrams

sequenceDiagram
    participant User
    participant DragContext as DragDropContext
    participant PointerListener as Pointer Listener<br/>(Auto-Scroll)
    participant GroupCard as Group Card<br/>(Auto-Expand)
    participant Store as Group State

    User->>DragContext: Drag resource over group
    DragContext->>PointerListener: onDragUpdate(pointerPosition)
    PointerListener->>PointerListener: elementFromPoint(x, y)<br/>identify hovered group
    PointerListener->>Store: setHoveredGroupId(groupId)
    Store->>GroupCard: hoveredGroupId prop updated
    GroupCard->>GroupCard: Detect hover state
    GroupCard->>Store: setGroupExpanded(groupId, true)<br/>via onExpand callback
    GroupCard->>User: Render expanded content
    PointerListener->>User: Trigger edge auto-scroll<br/>if pointer near viewport edge
    User->>DragContext: Drop resource on group
    DragContext->>Store: Add resource to group<br/>via mutation
Loading
sequenceDiagram
    participant User
    participant Modal as GroupAddNodesModal
    participant Dialog as SelectionDialog<br/>(Internal)
    participant Filter as Filter Logic
    participant API as onSubmit<br/>Mutation

    User->>Modal: Open modal (opened=true)
    Modal->>Dialog: Render with items list
    Dialog->>Dialog: Reset selectedIds &<br/>query on open
    User->>Dialog: Type in search field
    Dialog->>Filter: Update query state
    Filter->>Filter: Filter items by substring<br/>match (title/description/meta/keywords)
    Dialog->>User: Render filtered results
    User->>Dialog: Click item or press<br/>Enter/Space
    Dialog->>Dialog: Toggle selection in<br/>selectedIds state
    User->>Dialog: Click Submit button
    Dialog->>Dialog: Validate selectedIds.length > 0
    Dialog->>API: Call onSubmit(selectedIds)
    API->>API: Execute async mutation
    API-->>Dialog: Success
    Dialog->>Dialog: Close modal &<br/>reset state
    Dialog->>User: Render closed
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Handles now dance on tiny wrapper divs,
Groups expand when hovered—what joy it gives!
Drag, drop, and sort with auto-scrolling grace,
Modals pick resources from every place. 🎯✨
The orchestrate page now flows like a dream!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely summarizes the two main changes: adding group resource pickers and improving drag UX. Both aspects are present in the changeset and directly related to the primary features being implemented.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

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

🧹 Nitpick comments (3)
apps/web/src/components/SortableGroupContent.tsx (1)

188-193: Minor: Consider simplifying effectiveExpandedSections logic.

The current logic handles collapsed early-return but the check !autoExpandValue could short-circuit before checking expandedSections.includes(autoExpandValue).

Current behavior is correct, but for clarity:

♻️ Optional simplification
 const effectiveExpandedSections = useMemo(() => {
-  if (collapsed || !autoExpandValue || expandedSections.includes(autoExpandValue)) {
+  if (collapsed) {
     return expandedSections
   }
+  if (!autoExpandValue || expandedSections.includes(autoExpandValue)) {
+    return expandedSections
+  }
   return [...expandedSections, autoExpandValue]
 }, [autoExpandValue, collapsed, expandedSections])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/SortableGroupContent.tsx` around lines 188 - 193, The
logic inside the useMemo for effectiveExpandedSections is correct but can be
simplified for clarity: first short-circuit on collapsed by returning
expandedSections, then check autoExpandValue presence and membership in
expandedSections (i.e., if !autoExpandValue ||
expandedSections.includes(autoExpandValue) return expandedSections), otherwise
return [...expandedSections, autoExpandValue]; update the
effectiveExpandedSections useMemo (and its dependency array [autoExpandValue,
collapsed, expandedSections]) to reflect this clearer conditional flow.
apps/web/src/pages/Orchestrate/index.tsx (2)

194-216: Consider extracting magic numbers as named constants.

The edge auto-scroll implementation uses several magic numbers (24, 176, 180, 320, 0.3) that would benefit from named constants for maintainability.

♻️ Optional: Extract constants
+const EDGE_SCROLL_BASE_SPEED = 24
+const EDGE_SCROLL_MAX_ADDITIONAL_SPEED = 176
+const EDGE_SCROLL_MIN_THRESHOLD = 180
+const EDGE_SCROLL_MAX_THRESHOLD = 320
+const EDGE_SCROLL_THRESHOLD_RATIO = 0.3

 const tickEdgeAutoScroll = useCallback(() => {
   autoScrollFrameRef.current = null

   if (!draggingActiveRef.current || !edgeAutoScrollEnabledRef.current || !dragPointerRef.current) return

   const viewportHeight = window.innerHeight
-  const threshold = Math.min(320, Math.max(180, Math.round(viewportHeight * 0.3)))
+  const threshold = Math.min(EDGE_SCROLL_MAX_THRESHOLD, Math.max(EDGE_SCROLL_MIN_THRESHOLD, Math.round(viewportHeight * EDGE_SCROLL_THRESHOLD_RATIO)))
   const pointerY = dragPointerRef.current.y
   let delta = 0

   if (pointerY < threshold) {
     const intensity = Math.min(1, (threshold - pointerY) / threshold)
-    delta = -Math.round(24 + intensity * intensity * 176)
+    delta = -Math.round(EDGE_SCROLL_BASE_SPEED + intensity * intensity * EDGE_SCROLL_MAX_ADDITIONAL_SPEED)
   } else if (pointerY > viewportHeight - threshold) {
     const intensity = Math.min(1, (pointerY - (viewportHeight - threshold)) / threshold)
-    delta = Math.round(24 + intensity * intensity * 176)
+    delta = Math.round(EDGE_SCROLL_BASE_SPEED + intensity * intensity * EDGE_SCROLL_MAX_ADDITIONAL_SPEED)
   }
   // ...
 }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/Orchestrate/index.tsx` around lines 194 - 216, The
tickEdgeAutoScroll function contains magic numbers; extract 24, 176, 180, 320,
and 0.3 into named constants (e.g., BASE_SCROLL_SPEED = 24,
MAX_ADDITIONAL_SCROLL = 176, MIN_THRESHOLD = 180, MAX_THRESHOLD = 320,
THRESHOLD_RATIO = 0.3) defined near the hook or top of the file, then replace
the literals inside tickEdgeAutoScroll with those constants (referencing
function name tickEdgeAutoScroll, refs like dragPointerRef.current,
edgeAutoScrollEnabledRef.current, and autoScrollFrameRef.current) to improve
readability and maintainability while keeping existing behavior.

255-261: Use only pointermove to avoid potential double-handling.

Both mousemove and pointermove events are registered. On devices that support both, this causes the handler to be called twice per movement. The pointermove event is the modern unified API that handles both mouse and touch input across all major browsers, including Safari.

♻️ Proposed simplification
-    window.addEventListener('mousemove', handlePointerMove, { passive: true })
-    window.addEventListener('pointermove', handlePointerMove, { passive: true })
+    window.addEventListener('pointermove', handlePointerMove, { passive: true })

     return () => {
-      window.removeEventListener('mousemove', handlePointerMove)
-      window.removeEventListener('pointermove', handlePointerMove)
+      window.removeEventListener('pointermove', handlePointerMove)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/Orchestrate/index.tsx` around lines 255 - 261, The event
handler handlePointerMove is being attached to both 'mousemove' and
'pointermove' causing duplicate calls; change the registration to only
window.addEventListener('pointermove', handlePointerMove, { passive: true }) and
update the cleanup to only removeEventListener('pointermove',
handlePointerMove), removing all 'mousemove' additions/removals so pointer
events are the single source of pointer movement handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/components/SortableGroupContent.tsx`:
- Around line 188-193: The logic inside the useMemo for
effectiveExpandedSections is correct but can be simplified for clarity: first
short-circuit on collapsed by returning expandedSections, then check
autoExpandValue presence and membership in expandedSections (i.e., if
!autoExpandValue || expandedSections.includes(autoExpandValue) return
expandedSections), otherwise return [...expandedSections, autoExpandValue];
update the effectiveExpandedSections useMemo (and its dependency array
[autoExpandValue, collapsed, expandedSections]) to reflect this clearer
conditional flow.

In `@apps/web/src/pages/Orchestrate/index.tsx`:
- Around line 194-216: The tickEdgeAutoScroll function contains magic numbers;
extract 24, 176, 180, 320, and 0.3 into named constants (e.g., BASE_SCROLL_SPEED
= 24, MAX_ADDITIONAL_SCROLL = 176, MIN_THRESHOLD = 180, MAX_THRESHOLD = 320,
THRESHOLD_RATIO = 0.3) defined near the hook or top of the file, then replace
the literals inside tickEdgeAutoScroll with those constants (referencing
function name tickEdgeAutoScroll, refs like dragPointerRef.current,
edgeAutoScrollEnabledRef.current, and autoScrollFrameRef.current) to improve
readability and maintainability while keeping existing behavior.
- Around line 255-261: The event handler handlePointerMove is being attached to
both 'mousemove' and 'pointermove' causing duplicate calls; change the
registration to only window.addEventListener('pointermove', handlePointerMove, {
passive: true }) and update the cleanup to only
removeEventListener('pointermove', handlePointerMove), removing all 'mousemove'
additions/removals so pointer events are the single source of pointer movement
handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa7e9ee6-ec15-460e-bb07-b343b0abcd86

📥 Commits

Reviewing files that changed from the base of the PR and between 70e413c and e09d515.

📒 Files selected for processing (10)
  • apps/web/src/components/DraggableResourceBadge.tsx
  • apps/web/src/components/DroppableGroupCard.tsx
  • apps/web/src/components/GroupResourcePickerModal.tsx
  • apps/web/src/components/SortableGroupContent.tsx
  • apps/web/src/components/SortableResourceBadge.tsx
  • apps/web/src/i18n/locales/en.json
  • apps/web/src/i18n/locales/zh-Hans.json
  • apps/web/src/pages/Orchestrate/Group.tsx
  • apps/web/src/pages/Orchestrate/Subscription.tsx
  • apps/web/src/pages/Orchestrate/index.tsx

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Orchestrate UX improvements centered on group management: group-level resource pickers, sortable group cards, and more forgiving drag-and-drop (including edge auto-scroll and better interactions with collapsed groups).

Changes:

  • Introduce group resource picker modals for adding nodes and subscription groups.
  • Make groups sortable and adjust group card layout/interaction (collapsed-by-default, summary line, drag handle).
  • Improve drag UX in Orchestrate (hover-based fallback drop, destination tracking, edge auto-scroll), plus i18n updates.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/web/src/components/DraggableResourceBadge.tsx Refines draggable badge UI with an explicit drag handle.
apps/web/src/components/DroppableGroupCard.tsx Adds collapse/expand UI, summary slot, and group reorder drag handle support.
apps/web/src/components/GroupResourcePickerModal.tsx New reusable modal for multi-select picking (nodes/subscription groups).
apps/web/src/components/SortableGroupContent.tsx Reworks group content into compact droppable zones with “add” actions.
apps/web/src/components/SortableResourceBadge.tsx Refines sortable badge styling and moves drag handle to a dedicated element.
apps/web/src/i18n/locales/en.json Adds strings for group picker UI and expand action.
apps/web/src/i18n/locales/zh-Hans.json Adds Chinese strings for group picker UI and expand action.
apps/web/src/pages/Orchestrate/Group.tsx Implements sortable/collapsible group cards and wires in picker modals + add actions.
apps/web/src/pages/Orchestrate/Subscription.tsx Adjusts accordion default open behavior in the subscription resource UI.
apps/web/src/pages/Orchestrate/index.tsx Adds edge auto-scroll, drag destination tracking, hovered-group fallback drop behavior, and group sorting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 318 to +344
const onDragEnd = (result: DropResult) => {
const { source, destination, draggableId } = result
const fallbackGroupId = hoveredGroupIdRef.current

setIsDragging(false)
setDraggingResource(null)

if (!destination) return
setDragDestinationDroppableId(null)
setHoveredGroupId(null)
hoveredGroupIdRef.current = null
edgeAutoScrollEnabledRef.current = false
stopEdgeAutoScroll()

const sourceDroppableId = source.droppableId
const destDroppableId = destination.droppableId
const destDroppableId = destination?.droppableId

if (sourceDroppableId === 'group-list' && destDroppableId === 'group-list' && destination) {
if (source.index !== destination.index) {
setGroupSortOrder(arrayMove(sortedGroupIds, source.index, destination.index))
}
return
}

if (!destination) {
if (fallbackGroupId) {
if (sourceDroppableId === 'subscription-list') {
const subId = draggableId.replace('subscription-', '')
const targetGroup = groupsQuery?.groups.find((group: GroupsQuery['groups'][number]) => group.id === fallbackGroupId)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

onDragEnd performs “fallback” mutations when destination is null, but @hello-pangea/dnd can also produce destination: null when the drag is cancelled (e.g. ESC), which would unintentionally add nodes/subscription groups to the hovered group. Guard this branch by checking result.reason === 'DROP' (or explicitly returning early on reason === 'CANCEL') before applying any mutations based on hoveredGroupIdRef.

Copilot uses AI. Check for mistakes.
Comment on lines +255 to +260
window.addEventListener('mousemove', handlePointerMove, { passive: true })
window.addEventListener('pointermove', handlePointerMove, { passive: true })

return () => {
window.removeEventListener('mousemove', handlePointerMove)
window.removeEventListener('pointermove', handlePointerMove)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The drag pointer tracking effect registers both mousemove and pointermove handlers. On browsers that dispatch compatibility mouse events after pointer events, this can double-fire and cause unnecessary state updates / elementFromPoint calls during dragging. Consider using a single input source (prefer pointermove), or feature-detect and only register one listener.

Suggested change
window.addEventListener('mousemove', handlePointerMove, { passive: true })
window.addEventListener('pointermove', handlePointerMove, { passive: true })
return () => {
window.removeEventListener('mousemove', handlePointerMove)
window.removeEventListener('pointermove', handlePointerMove)
const moveEventName = 'PointerEvent' in window ? 'pointermove' : 'mousemove'
window.addEventListener(moveEventName, handlePointerMove, { passive: true })
return () => {
window.removeEventListener(moveEventName, handlePointerMove)

Copilot uses AI. Check for mistakes.
Comment on lines +125 to +127
role="button"
tabIndex={0}
aria-pressed={checked}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The selectable item wrapper uses role="button" with aria-pressed, but the UI is semantically a multi-select checkbox list. For better screen reader behavior, prefer role="checkbox" with aria-checked (or use an actual <button>/<label> pattern and let the checkbox be the only interactive control) so the state is announced correctly.

Suggested change
role="button"
tabIndex={0}
aria-pressed={checked}
role="checkbox"
tabIndex={0}
aria-checked={checked}

Copilot uses AI. Check for mistakes.
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.

2 participants