Skip to content

feat(bulk): replace modals with anchored flyouts#38

Merged
fathiraz merged 12 commits into
mainfrom
feat/bulk-actions-flyouts
Jun 1, 2026
Merged

feat(bulk): replace modals with anchored flyouts#38
fathiraz merged 12 commits into
mainfrom
feat/bulk-actions-flyouts

Conversation

@fathiraz
Copy link
Copy Markdown
Owner

@fathiraz fathiraz commented Jun 1, 2026

Summary

  • Replace large bulk modals (edit, rename, move/reorder, mark, random assign) with anchored flyouts built on a shared BulkFlyout shell
  • Align bulk UI with GitHub-style Primer patterns: flat surfaces, design tokens, no heavy shadows
  • Reorganize the bulk actions bar and menu, add bar keyboard chords, and extend queue tracker / transfer eligibility handling

Changes

Flyouts & shell (src/ui/bulk-flyout.tsx, src/features/bulk-*-flyout.tsx)

  • New flyout components for edit, rename, reorder, mark, and random assign
  • Remove legacy modal implementations and related preview/relationship modules
  • Wire flyouts through bulk-actions-modals and the actions bar

Bulk bar & menu (src/features/bulk-actions-bar.tsx, bulk-actions-menu.tsx)

  • Restructure action entry points and shortcuts
  • Add use-bar-keyboard-chords and document chords in the keyboard help overlay

