mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 16:24:06 +00:00
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 <github-actions@github.com>
178 lines
5.5 KiB
TypeScript
178 lines
5.5 KiB
TypeScript
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
|
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { api } from '@/scripts/api'
|
|
import { app } from '@/scripts/app'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useExtensionStore } from '@/stores/extensionStore'
|
|
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
|
import { useMenuItemStore } from '@/stores/menuItemStore'
|
|
import { useWidgetStore } from '@/stores/widgetStore'
|
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
|
import type { ComfyExtension } from '@/types/comfy'
|
|
|
|
export const useExtensionService = () => {
|
|
const extensionStore = useExtensionStore()
|
|
const settingStore = useSettingStore()
|
|
const keybindingStore = useKeybindingStore()
|
|
const { wrapWithErrorHandling } = useErrorHandling()
|
|
|
|
/**
|
|
* Loads all extensions from the API into the window in parallel
|
|
*/
|
|
const loadExtensions = async () => {
|
|
extensionStore.loadDisabledExtensionNames(
|
|
settingStore.get('Comfy.Extension.Disabled')
|
|
)
|
|
|
|
const extensions = await api.getExtensions()
|
|
|
|
// Need to load core extensions first as some custom extensions
|
|
// may depend on them.
|
|
await import('../extensions/core/index')
|
|
extensionStore.captureCoreExtensions()
|
|
await Promise.all(
|
|
extensions
|
|
.filter((extension) => !extension.includes('extensions/core'))
|
|
.map(async (ext) => {
|
|
try {
|
|
await import(/* @vite-ignore */ api.fileURL(ext))
|
|
} catch (error) {
|
|
console.error('Error loading extension', ext, error)
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Register an extension with the app
|
|
* @param extension The extension to register
|
|
*/
|
|
const registerExtension = (extension: ComfyExtension) => {
|
|
extensionStore.registerExtension(extension)
|
|
|
|
const addKeybinding = wrapWithErrorHandling(
|
|
keybindingStore.addDefaultKeybinding
|
|
)
|
|
const addSetting = wrapWithErrorHandling(settingStore.addSetting)
|
|
|
|
extension.keybindings?.forEach((keybinding) => {
|
|
addKeybinding(new KeybindingImpl(keybinding))
|
|
})
|
|
useCommandStore().loadExtensionCommands(extension)
|
|
useMenuItemStore().loadExtensionMenuCommands(extension)
|
|
extension.settings?.forEach(addSetting)
|
|
useBottomPanelStore().registerExtensionBottomPanelTabs(extension)
|
|
if (extension.getCustomWidgets) {
|
|
// TODO(huchenlei): We should deprecate the async return value of
|
|
// getCustomWidgets.
|
|
void (async () => {
|
|
if (extension.getCustomWidgets) {
|
|
const widgets = await extension.getCustomWidgets(app)
|
|
useWidgetStore().registerCustomWidgets(widgets)
|
|
}
|
|
})()
|
|
}
|
|
|
|
if (extension.onAuthUserResolved) {
|
|
const { onUserResolved } = useCurrentUser()
|
|
onUserResolved((user) => {
|
|
void extension.onAuthUserResolved?.(user, app)
|
|
})
|
|
}
|
|
|
|
if (extension.onAuthTokenRefreshed) {
|
|
const { onTokenRefreshed } = useCurrentUser()
|
|
onTokenRefreshed(() => {
|
|
void extension.onAuthTokenRefreshed?.()
|
|
})
|
|
}
|
|
|
|
if (extension.onAuthUserLogout) {
|
|
const { onUserLogout } = useCurrentUser()
|
|
onUserLogout(() => {
|
|
void extension.onAuthUserLogout?.()
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoke an extension callback
|
|
* @param {keyof ComfyExtension} method The extension callback to execute
|
|
* @param {any[]} args Any arguments to pass to the callback
|
|
* @returns
|
|
*/
|
|
const invokeExtensions = (method: keyof ComfyExtension, ...args: any[]) => {
|
|
const results: any[] = []
|
|
for (const ext of extensionStore.enabledExtensions) {
|
|
if (method in ext) {
|
|
try {
|
|
results.push(ext[method](...args, app))
|
|
} catch (error) {
|
|
console.error(
|
|
`Error calling extension '${ext.name}' method '${method}'`,
|
|
{ error },
|
|
{ extension: ext },
|
|
{ args }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Invoke an async extension callback
|
|
* Each callback will be invoked concurrently
|
|
* @param {string} method The extension callback to execute
|
|
* @param {...any} args Any arguments to pass to the callback
|
|
* @returns
|
|
*/
|
|
const invokeExtensionsAsync = async (
|
|
method: keyof ComfyExtension,
|
|
...args: any[]
|
|
) => {
|
|
return await Promise.all(
|
|
extensionStore.enabledExtensions.map(async (ext) => {
|
|
if (method in ext) {
|
|
try {
|
|
// Set current extension name for legacy compatibility tracking
|
|
if (method === 'setup') {
|
|
legacyMenuCompat.setCurrentExtension(ext.name)
|
|
}
|
|
|
|
const result = await ext[method](...args, app)
|
|
|
|
// Clear current extension after setup
|
|
if (method === 'setup') {
|
|
legacyMenuCompat.setCurrentExtension(null)
|
|
}
|
|
|
|
return result
|
|
} catch (error) {
|
|
// Clear current extension on error too
|
|
if (method === 'setup') {
|
|
legacyMenuCompat.setCurrentExtension(null)
|
|
}
|
|
|
|
console.error(
|
|
`Error calling extension '${ext.name}' method '${method}'`,
|
|
{ error },
|
|
{ extension: ext },
|
|
{ args }
|
|
)
|
|
}
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
return {
|
|
loadExtensions,
|
|
registerExtension,
|
|
invokeExtensions,
|
|
invokeExtensionsAsync
|
|
}
|
|
}
|