Skip to content

feat(projects): add right-click context menu to project cards#711

Open
malthart wants to merge 2 commits intoOpenCut-app:mainfrom
malthart:feature/project-grid-context-menu
Open

feat(projects): add right-click context menu to project cards#711
malthart wants to merge 2 commits intoOpenCut-app:mainfrom
malthart:feature/project-grid-context-menu

Conversation

@malthart
Copy link

@malthart malthart commented Feb 24, 2026

Summary

Adds a right-click context menu to project cards on the projects page.

Changes

  • Add context menu with Rename, Duplicate, Info, Delete actions
  • Works in both grid and list view
  • Uses existing Radix UI ContextMenu components
  • Mirrors actions from the existing ProjectMenu dropdown

How to test

  1. Go to the Projects page
  2. Right-click on any project card (grid view) or row (list view)
  3. Verify the menu shows and actions work

Summary by CodeRabbit

  • New Features

    • Introduced a context menu on project items with Rename, Duplicate, Info, and Delete actions.
    • Added in-place dialogs for Rename, Delete confirmation, and Project Info triggered from the context menu.
    • Checkbox overlay support for grid/list items to select projects while preserving context-menu interactions.
  • Refactor

    • Centralized per-project actions into the context-menu flow for a more consistent UX.

- Add context menu with Rename, Duplicate, Info, Delete actions
- Works in both grid and list view
- Uses existing Radix UI ContextMenu components
- Mirrors actions from the existing ProjectMenu dropdown

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link

vercel bot commented Feb 24, 2026

@malthart is attempting to deploy a commit to the OpenCut OSS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds ContextMenu-based interactions for project items in both grid and list views. Introduces a ProjectContextMenuContent component and routes per-project actions (Rename, Duplicate, Info, Delete) through context-menu items and dialog handlers, replacing inline action controls.

Changes

Cohort / File(s) Summary
Context Menu & Project Actions
apps/web/src/app/projects/page.tsx
Wraps grid/list project items with ContextMenu/ContextMenuTrigger; adds ProjectContextMenuContent component and context-menu items (Rename, Duplicate, Info, Delete). Routes action handling through callbacks and per-project dialogs (rename, delete, info).
UI behavior & item rendering
apps/web/src/app/projects/page.tsx (grid/list item sections)
Refactors item rendering to support checkbox overlay visibility coordination with context-menu state and consolidates action controls into the context menu.
API surface (internal)
apps/web/src/app/projects/page.tsx
ProjectMenu signature changed: now accepts onRenameClick, onDuplicateClick, onDeleteClick, onInfoClick handlers instead of using in-component project action implementations. No exported/public API changed outside this file.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as "Projects UI"
    participant CM as "ContextMenu"
    participant Dialog as "Action Dialog"
    participant Actions as "Project Actions"

    User->>UI: Right-click (or open) project item
    UI->>CM: show menu for item
    User->>CM: select (Rename / Duplicate / Info / Delete)
    CM->>Dialog: open corresponding dialog
    User->>Dialog: confirm action
    Dialog->>Actions: invoke handler (rename/duplicate/delete/getInfo)
    Actions->>UI: update project state / return info
    UI->>User: reflect updated state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A twitch, a hop, a right-click cheer,
