feat: add Copy, Paste, Select All commands to Edit menu (#8954)

## Summary

- Add Copy, Paste, and Select All commands to the Edit menu for
mobile/touch users and accessibility
- Menu-based copy uses LiteGraph internal clipboard; existing Ctrl+C/V
behavior is unchanged

## Changes

- `useCoreCommands.ts`: Register three new commands (`CopySelected`,
`PasteFromClipboard`, `SelectAll`)
- `coreMenuCommands.ts`: Add menu entries under Edit (between Undo/Redo
and Clear Workflow)
- `useCoreCommands.test.ts`: Add unit tests for the new commands

### AS IS
<img width="260" height="176" alt="스크린샷 2026-02-18 오후 5 44 14"
src="https://github.com/user-attachments/assets/8c9c86e1-55cc-411b-9d42-429001e04630"
/>


### TO BE
<img width="516" height="497" alt="스크린샷 2026-02-19 오후 5 07 28"
src="https://github.com/user-attachments/assets/a2047541-582f-4520-a08f-98c6e532d29f"
/>


## Test plan

- [x] Verify Copy/Paste/Select All appear in Edit menu
- [x] Select nodes → Edit > Copy → Edit > Paste → nodes duplicated
- [x] Edit > Select All → all canvas items selected
- [x] Copy with no selection → no-op (no error)
- [x] Existing Ctrl+C/V keyboard shortcuts still work

Fixes #2892

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8954-feat-add-Copy-Paste-Select-All-commands-to-Edit-menu-30b6d73d365081ec9270ed2a562eaf0b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dante
2026-02-20 19:34:45 +09:00
committed by GitHub
parent 06732b84bb
commit f4ca285d07
3 changed files with 85 additions and 2 deletions

View File

@@ -23,7 +23,13 @@ vi.mock('vue-i18n', async () => {
vi.mock('@/scripts/app', () => {
const mockGraphClear = vi.fn()
const mockCanvas = { subgraph: undefined }
const mockCanvas = {
subgraph: undefined,
selectedItems: new Set(),
copyToClipboard: vi.fn(),
pasteFromClipboard: vi.fn(),
selectItems: vi.fn()
}
return {
app: {
@@ -105,7 +111,8 @@ vi.mock('@/stores/subgraphStore', () => ({
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: vi.fn(() => ({
getCanvas: () => app.canvas
getCanvas: () => app.canvas,
canvas: app.canvas
})),
useTitleEditorStore: vi.fn(() => ({
titleEditorTarget: null
@@ -300,6 +307,48 @@ describe('useCoreCommands', () => {
})
})
describe('Canvas clipboard commands', () => {
function findCommand(id: string) {
return useCoreCommands().find((cmd) => cmd.id === id)!
}
beforeEach(() => {
app.canvas.selectedItems = new Set()
vi.mocked(app.canvas.copyToClipboard).mockClear()
vi.mocked(app.canvas.pasteFromClipboard).mockClear()
vi.mocked(app.canvas.selectItems).mockClear()
})
it('should copy selected items when selection exists', async () => {
app.canvas.selectedItems = new Set([
{}
]) as typeof app.canvas.selectedItems
await findCommand('Comfy.Canvas.CopySelected').function()
expect(app.canvas.copyToClipboard).toHaveBeenCalledWith()
})
it('should not copy when no items are selected', async () => {
await findCommand('Comfy.Canvas.CopySelected').function()
expect(app.canvas.copyToClipboard).not.toHaveBeenCalled()
})
it('should paste from clipboard', async () => {
await findCommand('Comfy.Canvas.PasteFromClipboard').function()
expect(app.canvas.pasteFromClipboard).toHaveBeenCalledWith()
})
it('should select all items', async () => {
await findCommand('Comfy.Canvas.SelectAll').function()
// No arguments means "select all items on canvas"
expect(app.canvas.selectItems).toHaveBeenCalledWith()
})
})
describe('Subgraph metadata commands', () => {
beforeEach(() => {
mockSubgraph.extra = {}

View File

@@ -884,6 +884,32 @@ export function useCoreCommands(): ComfyCommand[] {
window.open(staticUrls.forum, '_blank')
}
},
{
id: 'Comfy.Canvas.CopySelected',
icon: 'icon-[lucide--copy]',
label: 'Copy',
function: () => {
if (app.canvas.selectedItems?.size) {
app.canvas.copyToClipboard()
}
}
},
{
id: 'Comfy.Canvas.PasteFromClipboard',
icon: 'icon-[lucide--clipboard-paste]',
label: 'Paste',
function: () => {
app.canvas.pasteFromClipboard()
}
},
{
id: 'Comfy.Canvas.SelectAll',
icon: 'icon-[lucide--lasso-select]',
label: 'Select All',
function: () => {
app.canvas.selectItems()
}
},
{
id: 'Comfy.Canvas.DeleteSelectedItems',
icon: 'pi pi-trash',

View File

@@ -14,6 +14,14 @@ export const CORE_MENU_COMMANDS = [
]
],
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
[
['Edit'],
[
'Comfy.Canvas.CopySelected',
'Comfy.Canvas.PasteFromClipboard',
'Comfy.Canvas.SelectAll'
]
],
[['Edit'], ['Comfy.ClearWorkflow']],
[['Edit'], ['Comfy.OpenClipspace']],
[