Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5b37fc5
feat(contextMenu): add extension API for context menu items
Myestery Oct 9, 2025
0685a1d
feat(contextMenu): add legacy compatibility layer for monkey-patched …
Myestery Oct 9, 2025
d94821b
feat(contextMenu): migrate core extensions to new context menu API
Myestery Oct 9, 2025
9981f25
feat(contextMenu): migrate groupNode extension to new API
Myestery Oct 9, 2025
bee6f05
Merge remote-tracking branch 'origin/main' into contextmenu-extension…
Myestery Oct 13, 2025
f939dd6
Merge branch 'main' into contextmenu-extension-migration
Myestery Oct 14, 2025
6d4a926
Merge branch 'main' into contextmenu-extension-migration
Myestery Oct 14, 2025
ef1ee73
register wrapper for getCanvasMenuOptions to avoid legacy monkey-patc…
Myestery Oct 17, 2025
d0c2151
Enhance LegacyMenuCompat to prevent duplicate menu items during colle…
Myestery Oct 17, 2025
5753b09
Add tests for legacy item extraction in contextMenuCompat
Myestery Oct 17, 2025
858e94c
Merge branch 'main' into contextmenu-extension-migration
Myestery Oct 20, 2025
107aed8
Merge remote-tracking branch 'origin/main' into contextmenu-extension…
Myestery Oct 20, 2025
08ec5c3
Update Translations composable to not run extension patch calls twice
Myestery Oct 21, 2025
2b12b86
Fix TS issues
Myestery Oct 21, 2025
17285e8
Fix issue with getNodemenuoptions
Myestery Oct 21, 2025
9b38a88
align tests to show extension name
Myestery Oct 21, 2025
de8f6d9
Merge branch 'main' into contextmenu-extension-migration
Myestery Oct 24, 2025
abbcbdb
refactor: update getCanvasMenuOptions to use generic IContextMenuValu…
Myestery Oct 24, 2025
e928600
fix ts issues
Myestery Oct 24, 2025
47772bf
use implicit types for context menu
Myestery Oct 24, 2025
7341219
refactor: simplify convertDisabled logic in groupNode.ts and update c…
Myestery Oct 24, 2025
c0cedc6
fix lint issue by prefixing with void
Myestery Oct 28, 2025
3c385d4
[automated] Update test expectations
invalid-email-address Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion src/composables/useContextMenuTranslation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
import { st, te } from '@/i18n'
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
import type {
IContextMenuOptions,
IContextMenuValue,
INodeInputSlot,
IWidget
} from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { normalizeI18nKey } from '@/utils/formatUtil'

