feat(projects): add right-click context menu to project cards#711
feat(projects): add right-click context menu to project cards#711malthart wants to merge 2 commits intoOpenCut-app:mainfrom
Conversation
- 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>
|
@malthart is attempting to deploy a commit to the OpenCut OSS Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/web/src/app/projects/page.tsx (1)
632-645: Drop the no‑oponCheckedChange(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.
apps/web/src/app/projects/page.tsx
Outdated
| 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> |
There was a problem hiding this comment.
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.
apps/web/src/app/projects/page.tsx
Outdated
| 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); | ||
| }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
apps/web/src/app/projects/page.tsx (2)
683-731: Context menu still ignores multi-select state.
ContextMenuis always rendered regardless ofisMultiSelect, 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 bypassinvokeAction().
handleDuplicateandhandleDeleteConfirmcallduplicateProjects/deleteProjectsdirectly, 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: Prefercn()for conditional class composition.Both the grid Checkbox (lines 702-706) and the
ProjectMenutrigger 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).
| <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" | ||
| }`} | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| <ContextMenuItem | ||
| icon={<HugeiconsIcon icon={Copy01Icon} />} | ||
| onClick={onDuplicateClick} | ||
| > | ||
| Duplicate | ||
| </ContextMenuItem> |
There was a problem hiding this comment.
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.
| const handleDuplicate = () => { | ||
| onDuplicateClick(); | ||
| onOpenChange(false); | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
Summary
Adds a right-click context menu to project cards on the projects page.
Changes
How to test
Summary by CodeRabbit
New Features
Refactor