mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-21 23:34:31 +00:00
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:
@@ -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 = {}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']],
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user