Queue & background (src/lib/queue-store.ts, src/background/*)

  • Queue phases, undo/reverse hints for bulk close, and transfer repo eligibility validation
  • Message/schema updates for new bulk flows

Tests

  • Component tests for flyouts, menu, modals, and keyboard chords; expanded queue-store and queue-tracker coverage

Test Plan

  • Load a GitHub Project table, select multiple items, open each bulk flyout from the bar and overflow menu
  • Confirm flyouts anchor correctly, use Primer styling (flat panels, tokens), and dismiss without layout bleed
  • Exercise keyboard chords on the bulk bar per keyboard help overlay
  • Run bulk edit, rename, reorder, mark, random assign, duplicate, delete, and transfer on a small selection; confirm queue tracker updates
  • Run pnpm typecheck and targeted vitest for src/features/__tests__/bulk-* and src/ui/__tests__/bulk-flyout.test.tsx

Summary by cubic

Replaced bulk-action modals with anchored, Primer-styled flyouts powered by a shared BulkFlyout. The bulk bar now supports keyboard chords, and the queue tracker adds clearer phases with Undo/Retry.

  • New Features

    • Added flyouts: Edit field, Rename, Reorder, Mark, Random Assign.
    • Shared BulkFlyout shell (simple/tabbed/drilldown) with flat Primer surfaces.
    • Bar chords: E/M/R/O/A/C/P/L/T/D, with an updated keyboard help overlay.
    • Queue tracker phases (in-flight/success/partial/error) with Retry and Undo via reverse hints.
    • Transfer modal: owner-scope search, repo eligibility tags, per-item preflight; proceed with eligible subset.
    • Delete modal: typed confirm for large runs and inline title preview.
    • Duplicate: single-step Review with inline editors and diff badges.
    • Reorder: Top/Bottom or Custom Before/After with recent targets; Random Assign: strategies with live preview and Shuffle.
  • Bug Fixes

    • Bulk Edit: awaits bulkUpdate dispatch before closing, rejects non-finite numbers, and shows a clear queue-full message.
    • Transfer: preflight returns unresolved rows on resolver errors; resets eligibility loading on early returns and merges recents for fresh eligibility/disabled state.
    • Mark: unlock correctly routed to bulkUnlock; adds lockOrPinUnknownCount so unknowns don’t pollute status fallback.
    • Queue tracker: clears phase hints on auto-dismiss and excludes unprocessed items from Undo after cancellation.
    • Reorder: disables Top/Bottom/Custom until context loads or errors resolve; guards async metadata to avoid stale overwrites.
    • Rename: {date} uses local calendar; fetch guarded with a monotonic token and cleanup to prevent spinner leaks; fixed numbering radio labeling.
    • Random Assign: “preserve existing” merges current assignees; early-return path resets loading; added helpers and tests.
    • BulkFlyout applies bodySx to the body container; Bulk Edit uses RadioGroup.Label for accessibility; shared BulkUpdateMessageData type keeps sender and background in sync.
    • Keyboard help: hides the D shortcut hint on Delete when count === 1.

Written for commit 838c46a. Summary will update on new commits.

Review in cubic

Bulk edit, rename, reorder, mark, and random-assign now open in a shared
BulkFlyout shell with Primer tokens and flat surfaces instead of full-screen
modals. The actions bar and menu are reorganized with keyboard chords and
updated queue/transfer handling in the background worker.
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Cursor auto review

Found 3 actionable issue(s) on changed lines.

Large, well-structured flyout migration with solid transfer pre-flight and bulk-close undo wiring. Actionable: the new Mark Unlock path calls bulkLock (lock mutation) and there is no unlock handler/mutation; reorder/edit flyout async loads also lack in-flight cancellation and can apply stale data after close or fast input.

Generated automatically when this PR was submitted using Cursor CLI with --model auto.

Comment thread src/features/bulk-actions-bar.tsx Outdated
Comment thread src/features/bulk-reorder-flyout.tsx
Comment thread src/features/bulk-edit-flyout.tsx
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

20 issues found across 46 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/bulk-random-assign-flyout.tsx">

<violation number="1" location="src/features/bulk-random-assign-flyout.tsx:319">
P2: The "Preserve existing assignees" checkbox is currently a no-op: its state is never used when applying random assignment.</violation>
</file>

<file name="src/features/bulk-transfer-modal.tsx">

<violation number="1" location="src/features/bulk-transfer-modal.tsx:137">
P2: When merging recents, prefer the current search result object for matching repo IDs so eligibility/disabled state stays accurate.</violation>

<violation number="2" location="src/features/bulk-transfer-modal.tsx:206">
P2: Reset `eligibilityLoading` on early-return paths in the preflight effect; otherwise the transfer button can remain stuck in a disabled "Checking…" state after target switches.</violation>
</file>

<file name="src/features/bulk-edit-flyout.tsx">

<violation number="1" location="src/features/bulk-edit-flyout.tsx:110">
P1: Handle bulkUpdate dispatch failures before marking the field applied and closing the flyout.</violation>

<violation number="2" location="src/features/bulk-edit-flyout.tsx:141">
P2: Guard async metadata responses so older searches cannot overwrite newer results.</violation>

<violation number="3" location="src/features/bulk-edit-flyout.tsx:257">
P2: This flow skips the documented “Review & Apply” step and allows immediate mutation from the value pane.</violation>
</file>

<file name="src/ui/bulk-flyout.tsx">

<violation number="1" location="src/ui/bulk-flyout.tsx:207">
P2: `bodySx` is applied to the flyout shell instead of the body container, which violates the prop contract and can cause unintended layout overrides.</violation>
</file>

<file name="src/features/keyboard-help-overlay.tsx">

<violation number="1" location="src/features/keyboard-help-overlay.tsx:114">
P3: The bar-chord row rendering duplicates the layout pattern from the shortcuts section below. Consider extracting a shared `ShortcutRow` sub-component to keep the styling in one place.</violation>
</file>

<file name="src/features/bulk-rename-flyout.tsx">

<violation number="1" location="src/features/bulk-rename-flyout.tsx:84">
P2: Guard `getItemTitles` responses against stale async completions; older requests can overwrite newer flyout state after close/reopen or selection changes.</violation>
</file>

<file name="src/features/bulk-rename-utils.ts">

<violation number="1" location="src/features/bulk-rename-utils.ts:112">
P2: `{date}` token uses UTC (`toISOString`) instead of local calendar date, which can produce the wrong day for users outside UTC.</violation>
</file>

<file name="src/features/bulk-edit-flyout-helpers.ts">

<violation number="1" location="src/features/bulk-edit-flyout-helpers.ts:47">
P2: Number validation should reject non-finite values; `Infinity` currently passes and can be submitted as a bulk field update.</violation>
</file>

<file name="src/features/bulk-actions-menu.tsx">

<violation number="1" location="src/features/bulk-actions-menu.tsx:165">
P2: The Delete menu item always shows `D`, but for single selection `D` is mapped to Duplicate, so this shortcut hint is incorrect and misleading.</violation>
</file>

<file name="src/features/bulk-mark-flyout.tsx">

<violation number="1" location="src/features/bulk-mark-flyout.tsx:210">
P1: The new `unlock` flyout option dispatches the lock mutation path, so “Unlock conversations” performs a lock operation instead of unlocking.</violation>
</file>

<file name="src/background/bulk-state.ts">

<violation number="1" location="src/background/bulk-state.ts:94">
P2: Undo targets are inferred as “not failed”, which incorrectly includes unprocessed items after cancellation. This can show an Undo action for items that were never closed.</violation>
</file>

<file name="src/lib/project-table-dom.ts">

<violation number="1" location="src/lib/project-table-dom.ts:143">
P2: This no-op prevents pin/lock uncertainty from being reflected in `unknownCount`, so the mark flyout fallback can be skipped incorrectly.</violation>
</file>

<file name="src/features/bulk-reorder-flyout.tsx">

<violation number="1" location="src/features/bulk-reorder-flyout.tsx:88">
P2: The reorder-context fetch has an out-of-order response race; stale async results can overwrite newer state and show/apply the wrong target list.</violation>

<violation number="2" location="src/features/bulk-reorder-flyout.tsx:174">
P2: Top/Bottom/Custom actions should be disabled when reorder context failed or is unresolved; currently they can dispatch a no-op reorder after fetch errors.</violation>
</file>

<file name="src/lib/queue-store.ts">

<violation number="1" location="src/lib/queue-store.ts:285">
P1: Auto-dismissed processes leave `phaseHintsMap` entries behind, which can leak stale undo/retry hints into future runs with the same `processId`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/background/field-handlers.ts Outdated
Comment thread src/features/bulk-actions-bar.tsx
Comment thread src/features/bulk-edit-flyout.tsx Outdated
Comment thread src/features/bulk-mark-flyout.tsx
Comment thread src/lib/queue-store.ts
Comment thread src/lib/project-table-dom.ts Outdated
Comment thread src/features/bulk-edit-flyout.tsx
Comment thread src/features/bulk-reorder-flyout.tsx
Comment thread src/features/bulk-reorder-flyout.tsx Outdated
Comment thread src/features/keyboard-help-overlay.tsx Outdated
Wrap resolveProjectItemIdsWithTitles in validateTransferEligibility with
try/catch so transient GraphQL errors yield per-item unresolved rows instead
of rejecting the whole message. Extract classification into
transfer-eligibility helpers and add unit tests.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/background/bulk-state.ts">

<violation number="1" location="src/background/bulk-state.ts:94">
P2: Undo targets are inferred as “not failed”, which incorrectly includes unprocessed items after cancellation. This can show an Undo action for items that were never closed.</violation>

<violation number="2" location="src/background/bulk-state.ts:327">
P1: The `bulkUnlock` message handler is registered twice, so each unlock request can run twice and emit duplicate queue updates.</violation>
</file>

<file name="src/features/bulk-random-assign-flyout.tsx">

<violation number="1" location="src/features/bulk-random-assign-flyout.tsx:319">
P2: The "Preserve existing assignees" checkbox is currently a no-op: its state is never used when applying random assignment.</violation>
</file>

<file name="src/features/bulk-transfer-modal.tsx">

<violation number="1" location="src/features/bulk-transfer-modal.tsx:137">
P2: When merging recents, prefer the current search result object for matching repo IDs so eligibility/disabled state stays accurate.</violation>

<violation number="2" location="src/features/bulk-transfer-modal.tsx:206">
P2: Reset `eligibilityLoading` on early-return paths in the preflight effect; otherwise the transfer button can remain stuck in a disabled "Checking…" state after target switches.</violation>
</file>

<file name="src/features/bulk-edit-flyout.tsx">

<violation number="1" location="src/features/bulk-edit-flyout.tsx:141">
P2: Guard async metadata responses so older searches cannot overwrite newer results.</violation>

<violation number="2" location="src/features/bulk-edit-flyout.tsx:257">
P2: This flow skips the documented “Review & Apply” step and allows immediate mutation from the value pane.</violation>
</file>

<file name="src/features/keyboard-help-overlay.tsx">

<violation number="1" location="src/features/keyboard-help-overlay.tsx:114">
P3: The bar-chord row rendering duplicates the layout pattern from the shortcuts section below. Consider extracting a shared `ShortcutRow` sub-component to keep the styling in one place.</violation>
</file>

<file name="src/features/bulk-rename-flyout.tsx">

<violation number="1" location="src/features/bulk-rename-flyout.tsx:84">
P2: Guard `getItemTitles` responses against stale async completions; older requests can overwrite newer flyout state after close/reopen or selection changes.</violation>
</file>

<file name="src/features/bulk-rename-utils.ts">

<violation number="1" location="src/features/bulk-rename-utils.ts:112">
P2: `{date}` token uses UTC (`toISOString`) instead of local calendar date, which can produce the wrong day for users outside UTC.</violation>
</file>

<file name="src/features/bulk-edit-flyout-helpers.ts">

<violation number="1" location="src/features/bulk-edit-flyout-helpers.ts:47">
P2: Number validation should reject non-finite values; `Infinity` currently passes and can be submitted as a bulk field update.</violation>
</file>

<file name="src/features/bulk-actions-menu.tsx">

<violation number="1" location="src/features/bulk-actions-menu.tsx:165">
P2: The Delete menu item always shows `D`, but for single selection `D` is mapped to Duplicate, so this shortcut hint is incorrect and misleading.</violation>
</file>

<file name="src/features/bulk-mark-flyout.tsx">

<violation number="1" location="src/features/bulk-mark-flyout.tsx:210">
P1: The new `unlock` flyout option dispatches the lock mutation path, so “Unlock conversations” performs a lock operation instead of unlocking.</violation>
</file>

<file name="src/lib/project-table-dom.ts">

<violation number="1" location="src/lib/project-table-dom.ts:143">
P2: This no-op prevents pin/lock uncertainty from being reflected in `unknownCount`, so the mark flyout fallback can be skipped incorrectly.</violation>
</file>

<file name="src/features/bulk-reorder-flyout.tsx">

<violation number="1" location="src/features/bulk-reorder-flyout.tsx:88">
P2: The reorder-context fetch has an out-of-order response race; stale async results can overwrite newer state and show/apply the wrong target list.</violation>

<violation number="2" location="src/features/bulk-reorder-flyout.tsx:174">
P2: Top/Bottom/Custom actions should be disabled when reorder context failed or is unresolved; currently they can dispatch a no-op reorder after fetch errors.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/background/bulk-state.ts
fathiraz and others added 3 commits June 1, 2026 20:08
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/background/bulk-update.ts">

<violation number="1" location="src/background/bulk-update.ts:28">
P3: The bulkUpdate payload type is duplicated locally instead of reusing the shared message contract, which increases the chance of type drift between sender and background handler.</violation>
</file>

<file name="src/features/bulk-transfer-modal.tsx">

<violation number="1" location="src/features/bulk-transfer-modal.tsx:137">
P2: When merging recents, prefer the current search result object for matching repo IDs so eligibility/disabled state stays accurate.</violation>

<violation number="2" location="src/features/bulk-transfer-modal.tsx:206">
P2: Reset `eligibilityLoading` on early-return paths in the preflight effect; otherwise the transfer button can remain stuck in a disabled "Checking…" state after target switches.</violation>
</file>

<file name="src/features/bulk-edit-flyout.tsx">

<violation number="1" location="src/features/bulk-edit-flyout.tsx:141">
P2: Guard async metadata responses so older searches cannot overwrite newer results.</violation>

<violation number="2" location="src/features/bulk-edit-flyout.tsx:257">
P2: This flow skips the documented “Review & Apply” step and allows immediate mutation from the value pane.</violation>
</file>

<file name="src/features/keyboard-help-overlay.tsx">

<violation number="1" location="src/features/keyboard-help-overlay.tsx:114">
P3: The bar-chord row rendering duplicates the layout pattern from the shortcuts section below. Consider extracting a shared `ShortcutRow` sub-component to keep the styling in one place.</violation>
</file>

<file name="src/features/bulk-edit-flyout-helpers.ts">

<violation number="1" location="src/features/bulk-edit-flyout-helpers.ts:47">
P2: Number validation should reject non-finite values; `Infinity` currently passes and can be submitted as a bulk field update.</violation>
</file>

<file name="src/features/bulk-actions-menu.tsx">

<violation number="1" location="src/features/bulk-actions-menu.tsx:165">
P2: The Delete menu item always shows `D`, but for single selection `D` is mapped to Duplicate, so this shortcut hint is incorrect and misleading.</violation>
</file>

<file name="src/features/bulk-mark-flyout.tsx">

<violation number="1" location="src/features/bulk-mark-flyout.tsx:210">
P1: The new `unlock` flyout option dispatches the lock mutation path, so “Unlock conversations” performs a lock operation instead of unlocking.</violation>
</file>

<file name="src/background/bulk-state.ts">

<violation number="1" location="src/background/bulk-state.ts:94">
P2: Undo targets are inferred as “not failed”, which incorrectly includes unprocessed items after cancellation. This can show an Undo action for items that were never closed.</violation>
</file>

<file name="src/lib/project-table-dom.ts">

<violation number="1" location="src/lib/project-table-dom.ts:143">
P2: This no-op prevents pin/lock uncertainty from being reflected in `unknownCount`, so the mark flyout fallback can be skipped incorrectly.</violation>
</file>

<file name="src/features/bulk-reorder-flyout.tsx">

<violation number="1" location="src/features/bulk-reorder-flyout.tsx:88">
P2: The reorder-context fetch has an out-of-order response race; stale async results can overwrite newer state and show/apply the wrong target list.</violation>

<violation number="2" location="src/features/bulk-reorder-flyout.tsx:174">
P2: Top/Bottom/Custom actions should be disabled when reorder context failed or is unresolved; currently they can dispatch a no-op reorder after fetch errors.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/background/bulk-update.ts Outdated
cursoragent and others added 4 commits June 1, 2026 13:17
toISOString() used UTC and could show the wrong day ahead of UTC.
Format with local year/month/day and pin a Sydney timezone test.
Honor "preserve existing" in random assign by loading per-item assignees
via getItemPreview and merging with strategy output. Guard rename title
fetches against races, fix numbering RadioGroup labeling, and add tests.
Primer requires choice groups to expose a proper group label.
Use RadioGroup.Label with the field name for single-select and iteration pickers.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 9 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/features/bulk-rename-flyout.tsx Outdated
Comment thread src/features/bulk-random-assign-flyout.tsx
Comment thread src/features/bulk-rename-flyout.tsx Outdated
- Reject non-finite numbers in bulk edit (Infinity no longer applies).
- Reset eligibilityLoading on transfer-modal early-return paths.
- Merge recent transfer destinations using current search items for
  fresh eligibility/disabled state.
- Hide the duplicate D shortcut hint on Delete when count === 1.
- Track lastCompleted in bulk close to exclude unprocessed items from
  the Undo target set after cancellation.
- Add lockOrPinUnknownCount so pin/lock uncertainty no longer pollutes
  status fallback; mark flyout uses the new counter.
- Guard async metadata responses in bulk edit, bulk reorder, and bulk
  rename to prevent stale results overwriting newer state.
- Disable Top/Bottom/Custom reorder actions when context is loading,
  errored, or unresolved.
- Refactor keyboard help overlay rows into a shared ShortcutRow.
- Share BulkUpdateMessageData type between sender and background
  handler so the contract cannot drift.
- Reset loadingExisting on random-assign early-return path.
- Replace Date.now() rename fetch token with a monotonic counter and
  ensure cleanup releases the loading spinner.
@fathiraz fathiraz merged commit c0436d1 into main Jun 1, 2026
3 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.

2 participants