Skip to content

Commit b3da6cf

Browse files
Myesterygithub-actions
andauthored
Contextmenu extension migration (#5993)
This pull request refactors how context menu items are contributed by extensions in the LiteGraph-based canvas. The legacy monkey-patching approach for adding context menu options is replaced by a new, explicit API (`getCanvasMenuItems` and `getNodeMenuItems`) for extensions. A compatibility layer is added to support legacy extensions and warn developers about deprecated usage. The changes improve maintainability, extension interoperability, and migration to the new context menu system. ### Context Menu System Refactor * Introduced a new API for extensions to contribute context menu items via `getCanvasMenuItems` and `getNodeMenuItems` methods, replacing legacy monkey-patching of `LGraphCanvas.prototype.getCanvasMenuOptions`. Major extension files (`groupNode.ts`, `groupOptions.ts`, `nodeTemplates.ts`) now use this new API. [[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917L1779-R1771) [[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL232-R239) [[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL447-R458) * Added a compatibility layer (`legacyMenuCompat` in `contextMenuCompat.ts`) to detect and warn when legacy monkey-patching is used, and to extract legacy-added menu items for backward compatibility. [[1]](diffhunk://#diff-2b724cb107c04e290369fb927e2ae9fad03be9e617a7d4de2487deab89d0d018R2-R45) [[2]](diffhunk://#diff-d3a8284ec16ae3f9512e33abe44ae653ed1aa45c9926485ef6270cc8d2b94ae6R1-R115) ### Extension Migration * Refactored core extensions (`groupNode`, `groupOptions`, and `nodeTemplates`) to implement the new context menu API, moving menu item logic out of monkey-patched methods and into explicit extension methods. [[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917L1633-L1683) [[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL19-R77) [[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL366-R373) ### Type and Import Cleanup * Updated imports for context menu types (`IContextMenuValue`) across affected files for consistency with the new API. [[1]](diffhunk://#diff-b29f141b89433027e7bb7cde57fad84f9e97ffbe5c58040d3e0fdb7905022917R4-L7) [[2]](diffhunk://#diff-91169f3a27ff8974d5c8fc3346bd99c07bdfb5399984484630125fdd647ff02fL1-R11) [[3]](diffhunk://#diff-04c18583d2dbfc013888e4c02fd432c250acbcecdef82bf7f6d9fd888e632a6eL2-R6) [[4]](diffhunk://#diff-bde0dce9fe2403685d27b0e94a938c3d72824d02d01d1fd6167a0dddc6e585ddR10) ### Backward Compatibility and Migration Guidance * The compatibility layer logs a deprecation warning to the console when legacy monkey-patching is detected, helping developers migrate to the new API. --- These changes collectively modernize the context menu extension mechanism, improve code clarity, and provide a migration path for legacy extensions. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5993-Contextmenu-extension-migration-2876d73d3650813fae07c1141679637a) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <[email protected]>
1 parent 6afdb95 commit b3da6cf

File tree

15 files changed

+1016
-360
lines changed

15 files changed

+1016
-360
lines changed
-363 Bytes
Loading
-171 Bytes
Loading
-363 Bytes
Loading

src/composables/useContextMenuTranslation.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
11
import { st, te } from '@/i18n'
2+
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
23
import type {
34
IContextMenuOptions,
45
IContextMenuValue,
56
INodeInputSlot,
67
IWidget
78
} from '@/lib/litegraph/src/litegraph'
89
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
10+
import { app } from '@/scripts/app'
911
import { normalizeI18nKey } from '@/utils/formatUtil'
1012

1113
/**
1214
* Add translation for litegraph context menu.
1315
*/
1416
export const useContextMenuTranslation = () => {
15-
const f = LGraphCanvas.prototype.getCanvasMenuOptions
17+
// Install compatibility layer BEFORE any extensions load
18+
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')
19+
20+
const { getCanvasMenuOptions } = LGraphCanvas.prototype
1621
const getCanvasCenterMenuOptions = function (
1722
this: LGraphCanvas,
18-
...args: Parameters<typeof f>
23+
...args: Parameters<typeof getCanvasMenuOptions>
1924
) {
20-
const res = f.apply(this, args) as ReturnType<typeof f>
25+
const res: IContextMenuValue[] = getCanvasMenuOptions.apply(this, args)
26+
27+
// Add items from new extension API
28+
const newApiItems = app.collectCanvasMenuItems(this)
29+
for (const item of newApiItems) {
30+
res.push(item)
31+
}
32+
33+
// Add legacy monkey-patched items
34+
const legacyItems = legacyMenuCompat.extractLegacyItems(
35+
'getCanvasMenuOptions',
36+
this,
37+
...args
38+
)
39+
for (const item of legacyItems) {
40+
res.push(item)
41+
}
42+
43+
// Translate all items
2144
for (const item of res) {
2245
if (item?.content) {
2346
item.content = st(`contextMenu.${item.content}`, item.content)
@@ -28,6 +51,33 @@ export const useContextMenuTranslation = () => {
2851

2952
LGraphCanvas.prototype.getCanvasMenuOptions = getCanvasCenterMenuOptions
3053

54+
legacyMenuCompat.registerWrapper(
55+
'getCanvasMenuOptions',
56+
getCanvasCenterMenuOptions,
57+
getCanvasMenuOptions,
58+
LGraphCanvas.prototype
59+
)
60+
61+
// Wrap getNodeMenuOptions to add new API items
62+
const nodeMenuFn = LGraphCanvas.prototype.getNodeMenuOptions
63+
const getNodeMenuOptionsWithExtensions = function (
64+
this: LGraphCanvas,
65+
...args: Parameters<typeof nodeMenuFn>
66+
) {
67+
const res = nodeMenuFn.apply(this, args)
68+
69+
// Add items from new extension API
70+
const node = args[0]
71+
const newApiItems = app.collectNodeMenuItems(node)
72+
for (const item of newApiItems) {
73+
res.push(item)
74+
}
75+
76+
return res
77+
}
78+
79+
LGraphCanvas.prototype.getNodeMenuOptions = getNodeMenuOptionsWithExtensions
80+
3181
function translateMenus(
3282
values: readonly (IContextMenuValue | string | null)[] | undefined,
3383
options: IContextMenuOptions

src/extensions/core/groupNode.ts

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
22
import { t } from '@/i18n'
33
import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
4+
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
45
import {
56
type ExecutableLGraphNode,
67
type ExecutionId,
7-
LGraphCanvas,
88
LGraphNode,
99
LiteGraph,
1010
SubgraphNode
@@ -1630,57 +1630,6 @@ export class GroupNodeHandler {
16301630
}
16311631
}
16321632

1633-
function addConvertToGroupOptions() {
1634-
// @ts-expect-error fixme ts strict error
1635-
function addConvertOption(options, index) {
1636-
const selected = Object.values(app.canvas.selected_nodes ?? {})
1637-
const disabled =
1638-
selected.length < 2 ||
1639-
selected.find((n) => GroupNodeHandler.isGroupNode(n))
1640-
options.splice(index, null, {
1641-
content: `Convert to Group Node (Deprecated)`,
1642-
disabled,
1643-
callback: convertSelectedNodesToGroupNode
1644-
})
1645-
}
1646-
1647-
// @ts-expect-error fixme ts strict error
1648-
function addManageOption(options, index) {
1649-
const groups = app.graph.extra?.groupNodes
1650-
const disabled = !groups || !Object.keys(groups).length
1651-
options.splice(index, null, {
1652-
content: `Manage Group Nodes`,
1653-
disabled,
1654-
callback: () => manageGroupNodes()
1655-
})
1656-
}
1657-
1658-
// Add to canvas
1659-
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions
1660-
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
1661-
// @ts-expect-error fixme ts strict error
1662-
const options = getCanvasMenuOptions.apply(this, arguments)
1663-
const index = options.findIndex((o) => o?.content === 'Add Group')
1664-
const insertAt = index === -1 ? options.length - 1 : index + 2
1665-
addConvertOption(options, insertAt)
1666-
addManageOption(options, insertAt + 1)
1667-
return options
1668-
}
1669-
1670-
// Add to nodes
1671-
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions
1672-
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
1673-
// @ts-expect-error fixme ts strict error
1674-
const options = getNodeMenuOptions.apply(this, arguments)
1675-
if (!GroupNodeHandler.isGroupNode(node)) {
1676-
const index = options.findIndex((o) => o?.content === 'Properties')
1677-
const insertAt = index === -1 ? options.length - 1 : index
1678-
addConvertOption(options, insertAt)
1679-
}
1680-
return options
1681-
}
1682-
}
1683-
16841633
const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
16851634
for (const node of nodes) {
16861635
if (typeof node.type === 'string' && node.type.startsWith('workflow/')) {
@@ -1718,6 +1667,9 @@ async function convertSelectedNodesToGroupNode() {
17181667
return await GroupNodeHandler.fromNodes(nodes)
17191668
}
17201669

1670+
const convertDisabled = (selected: LGraphNode[]) =>
1671+
selected.length < 2 || !!selected.find((n) => GroupNodeHandler.isGroupNode(n))
1672+
17211673
function ungroupSelectedGroupNodes() {
17221674
const nodes = Object.values(app.canvas.selected_nodes ?? {})
17231675
for (const node of nodes) {
@@ -1776,8 +1728,46 @@ const ext: ComfyExtension = {
17761728
}
17771729
}
17781730
],
1779-
setup() {
1780-
addConvertToGroupOptions()
1731+
1732+
getCanvasMenuItems(canvas): IContextMenuValue[] {
1733+
const items: IContextMenuValue[] = []
1734+
const selected = Object.values(canvas.selected_nodes ?? {})
1735+
const convertEnabled = !convertDisabled(selected)
1736+
1737+
items.push({
1738+
content: `Convert to Group Node (Deprecated)`,
1739+
disabled: !convertEnabled,
1740+
// @ts-expect-error fixme ts strict error - async callback
1741+
callback: () => convertSelectedNodesToGroupNode()
1742+
})
1743+
1744+
const groups = canvas.graph?.extra?.groupNodes
1745+
const manageDisabled = !groups || !Object.keys(groups).length
1746+
items.push({
1747+
content: `Manage Group Nodes`,
1748+
disabled: manageDisabled,
1749+
callback: () => manageGroupNodes()
1750+
})
1751+
1752+
return items
1753+
},
1754+
1755+
getNodeMenuItems(node): IContextMenuValue[] {
1756+
if (GroupNodeHandler.isGroupNode(node)) {
1757+
return []
1758+
}
1759+
1760+
const selected = Object.values(app.canvas.selected_nodes ?? {})
1761+
const convertEnabled = !convertDisabled(selected)
1762+
1763+
return [
1764+
{
1765+
content: `Convert to Group Node (Deprecated)`,
1766+
disabled: !convertEnabled,
1767+
// @ts-expect-error fixme ts strict error - async callback
1768+
callback: () => convertSelectedNodesToGroupNode()
1769+
}
1770+
]
17811771
},
17821772
async beforeConfigureGraph(
17831773
graphData: ComfyWorkflowJSON,

0 commit comments

Comments
 (0)