Skip to content

Commit f4ca285

Browse files
dante01yoonclaude
andauthored
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>
1 parent 06732b8 commit f4ca285

File tree

3 files changed

+85
-2
lines changed

3 files changed

+85
-2
lines changed

src/composables/useCoreCommands.test.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ vi.mock('vue-i18n', async () => {
2323

2424
vi.mock('@/scripts/app', () => {
2525
const mockGraphClear = vi.fn()
26-
const mockCanvas = { subgraph: undefined }
26+
const mockCanvas = {
27+
subgraph: undefined,
28+
selectedItems: new Set(),
29+
copyToClipboard: vi.fn(),
30+
pasteFromClipboard: vi.fn(),
31+
selectItems: vi.fn()
32+
}
2733

2834
return {
2935
app: {
@@ -105,7 +111,8 @@ vi.mock('@/stores/subgraphStore', () => ({
105111

106112
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
107113
useCanvasStore: vi.fn(() => ({
108-
getCanvas: () => app.canvas
114+
getCanvas: () => app.canvas,
115+
canvas: app.canvas
109116
})),
110117
useTitleEditorStore: vi.fn(() => ({
111118
titleEditorTarget: null
@@ -300,6 +307,48 @@ describe('useCoreCommands', () => {
300307
})
301308
})
302309

310+
describe('Canvas clipboard commands', () => {
311+
function findCommand(id: string) {
312+
return useCoreCommands().find((cmd) => cmd.id === id)!
313+
}
314+
315+
beforeEach(() => {
316+
app.canvas.selectedItems = new Set()
317+
vi.mocked(app.canvas.copyToClipboard).mockClear()
318+
vi.mocked(app.canvas.pasteFromClipboard).mockClear()
319+
vi.mocked(app.canvas.selectItems).mockClear()
320+
})
321+
322+
it('should copy selected items when selection exists', async () => {
323+
app.canvas.selectedItems = new Set([
324+
{}
325+
]) as typeof app.canvas.selectedItems
326+
327+
await findCommand('Comfy.Canvas.CopySelected').function()
328+
329+
expect(app.canvas.copyToClipboard).toHaveBeenCalledWith()
330+
})
331+
332+
it('should not copy when no items are selected', async () => {
333+
await findCommand('Comfy.Canvas.CopySelected').function()
334+
335+
expect(app.canvas.copyToClipboard).not.toHaveBeenCalled()
336+
})
337+
338+
it('should paste from clipboard', async () => {
339+
await findCommand('Comfy.Canvas.PasteFromClipboard').function()
340+
341+
expect(app.canvas.pasteFromClipboard).toHaveBeenCalledWith()
342+
})
343+
344+
it('should select all items', async () => {
345+
await findCommand('Comfy.Canvas.SelectAll').function()
346+
347+
// No arguments means "select all items on canvas"
348+
expect(app.canvas.selectItems).toHaveBeenCalledWith()
349+
})
350+
})
351+
303352
describe('Subgraph metadata commands', () => {
304353
beforeEach(() => {
305354
mockSubgraph.extra = {}

src/composables/useCoreCommands.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,32 @@ export function useCoreCommands(): ComfyCommand[] {
884884
window.open(staticUrls.forum, '_blank')
885885
}
886886
},
887+
{
888+
id: 'Comfy.Canvas.CopySelected',
889+
icon: 'icon-[lucide--copy]',
890+
label: 'Copy',
891+
function: () => {
892+
if (app.canvas.selectedItems?.size) {
893+
app.canvas.copyToClipboard()
894+
}
895+
}
896+
},
897+
{
898+
id: 'Comfy.Canvas.PasteFromClipboard',
899+
icon: 'icon-[lucide--clipboard-paste]',
900+
label: 'Paste',
901+
function: () => {
902+
app.canvas.pasteFromClipboard()
903+
}
904+
},
905+
{
906+
id: 'Comfy.Canvas.SelectAll',
907+
icon: 'icon-[lucide--lasso-select]',
908+
label: 'Select All',
909+
function: () => {
910+
app.canvas.selectItems()
911+
}
912+
},
887913
{
888914
id: 'Comfy.Canvas.DeleteSelectedItems',
889915
icon: 'pi pi-trash',

src/constants/coreMenuCommands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export const CORE_MENU_COMMANDS = [
1414
]
1515
],
1616
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
17+
[
18+
['Edit'],
19+
[
20+
'Comfy.Canvas.CopySelected',
21+
'Comfy.Canvas.PasteFromClipboard',
22+
'Comfy.Canvas.SelectAll'
23+
]
24+
],
1725
[['Edit'], ['Comfy.ClearWorkflow']],
1826
[['Edit'], ['Comfy.OpenClipspace']],
1927
[

0 commit comments

Comments
 (0)