Menus appear where actions steer,
Rename, duplicate, info, delete —
Dialogs tidy every feat,
Projects prance, organized and near.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description does not follow the required template which explicitly states the repository is not accepting PRs except for critical bugs and requires issue discussion first. The template warning is disregarded. Acknowledge the repository's contribution policy by checking the required checkboxes in the template and confirm this feature was discussed and approved in an issue first.
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 (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a right-click context menu to project cards, which aligns with the primary objective and changes made.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
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/web/src/app/projects/page.tsx (1)

632-645: Drop the no‑op onCheckedChange (or wire it to selection).
The empty handler adds noise and can obscure the intended update path for the controlled checkbox.

🧹 Minimal cleanup
-				onCheckedChange={() => { }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 632 - 645, The Checkbox is
currently passed a no-op onCheckedChange prop which is noisy; remove the empty
onCheckedChange prop from the Checkbox component (or alternatively wire it to
the same update path) so selection state is handled only via the controlled
checked prop and handleCheckboxChange. Locate the Checkbox in page.tsx (props:
checked={isSelected}, onClick calling handleCheckboxChange) and either delete
the onCheckedChange={() => { }} line or replace it to call
handleCheckboxChange({ checked: !isSelected, shiftKey: /* derive from event if
needed */ }) so the component's handlers are consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/app/projects/page.tsx`:
- Around line 724-734: The handlers handleDuplicate and handleDeleteConfirm are
calling duplicateProjects/deleteProjects directly and must instead route
user-triggered mutations through invokeAction to ensure toasts/validation run;
update handleDuplicate to call await invokeAction(() => duplicateProjects({
editor, ids: [project.id] })) and update handleDeleteConfirm to call await
invokeAction(() => deleteProjects({ editor, ids: [project.id] })) (then close
the dialog with setIsDeleteDialogOpen(false)); keep existing editor and ids
usage and preserve async/await behavior so UI state updates remain unchanged.
- Around line 665-714: The context menu still exposes single-item actions when
multiple projects are selected; update the render logic around
ContextMenu/ContextMenuTrigger so that when isMultiSelect is true you either (a)
do not render the per-item ContextMenu/ContextMenuTrigger at all and instead
render a bulk actions menu/component, or (b) pass a different menu content
component (e.g., a BulkProjectContextMenuContent) to ContextMenu; modify the
block that renders ContextMenu/ContextMenuTrigger and ProjectContextMenuContent
(and where ProjectMenu is conditionally shown) to gate on isMultiSelect and swap
to the bulk action UI so single-item actions are not available during
multi-select.

---

Nitpick comments:
In `@apps/web/src/app/projects/page.tsx`:
- Around line 632-645: The Checkbox is currently passed a no-op onCheckedChange
prop which is noisy; remove the empty onCheckedChange prop from the Checkbox
component (or alternatively wire it to the same update path) so selection state
is handled only via the controlled checked prop and handleCheckboxChange. Locate
the Checkbox in page.tsx (props: checked={isSelected}, onClick calling
handleCheckboxChange) and either delete the onCheckedChange={() => { }} line or
replace it to call handleCheckboxChange({ checked: !isSelected, shiftKey: /*
derive from event if needed */ }) so the component's handlers are consistent.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d77e3f and 573ad57.

📒 Files selected for processing (1)
  • apps/web/src/app/projects/page.tsx

Comment on lines 665 to 714
if (!isGridView) {
return <div className="group relative">{listContent}</div>;
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="group relative">{listContent}</div>
</ContextMenuTrigger>
<ProjectContextMenuContent project={project} />
</ContextMenu>
);
}

return (
<div className="group relative">
<Link href={`/editor/${project.id}`} className="block">
{cardContent}
</Link>

{isGridView && (
<>
<Checkbox
checked={isSelected}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onCheckedChange={() => {}}
className={`absolute z-10 size-5 top-3 left-3 ${
isSelected || isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
/>
<ContextMenu>
<ContextMenuTrigger asChild>
<div className="group relative">
<Link href={`/editor/${project.id}`} className="block">
{cardContent}
</Link>

{isGridView && (
<>
<Checkbox
checked={isSelected}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onCheckedChange={() => { }}
className={`absolute z-10 size-5 top-3 left-3 ${isSelected || isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
/>

{!isMultiSelect && (
<ProjectMenu
isOpen={isDropdownOpen}
onOpenChange={setIsDropdownOpen}
project={project}
/>
{!isMultiSelect && (
<ProjectMenu
isOpen={isDropdownOpen}
onOpenChange={setIsDropdownOpen}
project={project}
/>
)}
</>
)}
</>
)}
</div>
</div>
</ContextMenuTrigger>
<ProjectContextMenuContent project={project} />
</ContextMenu>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Context menu ignores multi‑select state.
When multiple projects are selected, the per-item ProjectMenu is hidden, but the context menu still allows single-item actions. Consider gating the context menu or swapping to bulk actions in multi-select mode.

💡 Possible guard
-			<ProjectContextMenuContent project={project} />
+			{!isMultiSelect && <ProjectContextMenuContent project={project} />}
-			<ProjectContextMenuContent project={project} />
+			{!isMultiSelect && <ProjectContextMenuContent project={project} />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 665 - 714, The context menu
still exposes single-item actions when multiple projects are selected; update
the render logic around ContextMenu/ContextMenuTrigger so that when
isMultiSelect is true you either (a) do not render the per-item
ContextMenu/ContextMenuTrigger at all and instead render a bulk actions
menu/component, or (b) pass a different menu content component (e.g., a
BulkProjectContextMenuContent) to ContextMenu; modify the block that renders
ContextMenu/ContextMenuTrigger and ProjectContextMenuContent (and where
ProjectMenu is conditionally shown) to gate on isMultiSelect and swap to the
bulk action UI so single-item actions are not available during multi-select.

Comment on lines 724 to 734
const handleRename = () => setIsRenameDialogOpen(true);
const handleDuplicate = async () => {
await duplicateProjects({ editor, ids: [project.id] });
};
const handleDeleteClick = () => setIsDeleteDialogOpen(true);
const handleInfoClick = () => setIsInfoDialogOpen(true);

const handleDeleteConfirm = async () => {
await deleteProjects({ editor, ids: [project.id] });
setIsDeleteDialogOpen(false);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Route user-triggered mutations through invokeAction().
These handlers call project mutations directly, which skips the UX action layer (toasts/validation). Please route through invokeAction() (or update the shared helpers to do so) to keep behavior consistent across UI entry points.

As per coding guidelines: Use invokeAction() in components for user-triggered operations instead of directly calling editor methods, to ensure UX layer features like toasts and validation feedback are applied.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 724 - 734, The handlers
handleDuplicate and handleDeleteConfirm are calling
duplicateProjects/deleteProjects directly and must instead route user-triggered
mutations through invokeAction to ensure toasts/validation run; update
handleDuplicate to call await invokeAction(() => duplicateProjects({ editor,
ids: [project.id] })) and update handleDeleteConfirm to call await
invokeAction(() => deleteProjects({ editor, ids: [project.id] })) (then close
the dialog with setIsDeleteDialogOpen(false)); keep existing editor and ids
usage and preserve async/await behavior so UI state updates remain unchanged.

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: 3

♻️ Duplicate comments (2)
apps/web/src/app/projects/page.tsx (2)

683-731: Context menu still ignores multi-select state.

ContextMenu is always rendered regardless of isMultiSelect, exposing single-item actions while multiple projects are selected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 683 - 731, The context menu
and its trigger should be conditionally rendered based on isMultiSelect so
single-item actions are not exposed during multi-select; update the component to
skip rendering ContextMenu, ContextMenuTrigger, ProjectMenu and
ProjectContextMenuContent whenever isMultiSelect is true (or render an alternate
multi-select menu) and ensure any handlers like
onRenameClick/onDuplicateClick/onDeleteClick/onInfoClick are not wired when
isMultiSelect is active; locate the ContextMenu, ContextMenuTrigger, ProjectMenu
and ProjectContextMenuContent usage in this component and wrap or gate them with
a check on isMultiSelect to prevent single-item actions from appearing during
multi-selection.

553-561: Mutations still bypass invokeAction().

handleDuplicate and handleDeleteConfirm call duplicateProjects/deleteProjects directly, skipping toasts and validation from the UX action layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 553 - 561, handleDuplicate
and handleDeleteConfirm are calling duplicateProjects/deleteProjects directly
and bypassing the UX action layer; instead, replace those direct calls with
invokeAction calls (e.g., invokeAction('duplicateProjects', { ids: [project.id],
editor }) and invokeAction('deleteProjects', { ids: [project.id], editor })),
await the returned result/promise, handle success/failure via the invokeAction
response (so existing toasts/validation run), and still close dialogs
(setIsDeleteDialogOpen(false)) only after a successful delete; update the
functions handleDuplicate and handleDeleteConfirm to use invokeAction rather
than calling duplicateProjects/deleteProjects directly.
🧹 Nitpick comments (1)
apps/web/src/app/projects/page.tsx (1)

702-706: Prefer cn() for conditional class composition.

Both the grid Checkbox (lines 702-706) and the ProjectMenu trigger button (lines 867-870) build class strings via template literals with nested ternaries. cn() is already imported and used elsewhere in the file; using it here improves readability and avoids stray spaces.

♻️ Proposed refactor (grid Checkbox)
-								className={`absolute z-10 size-5 top-3 left-3 ${
-									isSelected || isDropdownOpen
-										? "opacity-100"
-										: "opacity-0 group-hover:opacity-100"
-								}`}
+								className={cn(
+									"absolute z-10 size-5 top-3 left-3",
+									isSelected || isDropdownOpen ? "opacity-100" : "opacity-0 group-hover:opacity-100",
+								)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 702 - 706, The className
string for the grid Checkbox (the JSX element that uses isSelected and
isDropdownOpen) and the ProjectMenu trigger button should be refactored to use
the existing cn() helper instead of the template literal with nested ternaries;
locate the element that currently sets className={`absolute z-10 size-5 top-3
left-3 ${isSelected || isDropdownOpen ? "opacity-100" : "opacity-0
group-hover:opacity-100"}`} and replace it with cn("absolute z-10 size-5 top-3
left-3", { "opacity-100": isSelected || isDropdownOpen, "opacity-0
group-hover:opacity-100": !(isSelected || isDropdownOpen) }) so the conditional
classes are composed clearly (apply the same cn() pattern to the ProjectMenu
trigger button found around the ProjectMenu component).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/app/projects/page.tsx`:
- Around line 778-783: The ContextMenuItem's onClick is calling the async
onDuplicateClick (which delegates to handleDuplicate / duplicateProjects)
without handling rejections; update the click handler so any promise rejection
is caught—either by wrapping the call in an async function with try/catch (e.g.,
async () => { try { await onDuplicateClick(); } catch (e) { /* show toast/log */
} }) or by calling onDuplicateClick().catch(...) to consume errors; reference
ContextMenuItem, onDuplicateClick, handleDuplicate and duplicateProjects when
making the change to ensure the thrown error from duplicateProjects is handled
locally.
- Around line 845-848: handleDuplicate currently calls onDuplicateClick()
without awaiting it and immediately closes the dropdown via onOpenChange(false),
which swallows any async errors; change handleDuplicate to await the async
onDuplicateClick() call, only call onOpenChange(false) after it resolves, and
add try/catch around the await to handle and surface any rejection (e.g., show
an error toast or set an error state) so failures in onDuplicateClick are not
silently ignored; update references in the same scope (handleDuplicate,
onDuplicateClick, onOpenChange) accordingly.
- Around line 692-707: Add a keyboard handler to the Checkbox so keyboard users
can trigger the same shift-select logic as mouse users: implement onKeyDown (or
onKeyUp) on the Checkbox component and when the key is Enter or Space call
handleCheckboxChange with { checked: !isSelected, shiftKey: event.shiftKey } to
mirror the existing onClick behavior; keep the existing onClick and existing
className logic (isSelected, isDropdownOpen) and ensure the handler uses the
same symbols: Checkbox, handleCheckboxChange, isSelected, isDropdownOpen.

---

Duplicate comments:
In `@apps/web/src/app/projects/page.tsx`:
- Around line 683-731: The context menu and its trigger should be conditionally
rendered based on isMultiSelect so single-item actions are not exposed during
multi-select; update the component to skip rendering ContextMenu,
ContextMenuTrigger, ProjectMenu and ProjectContextMenuContent whenever
isMultiSelect is true (or render an alternate multi-select menu) and ensure any
handlers like onRenameClick/onDuplicateClick/onDeleteClick/onInfoClick are not
wired when isMultiSelect is active; locate the ContextMenu, ContextMenuTrigger,
ProjectMenu and ProjectContextMenuContent usage in this component and wrap or
gate them with a check on isMultiSelect to prevent single-item actions from
appearing during multi-selection.
- Around line 553-561: handleDuplicate and handleDeleteConfirm are calling
duplicateProjects/deleteProjects directly and bypassing the UX action layer;
instead, replace those direct calls with invokeAction calls (e.g.,
invokeAction('duplicateProjects', { ids: [project.id], editor }) and
invokeAction('deleteProjects', { ids: [project.id], editor })), await the
returned result/promise, handle success/failure via the invokeAction response
(so existing toasts/validation run), and still close dialogs
(setIsDeleteDialogOpen(false)) only after a successful delete; update the
functions handleDuplicate and handleDeleteConfirm to use invokeAction rather
than calling duplicateProjects/deleteProjects directly.

---

Nitpick comments:
In `@apps/web/src/app/projects/page.tsx`:
- Around line 702-706: The className string for the grid Checkbox (the JSX
element that uses isSelected and isDropdownOpen) and the ProjectMenu trigger
button should be refactored to use the existing cn() helper instead of the
template literal with nested ternaries; locate the element that currently sets
className={`absolute z-10 size-5 top-3 left-3 ${isSelected || isDropdownOpen ?
"opacity-100" : "opacity-0 group-hover:opacity-100"}`} and replace it with
cn("absolute z-10 size-5 top-3 left-3", { "opacity-100": isSelected ||
isDropdownOpen, "opacity-0 group-hover:opacity-100": !(isSelected ||
isDropdownOpen) }) so the conditional classes are composed clearly (apply the
same cn() pattern to the ProjectMenu trigger button found around the ProjectMenu
component).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 573ad57 and 097891b.

📒 Files selected for processing (1)
  • apps/web/src/app/projects/page.tsx

Comment on lines +692 to +707
<Checkbox
checked={isSelected}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onCheckedChange={() => {}}
className={`absolute z-10 size-5 top-3 left-3 ${
isSelected || isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Grid Checkbox onClick lacks a keyboard companion handler.

Per coding guidelines, every onClick must be accompanied by at least one of onKeyUp, onKeyDown, or onKeyPress. The custom shift-select logic inside handleCheckboxChange is only reachable via mouse click.

🛠️ Proposed fix
 					onClick={(event) => {
 						handleCheckboxChange({
 							checked: !isSelected,
 							shiftKey: event.shiftKey,
 						});
 					}}
+					onKeyDown={(event) => {
+						if (event.key === " " || event.key === "Enter") {
+							handleCheckboxChange({
+								checked: !isSelected,
+								shiftKey: event.shiftKey,
+							});
+						}
+					}}

As per coding guidelines: "Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress."

📝 Committable suggestion

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

Suggested change
<Checkbox
checked={isSelected}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onCheckedChange={() => {}}
className={`absolute z-10 size-5 top-3 left-3 ${
isSelected || isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
/>
<Checkbox
checked={isSelected}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}}
onKeyDown={(event) => {
if (event.key === " " || event.key === "Enter") {
handleCheckboxChange({
checked: !isSelected,
shiftKey: event.shiftKey,
});
}
}}
onCheckedChange={() => {}}
className={`absolute z-10 size-5 top-3 left-3 ${
isSelected || isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
}`}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 692 - 707, Add a keyboard
handler to the Checkbox so keyboard users can trigger the same shift-select
logic as mouse users: implement onKeyDown (or onKeyUp) on the Checkbox component
and when the key is Enter or Space call handleCheckboxChange with { checked:
!isSelected, shiftKey: event.shiftKey } to mirror the existing onClick behavior;
keep the existing onClick and existing className logic (isSelected,
isDropdownOpen) and ensure the handler uses the same symbols: Checkbox,
handleCheckboxChange, isSelected, isDropdownOpen.

Comment on lines +778 to +783
<ContextMenuItem
icon={<HugeiconsIcon icon={Copy01Icon} />}
onClick={onDuplicateClick}
>
Duplicate
</ContextMenuItem>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unhandled promise from async onDuplicateClick in ContextMenuItem.

onDuplicateClick resolves to the handleDuplicate async function. ContextMenuItem's onClick does not await or .catch() the returned promise. Per project-manager.ts, duplicateProjects shows a toast and rethrows on failure, so a rejection here becomes an unhandled promise rejection.

🛠️ Proposed fix
-			onClick={onDuplicateClick}
+			onClick={() => { void onDuplicateClick(); }}

Or add a .catch() if local error feedback is needed:

-			onClick={onDuplicateClick}
+			onClick={() => onDuplicateClick().catch(() => {})}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 778 - 783, The
ContextMenuItem's onClick is calling the async onDuplicateClick (which delegates
to handleDuplicate / duplicateProjects) without handling rejections; update the
click handler so any promise rejection is caught—either by wrapping the call in
an async function with try/catch (e.g., async () => { try { await
onDuplicateClick(); } catch (e) { /* show toast/log */ } }) or by calling
onDuplicateClick().catch(...) to consume errors; reference ContextMenuItem,
onDuplicateClick, handleDuplicate and duplicateProjects when making the change
to ensure the thrown error from duplicateProjects is handled locally.

Comment on lines +845 to 848
const handleDuplicate = () => {
onDuplicateClick();
onOpenChange(false);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

onDuplicateClick() called without await in ProjectMenu.

The dropdown closes immediately (onOpenChange(false)) and any rejection from the async onDuplicateClick is silently swallowed, meaning duplicate failures produce no in-component feedback.

🛠️ Proposed fix
-	const handleDuplicate = () => {
-		onDuplicateClick();
-		onOpenChange(false);
-	};
+	const handleDuplicate = async () => {
+		await onDuplicateClick();
+		onOpenChange(false);
+	};
📝 Committable suggestion

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

Suggested change
const handleDuplicate = () => {
onDuplicateClick();
onOpenChange(false);
};
const handleDuplicate = async () => {
await onDuplicateClick();
onOpenChange(false);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/projects/page.tsx` around lines 845 - 848, handleDuplicate
currently calls onDuplicateClick() without awaiting it and immediately closes
the dropdown via onOpenChange(false), which swallows any async errors; change
handleDuplicate to await the async onDuplicateClick() call, only call
onOpenChange(false) after it resolves, and add try/catch around the await to
handle and surface any rejection (e.g., show an error toast or set an error
state) so failures in onDuplicateClick are not silently ignored; update
references in the same scope (handleDuplicate, onDuplicateClick, onOpenChange)
accordingly.

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