/**
* Add translation for litegraph context menu.
*/
export const useContextMenuTranslation = () => {
// Install compatibility layer BEFORE any extensions load
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')

const f = LGraphCanvas.prototype.getCanvasMenuOptions
const getCanvasCenterMenuOptions = function (
this: LGraphCanvas,
...args: Parameters<typeof f>
) {
const res = f.apply(this, args) as ReturnType<typeof f>
const res: IContextMenuValue<string>[] = f.apply(this, args)

// Add items from new extension API
const newApiItems = app.collectCanvasMenuItems(
this
) as IContextMenuValue<string>[]
for (const item of newApiItems) {
res.push(item)
}

// Add legacy monkey-patched items
const legacyItems = legacyMenuCompat.extractLegacyItems(
'getCanvasMenuOptions',
this,
...args
) as ReturnType<typeof f>
for (const item of legacyItems) {
res.push(item)
}

// Translate all items
for (const item of res) {
if (item?.content) {
item.content = st(`contextMenu.${item.content}`, item.content)
Expand All @@ -28,6 +53,33 @@ export const useContextMenuTranslation = () => {

LGraphCanvas.prototype.getCanvasMenuOptions = getCanvasCenterMenuOptions

legacyMenuCompat.registerWrapper(
'getCanvasMenuOptions',
getCanvasCenterMenuOptions,
f,
LGraphCanvas.prototype
)

// Wrap getNodeMenuOptions to add new API items
const nodeMenuFn = LGraphCanvas.prototype.getNodeMenuOptions
const getNodeMenuOptionsWithExtensions = function (
this: LGraphCanvas,
...args: Parameters<typeof nodeMenuFn>
) {
const res = nodeMenuFn.apply(this, args)

// Add items from new extension API
const node = args[0]
const newApiItems = app.collectNodeMenuItems(node)
for (const item of newApiItems) {
res.push(item as (typeof res)[number])
}

return res
}

LGraphCanvas.prototype.getNodeMenuOptions = getNodeMenuOptionsWithExtensions

function translateMenus(
values: readonly (IContextMenuValue | string | null)[] | undefined,
options: IContextMenuOptions
Expand Down
99 changes: 45 additions & 54 deletions src/extensions/core/groupNode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { t } from '@/i18n'
import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import {
type ExecutableLGraphNode,
type ExecutionId,
LGraphCanvas,
LGraphNode,
LiteGraph,
SubgraphNode
Expand Down Expand Up @@ -1630,57 +1630,6 @@ export class GroupNodeHandler {
}
}

function addConvertToGroupOptions() {
// @ts-expect-error fixme ts strict error
function addConvertOption(options, index) {
const selected = Object.values(app.canvas.selected_nodes ?? {})
const disabled =
selected.length < 2 ||
selected.find((n) => GroupNodeHandler.isGroupNode(n))
options.splice(index, null, {
content: `Convert to Group Node (Deprecated)`,
disabled,
callback: convertSelectedNodesToGroupNode
})
}

// @ts-expect-error fixme ts strict error
function addManageOption(options, index) {
const groups = app.graph.extra?.groupNodes
const disabled = !groups || !Object.keys(groups).length
options.splice(index, null, {
content: `Manage Group Nodes`,
disabled,
callback: () => manageGroupNodes()
})
}

// Add to canvas
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
// @ts-expect-error fixme ts strict error
const options = getCanvasMenuOptions.apply(this, arguments)
const index = options.findIndex((o) => o?.content === 'Add Group')
const insertAt = index === -1 ? options.length - 1 : index + 2
addConvertOption(options, insertAt)
addManageOption(options, insertAt + 1)
return options
}

// Add to nodes
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
// @ts-expect-error fixme ts strict error
const options = getNodeMenuOptions.apply(this, arguments)
if (!GroupNodeHandler.isGroupNode(node)) {
const index = options.findIndex((o) => o?.content === 'Properties')
const insertAt = index === -1 ? options.length - 1 : index
addConvertOption(options, insertAt)
}
return options
}
}

const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
for (const node of nodes) {
if (typeof node.type === 'string' && node.type.startsWith('workflow/')) {
Expand Down Expand Up @@ -1776,8 +1725,50 @@ const ext: ComfyExtension = {
}
}
],
setup() {
addConvertToGroupOptions()

getCanvasMenuItems(canvas): IContextMenuValue[] {
const items: IContextMenuValue[] = []
const selected = Object.values(canvas.selected_nodes ?? {})
const convertDisabled =
selected.length < 2 ||
!!selected.find((n) => GroupNodeHandler.isGroupNode(n))

items.push({
content: `Convert to Group Node (Deprecated)`,
disabled: convertDisabled,
// @ts-expect-error fixme ts strict error - async callback
callback: () => convertSelectedNodesToGroupNode()
})

const groups = canvas.graph?.extra?.groupNodes
const manageDisabled = !groups || !Object.keys(groups).length
items.push({
content: `Manage Group Nodes`,
disabled: manageDisabled,
callback: () => manageGroupNodes()
})

return items
},

getNodeMenuItems(node): IContextMenuValue[] {
if (GroupNodeHandler.isGroupNode(node)) {
return []
}

const selected = Object.values(app.canvas.selected_nodes ?? {})
const disabled =
selected.length < 2 ||
!!selected.find((n) => GroupNodeHandler.isGroupNode(n))

return [
{
content: `Convert to Group Node (Deprecated)`,
disabled,
// @ts-expect-error fixme ts strict error - async callback
callback: () => convertSelectedNodesToGroupNode()
}
]
},
async beforeConfigureGraph(
graphData: ComfyWorkflowJSON,
Expand Down
Loading
Loading