Files
MajoorWaldi-ComfyUI-Majoor-…/ui/features/runtime/entryUiRegistration.ts
T
2026-09-04 21:18:35 +02:00

809 lines
29 KiB
TypeScript

/**
* UI registration - sidebar, bottom panel, commands, node menu items.
*
* Vue migration notes
* -------------------
* Sidebar and bottom-panel tabs are now registered with `type: 'custom'` backed
* by a full Vue 3 + Pinia application mounted inside the container element.
* This replaces both the old DOM-manipulation approach AND the keep-alive hack
* (`_mjrKeepAliveMounted`): `mountKeepAlive` mounts the Vue app only once and
* re-uses the live DOM on subsequent `render()` calls, preserving all grid
* state and scroll positions across tab-switch cycles.
*
* Commands are declared as a plain array on the extension object (declarative
* API) so ComfyUI surfaces them in the command palette and the shortcut editor
* without any extra imperative registration calls.
*/
import { runStartupWarmup } from "../../app/bootstrap.js";
import {
activateBottomPanelTabForApp,
activateSidebarTabForApp,
registerBottomPanelTabForApp,
registerCommandForApp,
registerKeybindingForApp,
registerSidebarTabForApp,
setSettingForApp,
} from "../../app/hostAdapter.js";
import { EVENTS } from "../../app/events.js";
import { openMajoorSettings } from "../../app/openMajoorSettings.js";
import { comfyToast } from "../../app/toast.js";
import { t } from "../../app/i18n.js";
import { post, setIndexDirectorySetting, setOutputDirectorySetting } from "../../api/client.js";
import { ENDPOINTS } from "../../api/endpoints.js";
import { loadMajoorSettings, saveMajoorSettings } from "../../app/settings/settingsCore.js";
import { mountKeepAlive, unmountKeepAlive } from "../../vue/createVueApp.js";
import GlobalRuntimeApp from "../../vue/GlobalRuntime.vue";
import AssetsManagerApp from "../../vue/App.vue";
import GeneratedFeedApp from "../../vue/GeneratedFeedApp.vue";
import { mountTopBarMfvButton, teardownTopBarMfvButton } from "./topBarMfvButton.js";
import { startEarlyFetch, consumeEarlyFetch } from "./earlyFetch.js";
import { configureSidebarAssetBadge, resetSidebarAssetBadge } from "./sidebarAssetBadge.js";
import { consumePendingGeneratedAssets } from "./pendingGeneratedAssets.js";
import { refreshGeneratedFeedHosts } from "../bottomPanel/feed/feedHost.js";
// -- keep-alive mount keys -----------------------------------------------------
const GLOBAL_RUNTIME_ROOT_ID = "mjr-global-runtime-root";
const GLOBAL_RUNTIME_MOUNT_KEY = "_mjrGlobalRuntimeVueApp";
const SIDEBAR_MOUNT_KEY = "_mjrSidebarVueApp";
const FEED_MOUNT_KEY = "_mjrFeedVueApp";
const GENERATED_FEED_TAB_ID = "majoor-generated-feed";
let _lastSelectionToolboxNode: any = null;
// Re-exported from the dedicated lightweight module so existing imports
// continue to work. The module is auto-evaluated as a top-level side effect
// from `entry.js` so the first /list request leaves the browser before this
// (heavier, Vue-importing) module is even parsed.
export { startEarlyFetch, consumeEarlyFetch };
function ensureGlobalRuntimeRoot() {
if (typeof document === "undefined" || !document?.body) return null;
let root = document.getElementById(GLOBAL_RUNTIME_ROOT_ID);
if (root) return root;
root = document.createElement("div");
root.id = GLOBAL_RUNTIME_ROOT_ID;
root.setAttribute("role", "presentation");
root.style.cssText =
// Keep runtime hosts above panel layers so main viewer overlay cannot render underneath.
"position:fixed;inset:0;overflow:visible;pointer-events:none;z-index:10020;";
document.body.appendChild(root);
return root;
}
export function mountGlobalRuntime(): boolean {
try {
const root = ensureGlobalRuntimeRoot();
if (!root) return false;
const mounted = !!mountKeepAlive(root, GlobalRuntimeApp, GLOBAL_RUNTIME_MOUNT_KEY);
mountTopBarMfvButton();
return mounted;
} catch {
return false;
}
}
export function teardownGlobalRuntime(): void {
try {
teardownTopBarMfvButton();
const root = document.getElementById(GLOBAL_RUNTIME_ROOT_ID);
if (!root) return;
unmountKeepAlive(root, GLOBAL_RUNTIME_MOUNT_KEY);
root.remove?.();
} catch {
/* ignore */
}
}
export { teardownTopBarMfvButton };
// -- sidebar helpers -----------------------------------------------------------
export function openAssetsManagerPanel(runtimeApp: any, sidebarTabId: string): boolean {
const opened = activateSidebarTabForApp(runtimeApp, sidebarTabId);
resetSidebarAssetBadge();
if (!opened) {
try {
window.dispatchEvent(new Event(EVENTS.OPEN_ASSETS_MANAGER));
} catch (e) {
console.debug?.(e);
}
}
return opened;
}
export function triggerRefreshGrid(): void {
try {
window.dispatchEvent(
new CustomEvent(EVENTS.RELOAD_GRID, { detail: { reason: "command" } }),
);
} catch (e) {
console.debug?.(e);
}
}
function openGeneratedFeedPanel(runtimeApp: any): boolean {
const opened = activateBottomPanelTabForApp(runtimeApp, GENERATED_FEED_TAB_ID);
if (!opened) {
try {
refreshGeneratedFeedHosts();
} catch (e) {
console.debug?.(e);
}
}
return opened;
}
async function pickNativeFolderPath(): Promise<string> {
try {
const browseRes = await post(ENDPOINTS.BROWSE_FOLDER, {});
if (browseRes?.ok) {
return String(browseRes?.data?.path || "").trim();
}
const code = String(browseRes?.code || "").trim();
if (code && code !== "CANCELLED") {
comfyToast(
browseRes?.error ||
t(
"toast.nativeFolderBrowserUnavailable",
"Native folder browser unavailable. Please enter path manually.",
),
"warning",
3500,
);
}
} catch (e) {
console.debug?.(e);
comfyToast(
t(
"toast.nativeFolderBrowserUnavailable",
"Native folder browser unavailable. Please enter path manually.",
),
"warning",
3500,
);
}
return "";
}
async function pickSettingsDirectory(
runtimeApp: any,
{
settingId,
settingsPathKey,
save,
successMessage,
restartRequired = false,
}: {
settingId: string;
settingsPathKey: string;
save: (value: string) => Promise<any>;
successMessage: string;
restartRequired?: boolean;
},
): Promise<boolean> {
const pickedPath = await pickNativeFolderPath();
if (!pickedPath) return false;
const result = await save(pickedPath);
if (!result?.ok) {
comfyToast(result?.error || t("toast.settingsPathSaveFailed", "Failed to save path"), "error");
return false;
}
try {
const settings = loadMajoorSettings();
settings.paths = settings.paths || {};
settings.paths[settingsPathKey] = pickedPath;
saveMajoorSettings(settings);
setSettingForApp(runtimeApp, settingId, pickedPath);
window.dispatchEvent(
new CustomEvent(EVENTS.SETTINGS_CHANGED, {
detail: { key: `paths.${settingsPathKey}`, value: pickedPath },
}),
);
} catch (e) {
console.debug?.(e);
}
comfyToast(
restartRequired
? `${successMessage} ${t("toast.restartComfyUiToApply", "Restart ComfyUI to apply.")}`
: successMessage,
"success",
2800,
);
return true;
}
// -- commands (declarative array for ComfyUI extension API) --------------------
function firstTrackableSelectionItem(selectedItem: any) {
return getSelectionItems(selectedItem).find(isMajoorTrackableNode) || null;
}
function resolveNodeContextDetail(node: any) {
const sourceNodeId = String(node?.id ?? node?.nodeId ?? node?.node_id ?? "").trim();
if (!sourceNodeId) return null;
const sourceNodeType = String(
node?.comfyClass || node?.type || node?.constructor?.type || "",
).trim();
const title = String(
node?.title || node?.properties?.title || node?.properties?.name || sourceNodeType || "",
).trim();
return {
source_node_id: sourceNodeId,
sourceNodeId,
source_node_type: sourceNodeType,
sourceNodeType,
title,
};
}
/**
* Resolve the media file(s) referenced by a canvas node (Load Image widget
* value, node preview images, widget media elements). Lazy-imports the node
* stream extractor so this heavy module is only parsed on demand.
*/
async function resolveNodeFiles(node: any): Promise<Array<Record<string, any>>> {
if (!node) return [];
try {
const mod = await import("../viewer/nodeStream/NodeStreamController.js");
const fileData = mod.extractNodeFileData?.(node);
const filename = String(fileData?.filename || "").trim();
if (!filename) return [];
return [
{
filename,
subfolder: String(fileData?.subfolder || ""),
type: String(fileData?.type || "output"),
kind: String(fileData?.kind || ""),
},
];
} catch (e) {
console.debug?.(e);
return [];
}
}
function dispatchNodeContext(detail: Record<string, any>) {
try {
if (typeof window === "undefined") return;
(window as any).MajoorAssetsManager = (window as any).MajoorAssetsManager || {};
(window as any).MajoorAssetsManager.pendingNodeContext = detail;
window.dispatchEvent(new CustomEvent(EVENTS.OPEN_NODE_CONTEXT, { detail }));
} catch (e) {
console.debug?.(e);
}
}
/**
* Open the Assets Manager panel focused on the given canvas node: shows the
* node's indexed outputs (source-node context) or, when unavailable, the
* file currently referenced by the node (Load Image widget, preview image).
*/
export async function openNodeTargetInManager(
runtimeApp: any,
sidebarTabId: string,
node: any,
): Promise<boolean> {
openAssetsManagerPanel(runtimeApp, sidebarTabId);
const detail: Record<string, any> = resolveNodeContextDetail(node) || {};
const files = await resolveNodeFiles(node);
if (files.length) detail.files = files;
// Loader nodes reference an existing file rather than producing outputs:
// their node id would match unrelated source-node contexts, so resolve the
// referenced file directly.
const nodeType = String(node?.comfyClass || node?.type || "").toLowerCase();
if (files.length && /load|upload/.test(nodeType)) {
delete detail.source_node_id;
delete detail.sourceNodeId;
}
if (!detail.source_node_id && !files.length) {
comfyToast(t("toast.nodeContextMissing", "Select an output node first."), "info", 2200);
return false;
}
dispatchNodeContext(detail);
return true;
}
/**
* Open the floating viewer directly on the media referenced by the node.
* Falls back to the plain MFV open event when no file can be resolved.
*/
export async function openNodeTargetInFloatingViewer(node: any): Promise<boolean> {
try {
const files = await resolveNodeFiles(node);
if (files.length) {
const { floatingViewerManager } = await import("../viewer/floatingViewerManager.js");
const opened = await floatingViewerManager.openAssets({ assets: files, index: 0 });
if (opened) return true;
}
} catch (e) {
console.debug?.(e);
}
try {
window.dispatchEvent(new Event(EVENTS.MFV_OPEN));
} catch (e) {
console.debug?.(e);
}
return false;
}
function openNodeContext(runtimeApp: any, sidebarTabId: any) {
const node = _lastSelectionToolboxNode;
if (!node) {
comfyToast(t("toast.nodeContextMissing", "Select an output node first."), "info", 2200);
return false;
}
void openNodeTargetInManager(runtimeApp, sidebarTabId, node);
return true;
}
export function buildNativeCommands(runtimeApp: any, { sidebarTabId, triggerStartupScan }: { sidebarTabId: string; triggerStartupScan: () => void }): unknown[] {
return [
{
id: "mjr.openAssetsManager",
label: t("manager.title", "Assets Manager"),
tooltip: t("tooltip.openAssetsManager", "Open Majoor Assets Manager"),
title: t("tooltip.openAssetsManager", "Open Majoor Assets Manager"),
description: t("tooltip.openAssetsManager", "Open Majoor Assets Manager"),
icon: "pi pi-folder-open",
function: () => openAssetsManagerPanel(runtimeApp, sidebarTabId),
},
{
id: "mjr.scanAssets",
label: t("command.scanAssets", "Scan assets"),
icon: "pi pi-refresh",
function: () => triggerStartupScan(),
},
{
id: "mjr.toggleFloatingViewer",
label: t("command.toggleFloatingViewer", "Toggle floating viewer"),
icon: "pi pi-images",
function: () => {
try {
window.dispatchEvent(new Event(EVENTS.MFV_TOGGLE));
} catch (e) {
console.debug?.(e);
}
},
},
{
id: "mjr.refreshAssetsGrid",
label: t("command.refreshAssetsGrid", "Refresh assets grid"),
icon: "pi pi-sync",
function: () => triggerRefreshGrid(),
},
{
id: "mjr.openFloatingViewer",
label: t("command.openFloatingViewer", "Open floating viewer"),
tooltip: t("tooltip.openFloatingViewer", "Open Majoor floating viewer"),
title: t("tooltip.openFloatingViewer", "Open Majoor floating viewer"),
description: t("tooltip.openFloatingViewer", "Open Majoor floating viewer"),
icon: "pi pi-window-maximize",
function: () => {
try {
window.dispatchEvent(new Event(EVENTS.MFV_OPEN));
} catch (e) {
console.debug?.(e);
}
},
},
{
id: "mjr.openGeneratedFeed",
label: t("command.openGeneratedFeed", "Open generated feed"),
tooltip: t("tooltip.openGeneratedFeed", "Open the Majoor generated feed panel"),
title: t("tooltip.openGeneratedFeed", "Open the Majoor generated feed panel"),
description: t("tooltip.openGeneratedFeed", "Open the Majoor generated feed panel"),
icon: "pi pi-list",
function: () => openGeneratedFeedPanel(runtimeApp),
},
{
id: "mjr.openSettings",
label: t("command.openSettings", "Open Majoor settings"),
icon: "pi pi-cog",
function: () => openMajoorSettings(runtimeApp),
},
{
id: "mjr.pickOutputDirectory",
label: t("command.pickOutputDirectory", "Pick output directory"),
tooltip: t(
"tooltip.pickOutputDirectory",
"Use the native folder browser to set Majoor's generation output directory",
),
title: t(
"tooltip.pickOutputDirectory",
"Use the native folder browser to set Majoor's generation output directory",
),
description: t(
"tooltip.pickOutputDirectory",
"Use the native folder browser to set Majoor's generation output directory",
),
icon: "pi pi-folder",
function: () =>
pickSettingsDirectory(runtimeApp, {
settingId: "Majoor.Paths.OutputDirectory",
settingsPathKey: "outputDirectory",
save: setOutputDirectorySetting,
successMessage: t("toast.outputDirectorySaved", "Output directory saved."),
}),
},
{
id: "mjr.pickIndexDirectory",
label: t("command.pickIndexDirectory", "Pick index directory"),
tooltip: t(
"tooltip.pickIndexDirectory",
"Use the native folder browser to set Majoor's index database directory",
),
title: t(
"tooltip.pickIndexDirectory",
"Use the native folder browser to set Majoor's index database directory",
),
description: t(
"tooltip.pickIndexDirectory",
"Use the native folder browser to set Majoor's index database directory",
),
icon: "pi pi-database",
function: () =>
pickSettingsDirectory(runtimeApp, {
settingId: "Majoor.Paths.IndexDirectory",
settingsPathKey: "indexDirectory",
save: setIndexDirectorySetting,
successMessage: t("toast.indexDirectorySaved", "Index directory saved."),
restartRequired: true,
}),
},
{
id: "mjr.openNodeContext",
label: t("command.openNodeContext", "Show assets from selected node"),
tooltip: t(
"tooltip.openNodeContext",
"Show the latest indexed assets produced by this node",
),
title: t(
"tooltip.openNodeContext",
"Show the latest indexed assets produced by this node",
),
description: t(
"tooltip.openNodeContext",
"Show the latest indexed assets produced by this node",
),
icon: "pi pi-sitemap",
function: () => openNodeContext(runtimeApp, sidebarTabId),
},
{
id: "mjr.openNodeInFloatingViewer",
label: t("command.openNodeInFloatingViewer", "Open node media in floating viewer"),
tooltip: t(
"tooltip.openNodeInFloatingViewer",
"Open this node's media in the Majoor floating viewer",
),
title: t(
"tooltip.openNodeInFloatingViewer",
"Open this node's media in the Majoor floating viewer",
),
description: t(
"tooltip.openNodeInFloatingViewer",
"Open this node's media in the Majoor floating viewer",
),
icon: "pi pi-window-maximize",
function: () => {
void openNodeTargetInFloatingViewer(_lastSelectionToolboxNode);
},
},
];
}
export function buildNativeKeybindings(): unknown[] {
return [
{
combo: { alt: true, shift: true, key: "a" },
commandId: "mjr.openAssetsManager",
},
{
combo: { alt: true, shift: true, key: "v" },
commandId: "mjr.toggleFloatingViewer",
},
];
}
export function buildNativeMenuCommands(): unknown[] {
return [
{
path: ["Extensions", "Majoor Assets Manager"],
commands: [
"mjr.openAssetsManager",
"mjr.openFloatingViewer",
"mjr.openGeneratedFeed",
"mjr.refreshAssetsGrid",
"mjr.scanAssets",
"mjr.openSettings",
"mjr.pickOutputDirectory",
"mjr.pickIndexDirectory",
],
},
];
}
export function getMajoorCanvasMenuItems(runtimeApp: any, { sidebarTabId, triggerStartupScan }: { sidebarTabId: string; triggerStartupScan: () => void }): unknown[] {
return [
null,
{
content: "Majoor Assets Manager",
submenu: {
options: [
{
content: t("tooltip.openAssetsManager", "Open Majoor Assets Manager"),
callback: () => openAssetsManagerPanel(runtimeApp, sidebarTabId),
},
{
content: t("command.openFloatingViewer", "Open floating viewer"),
callback: () => {
try {
window.dispatchEvent(new Event(EVENTS.MFV_OPEN));
} catch (e) {
console.debug?.(e);
}
},
},
{
content: t("command.openGeneratedFeed", "Open generated feed"),
callback: () => openGeneratedFeedPanel(runtimeApp),
},
null,
{
content: t("command.refreshAssetsGrid", "Refresh assets grid"),
callback: () => triggerRefreshGrid(),
},
{
content: t("command.scanAssets", "Scan assets"),
callback: () => triggerStartupScan(),
},
{
content: t("command.openSettings", "Open Majoor settings"),
callback: () => openMajoorSettings(runtimeApp),
},
],
},
},
];
}
interface AboutPageBadge {
label: string;
icon: string;
url?: string;
}
export function buildAboutPageBadges(): AboutPageBadge[] {
return [
{
label: "Majoor Assets Manager",
icon: "pi pi-folder",
},
{
label: "v2.5.1",
icon: "pi pi-tag",
},
{
label: "Docs",
icon: "pi pi-book",
url: "https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/user_guide.html",
},
];
}
/** Imperatively registers commands - used as a fallback for older ComfyUI. */
export function registerNativeCommands(runtimeApp: any, options: any): void {
for (const command of buildNativeCommands(runtimeApp, options)) {
registerCommandForApp(runtimeApp, command);
}
}
export function registerNativeKeybindings(runtimeApp: any): void {
for (const keybinding of buildNativeKeybindings()) {
registerKeybindingForApp(runtimeApp, keybinding);
}
}
// -- sidebar registration ------------------------------------------------------
/**
* Registers the Majoor Assets Manager sidebar tab.
*
* The tab uses `type: 'custom'` so we control the full lifecycle.
* On `render()` a Vue 3 app (AssetsManagerApp) is mounted - or kept alive if
* already mounted. `destroy()` is intentionally a no-op so the Vue app (and
* therefore the grid, scroll position, and selections) survive tab-switches.
* Full teardown only happens when the extension is unregistered.
*/
export function registerAssetsSidebar(runtimeApp: any, { sidebarTabId }: { sidebarTabId: string }): any {
configureSidebarAssetBadge({ sidebarTabId });
return registerSidebarTabForApp(runtimeApp, {
id: sidebarTabId,
icon: "pi pi-folder",
title: t("manager.title"),
label: t("manager.sidebarLabel"),
tooltip: t("tooltip.sidebarTab"),
type: "custom",
render(el: any) {
resetSidebarAssetBadge();
// Generation/execution must not block panel rendering or warmup.
// Backend handlers are independent of the prompt queue, so always
// kick off the early fetch + warmup when the sidebar mounts.
startEarlyFetch();
void runStartupWarmup({ idleOnly: true }).catch(() => null);
// Mount the Vue app once; subsequent calls reuse the live instance.
mountKeepAlive(el, AssetsManagerApp, SIDEBAR_MOUNT_KEY);
setTimeout(() => {
const pending = consumePendingGeneratedAssets();
if (!pending) return;
try {
window.dispatchEvent(
new CustomEvent(EVENTS.RELOAD_GRID, {
detail: {
reason: "pending-generated-assets",
pending,
},
}),
);
} catch (e) {
console.debug?.(e);
}
}, 0);
},
destroy(el: any) {
// Keep-alive: intentionally do NOT call unmountKeepAlive here.
// The Vue app stays mounted so the grid survives tab-switches.
// unmountKeepAlive is only called on full extension teardown (see below).
void el; // suppress lint warning
},
});
}
/**
* Teardown the sidebar Vue app entirely (called only from the entry lifecycle
* controller on extension cleanup / ComfyUI reload).
*/
export function teardownAssetsSidebar(): void {
try {
unmountKeepAlive(null, SIDEBAR_MOUNT_KEY);
} catch {
/* ignore */
}
}
let _sidebarPrewarmStarted = false;
const PREWARM_HOST_ID = "mjr-sidebar-prewarm-host";
/**
* Prewarm the Assets Manager sidebar Vue app on a detached container so the
* heavy Vue mount (component tree creation, lazy chunk imports, store hydrate)
* happens during browser idle time - well before the user clicks the tab.
*
* When `render(el)` is later called by ComfyUI, `mountKeepAlive` finds the
* existing record and only re-attaches the live host into `el` (a single
* `replaceChildren` call), making the click->cards latency drop from ~2 s to
* essentially the time it takes to flush one DOM frame.
*
* Safe to call multiple times: only the first call does the work.
*/
export function prewarmAssetsSidebar(): boolean {
if (_sidebarPrewarmStarted) return false;
if (typeof document === "undefined" || !document?.body) return false;
_sidebarPrewarmStarted = true;
try {
let host = document.getElementById(PREWARM_HOST_ID);
if (!host) {
host = document.createElement("div");
host.id = PREWARM_HOST_ID;
// Off-screen but attached so layout/measure paths work the same as
// when the panel is visible. ResizeObserver and clientWidth still
// need a non-zero box, so size the host like a typical sidebar.
host.style.cssText =
"position:fixed;left:-99999px;top:0;width:360px;height:600px;" +
"overflow:hidden;visibility:hidden;pointer-events:none;contain:strict;";
host.setAttribute("aria-hidden", "true");
document.body.appendChild(host);
}
mountKeepAlive(host, AssetsManagerApp, SIDEBAR_MOUNT_KEY);
return true;
} catch (e) {
console.debug?.(e);
_sidebarPrewarmStarted = false;
return false;
}
}
// -- bottom panel registration -------------------------------------------------
/**
* Returns the bottom-panel tab definition.
* Used both as the declarative `bottomPanelTabs` array entry on the extension
* object and as the argument to registerBottomPanelTabCompat() for older
* ComfyUI versions.
*/
export function getGeneratedFeedBottomPanelTab() {
return {
id: GENERATED_FEED_TAB_ID,
title: t("bottomFeed.title", "Generated Feed"),
icon: "pi pi-images",
type: "custom",
render(el: any) {
mountKeepAlive(el, GeneratedFeedApp, FEED_MOUNT_KEY);
},
destroy(el: any) {
// Keep-alive: do not unmount on every hide.
void el;
},
};
}
export function buildBottomPanelTabs(): unknown[] {
return [getGeneratedFeedBottomPanelTab()];
}
export function registerGeneratedBottomPanel(runtimeApp: any): any {
return registerBottomPanelTabForApp(runtimeApp, getGeneratedFeedBottomPanelTab());
}
export function teardownGeneratedFeed(): void {
try {
unmountKeepAlive(null, FEED_MOUNT_KEY);
} catch {
/* ignore */
}
}
// -- node context-menu items ---------------------------------------------------
export function isMajoorTrackableNode(node: any): boolean {
const comfyClass = String(
node?.comfyClass || node?.type || node?.constructor?.type || "",
).trim();
if (!comfyClass) return false;
return /save|load|preview/i.test(comfyClass);
}
function getSelectionItems(selectedItem: any) {
if (!selectedItem) return [];
if (Array.isArray(selectedItem)) return selectedItem.filter(Boolean);
if (selectedItem instanceof Set) return Array.from(selectedItem).filter(Boolean);
if (selectedItem instanceof Map) return Array.from(selectedItem.values()).filter(Boolean);
if (Array.isArray(selectedItem?.items)) return selectedItem.items.filter(Boolean);
if (Array.isArray(selectedItem?.nodes)) return selectedItem.nodes.filter(Boolean);
return [selectedItem];
}
export function getMajoorSelectionToolboxCommands(selectedItem: any): string[] {
_lastSelectionToolboxNode = firstTrackableSelectionItem(selectedItem);
if (!_lastSelectionToolboxNode) return [];
return ["mjr.openNodeContext", "mjr.openAssetsManager", "mjr.openNodeInFloatingViewer"];
}
function nodeMenuEntry(label: any, callback: any) {
return { content: label, callback };
}
export function getMajoorNodeMenuItems(node: any, runtimeApp: any, { sidebarTabId }: { sidebarTabId: string }): unknown[] {
if (!isMajoorTrackableNode(node)) return [];
return [
nodeMenuEntry("View in Assets Manager", () => {
void openNodeTargetInManager(runtimeApp, sidebarTabId, node);
}),
nodeMenuEntry("Open in Floating Viewer", () => {
void openNodeTargetInFloatingViewer(node);
}),
nodeMenuEntry("Index Output", () => {
try {
triggerRefreshGrid();
comfyToast(t("toast.rescanningFile", "Rescanning file..."), "info", 1800);
} catch (e) {
console.debug?.(e);
}
}),
];
}