mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-14 01:36:14 +00:00
[automated] Apply ESLint and Oxfmt fixes
This commit is contained in:
@@ -1,58 +1,58 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { assert, setAssertReporter } from "@/base/assert";
|
||||
import { assert, setAssertReporter } from '@/base/assert'
|
||||
|
||||
describe("assert", () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
describe('assert', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
setAssertReporter(() => {});
|
||||
});
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllEnvs()
|
||||
setAssertReporter(() => {})
|
||||
})
|
||||
|
||||
it("does nothing when condition is true", () => {
|
||||
expect(() => assert(true, "should not throw")).not.toThrow();
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does nothing when condition is true', () => {
|
||||
expect(() => assert(true, 'should not throw')).not.toThrow()
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("logs console.error when condition is false", () => {
|
||||
vi.stubEnv("DEV", false);
|
||||
assert(false, "test message");
|
||||
it('logs console.error when condition is false', () => {
|
||||
vi.stubEnv('DEV', false)
|
||||
assert(false, 'test message')
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"[Assertion failed]: test message",
|
||||
);
|
||||
});
|
||||
'[Assertion failed]: test message'
|
||||
)
|
||||
})
|
||||
|
||||
it("throws in DEV mode when condition is false", () => {
|
||||
vi.stubEnv("DEV", true);
|
||||
expect(() => assert(false, "dev error")).toThrow(
|
||||
"[Assertion failed]: dev error",
|
||||
);
|
||||
});
|
||||
it('throws in DEV mode when condition is false', () => {
|
||||
vi.stubEnv('DEV', true)
|
||||
expect(() => assert(false, 'dev error')).toThrow(
|
||||
'[Assertion failed]: dev error'
|
||||
)
|
||||
})
|
||||
|
||||
it("does not throw in non-DEV mode when condition is false", () => {
|
||||
vi.stubEnv("DEV", false);
|
||||
expect(() => assert(false, "non-dev error")).not.toThrow();
|
||||
});
|
||||
it('does not throw in non-DEV mode when condition is false', () => {
|
||||
vi.stubEnv('DEV', false)
|
||||
expect(() => assert(false, 'non-dev error')).not.toThrow()
|
||||
})
|
||||
|
||||
it("calls registered reporter in non-DEV mode", () => {
|
||||
vi.stubEnv("DEV", false);
|
||||
const reporter = vi.fn();
|
||||
setAssertReporter(reporter);
|
||||
assert(false, "reporter message");
|
||||
expect(reporter).toHaveBeenCalledWith("reporter message");
|
||||
});
|
||||
it('calls registered reporter in non-DEV mode', () => {
|
||||
vi.stubEnv('DEV', false)
|
||||
const reporter = vi.fn()
|
||||
setAssertReporter(reporter)
|
||||
assert(false, 'reporter message')
|
||||
expect(reporter).toHaveBeenCalledWith('reporter message')
|
||||
})
|
||||
|
||||
it("does not call reporter when condition is true", () => {
|
||||
vi.stubEnv("DEV", false);
|
||||
const reporter = vi.fn();
|
||||
setAssertReporter(reporter);
|
||||
assert(true, "no call");
|
||||
expect(reporter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('does not call reporter when condition is true', () => {
|
||||
vi.stubEnv('DEV', false)
|
||||
const reporter = vi.fn()
|
||||
setAssertReporter(reporter)
|
||||
assert(true, 'no call')
|
||||
expect(reporter).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
type AssertReporter = (message: string) => void;
|
||||
type AssertReporter = (message: string) => void
|
||||
|
||||
let reporter: AssertReporter | null = null;
|
||||
let reporter: AssertReporter | null = null
|
||||
|
||||
/**
|
||||
* Register a reporter for assertion failures in non-DEV environments.
|
||||
@@ -8,7 +8,7 @@ let reporter: AssertReporter | null = null;
|
||||
* Sentry, toast notifications, etc.
|
||||
*/
|
||||
export function setAssertReporter(fn: AssertReporter): void {
|
||||
reporter = fn;
|
||||
reporter = fn
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,13 +19,13 @@ export function setAssertReporter(fn: AssertReporter): void {
|
||||
* - Otherwise: delegates to registered reporter (Sentry, toast, etc.)
|
||||
*/
|
||||
export function assert(condition: boolean, message: string): void {
|
||||
if (condition) return;
|
||||
if (condition) return
|
||||
|
||||
console.error(`[Assertion failed]: ${message}`);
|
||||
console.error(`[Assertion failed]: ${message}`)
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
throw new Error(`[Assertion failed]: ${message}`);
|
||||
throw new Error(`[Assertion failed]: ${message}`)
|
||||
}
|
||||
|
||||
reporter?.(message);
|
||||
reporter?.(message)
|
||||
}
|
||||
|
||||
122
src/main.ts
122
src/main.ts
@@ -1,63 +1,63 @@
|
||||
import { definePreset } from "@primevue/themes";
|
||||
import Aura from "@primevue/themes/aura";
|
||||
import * as Sentry from "@sentry/vue";
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { createPinia } from "pinia";
|
||||
import "primeicons/primeicons.css";
|
||||
import PrimeVue from "primevue/config";
|
||||
import ConfirmationService from "primevue/confirmationservice";
|
||||
import ToastService from "primevue/toastservice";
|
||||
import Tooltip from "primevue/tooltip";
|
||||
import { createApp } from "vue";
|
||||
import { VueFire, VueFireAuth } from "vuefire";
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { createApp } from 'vue'
|
||||
import { VueFire, VueFireAuth } from 'vuefire'
|
||||
|
||||
import { setAssertReporter } from "@/base/assert";
|
||||
import { getFirebaseConfig } from "@/config/firebase";
|
||||
import { setAssertReporter } from '@/base/assert'
|
||||
import { getFirebaseConfig } from '@/config/firebase'
|
||||
import {
|
||||
configValueOrDefault,
|
||||
remoteConfig,
|
||||
} from "@/platform/remoteConfig/remoteConfig";
|
||||
import "@/lib/litegraph/public/css/litegraph.css";
|
||||
import router from "@/router";
|
||||
import { isDesktop, isNightly } from "@/platform/distribution/types";
|
||||
import { useToastStore } from "@/platform/updates/common/toastStore";
|
||||
import { useBootstrapStore } from "@/stores/bootstrapStore";
|
||||
remoteConfig
|
||||
} from '@/platform/remoteConfig/remoteConfig'
|
||||
import '@/lib/litegraph/public/css/litegraph.css'
|
||||
import router from '@/router'
|
||||
import { isDesktop, isNightly } from '@/platform/distribution/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||
|
||||
import App from "./App.vue";
|
||||
import App from './App.vue'
|
||||
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
|
||||
import "./assets/css/style.css";
|
||||
import { i18n } from "./i18n";
|
||||
import './assets/css/style.css'
|
||||
import { i18n } from './i18n'
|
||||
|
||||
/**
|
||||
* CRITICAL: Load remote config FIRST for cloud builds to ensure
|
||||
* window.__CONFIG__is available for all modules during initialization
|
||||
*/
|
||||
const isCloud = __DISTRIBUTION__ === "cloud";
|
||||
const isCloud = __DISTRIBUTION__ === 'cloud'
|
||||
|
||||
if (isCloud) {
|
||||
const { refreshRemoteConfig } =
|
||||
await import("@/platform/remoteConfig/refreshRemoteConfig");
|
||||
await refreshRemoteConfig({ useAuth: false });
|
||||
await import('@/platform/remoteConfig/refreshRemoteConfig')
|
||||
await refreshRemoteConfig({ useAuth: false })
|
||||
|
||||
const { initTelemetry } = await import("@/platform/telemetry/initTelemetry");
|
||||
await initTelemetry();
|
||||
const { initTelemetry } = await import('@/platform/telemetry/initTelemetry')
|
||||
await initTelemetry()
|
||||
}
|
||||
|
||||
const ComfyUIPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
primary: Aura["primitive"].blue,
|
||||
},
|
||||
});
|
||||
primary: Aura['primitive'].blue
|
||||
}
|
||||
})
|
||||
|
||||
const firebaseApp = initializeApp(getFirebaseConfig());
|
||||
const firebaseApp = initializeApp(getFirebaseConfig())
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
const sentryDsn = isCloud
|
||||
? configValueOrDefault(remoteConfig.value, "sentry_dsn", __SENTRY_DSN__)
|
||||
: __SENTRY_DSN__;
|
||||
? configValueOrDefault(remoteConfig.value, 'sentry_dsn', __SENTRY_DSN__)
|
||||
: __SENTRY_DSN__
|
||||
|
||||
Sentry.init({
|
||||
app,
|
||||
@@ -75,47 +75,47 @@ Sentry.init({
|
||||
// Disable event target wrapping to reduce overhead on high-frequency
|
||||
// DOM events (pointermove, mousemove, wheel). Sentry still captures
|
||||
// errors via window.onerror and unhandledrejection.
|
||||
Sentry.browserApiErrorsIntegration({ eventTarget: false }),
|
||||
],
|
||||
Sentry.browserApiErrorsIntegration({ eventTarget: false })
|
||||
]
|
||||
}
|
||||
: {
|
||||
integrations: [],
|
||||
autoSessionTracking: false,
|
||||
defaultIntegrations: false,
|
||||
}),
|
||||
});
|
||||
defaultIntegrations: false
|
||||
})
|
||||
})
|
||||
setAssertReporter((message) => {
|
||||
if (isDesktop) {
|
||||
Sentry.captureMessage(`[Assertion failed]: ${message}`, {
|
||||
level: "warning",
|
||||
});
|
||||
level: 'warning'
|
||||
})
|
||||
}
|
||||
if (isNightly) {
|
||||
useToastStore().add({
|
||||
severity: "warn",
|
||||
summary: "Assertion failed",
|
||||
detail: message,
|
||||
});
|
||||
severity: 'warn',
|
||||
summary: 'Assertion failed',
|
||||
detail: message
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
app.directive("tooltip", Tooltip);
|
||||
app.directive('tooltip', Tooltip)
|
||||
app
|
||||
.use(router)
|
||||
.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: ComfyUIPreset,
|
||||
options: {
|
||||
prefix: "p",
|
||||
prefix: 'p',
|
||||
cssLayer: {
|
||||
name: "primevue",
|
||||
order: "theme, base, primevue",
|
||||
name: 'primevue',
|
||||
order: 'theme, base, primevue'
|
||||
},
|
||||
// This is a workaround for the issue with the dark mode selector
|
||||
// https://github.com/primefaces/primevue/issues/5515
|
||||
darkModeSelector: ".dark-theme, :root:has(.dark-theme)",
|
||||
},
|
||||
},
|
||||
darkModeSelector: '.dark-theme, :root:has(.dark-theme)'
|
||||
}
|
||||
}
|
||||
})
|
||||
.use(ConfirmationService)
|
||||
.use(ToastService)
|
||||
@@ -123,10 +123,10 @@ app
|
||||
.use(i18n)
|
||||
.use(VueFire, {
|
||||
firebaseApp,
|
||||
modules: [VueFireAuth()],
|
||||
});
|
||||
modules: [VueFireAuth()]
|
||||
})
|
||||
|
||||
const bootstrapStore = useBootstrapStore(pinia);
|
||||
void bootstrapStore.startStoreBootstrap();
|
||||
const bootstrapStore = useBootstrapStore(pinia)
|
||||
void bootstrapStore.startStoreBootstrap()
|
||||
|
||||
app.mount("#vue-app");
|
||||
app.mount('#vue-app')
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import _ from "es-toolkit/compat";
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { assert } from "@/base/assert";
|
||||
import type { CanvasPointerEvent } from "@/lib/litegraph/src/litegraph";
|
||||
import { LGraphCanvas, LiteGraph } from "@/lib/litegraph/src/litegraph";
|
||||
import type { ComfyWorkflow } from "@/platform/workflow/management/stores/workflowStore";
|
||||
import { useWorkflowStore } from "@/platform/workflow/management/stores/workflowStore";
|
||||
import type { ComfyWorkflowJSON } from "@/platform/workflow/validation/schemas/workflowSchema";
|
||||
import type { ExecutedWsMessage } from "@/schemas/apiSchema";
|
||||
import { useExecutionStore } from "@/stores/executionStore";
|
||||
import { useNodeOutputStore } from "@/stores/nodeOutputStore";
|
||||
import { useSubgraphNavigationStore } from "@/stores/subgraphNavigationStore";
|
||||
import { assert } from '@/base/assert'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
|
||||
import { api } from "./api";
|
||||
import type { ComfyApp } from "./app";
|
||||
import { app } from "./app";
|
||||
import { api } from './api'
|
||||
import type { ComfyApp } from './app'
|
||||
import { app } from './app'
|
||||
|
||||
function clone<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
function isActiveTracker(tracker: ChangeTracker): boolean {
|
||||
return useWorkflowStore().activeWorkflow?.changeTracker === tracker;
|
||||
return useWorkflowStore().activeWorkflow?.changeTracker === tracker
|
||||
}
|
||||
|
||||
const reportedInactiveCalls = new Set<string>();
|
||||
const reportedInactiveCalls = new Set<string>()
|
||||
|
||||
/**
|
||||
* Report a ChangeTracker method being called on an inactive tracker.
|
||||
* Deduplicates per method+workflow per session to avoid signal noise on hot paths.
|
||||
*/
|
||||
function reportInactiveTrackerCall(method: string, workflowPath: string) {
|
||||
const key = `${method}:${workflowPath}`;
|
||||
if (reportedInactiveCalls.has(key)) return;
|
||||
reportedInactiveCalls.add(key);
|
||||
const key = `${method}:${workflowPath}`
|
||||
if (reportedInactiveCalls.has(key)) return
|
||||
reportedInactiveCalls.add(key)
|
||||
assert(
|
||||
false,
|
||||
`ChangeTracker.${method}() called on inactive tracker for: ${workflowPath}`,
|
||||
);
|
||||
`ChangeTracker.${method}() called on inactive tracker for: ${workflowPath}`
|
||||
)
|
||||
}
|
||||
|
||||
export class ChangeTracker {
|
||||
static MAX_HISTORY = 50;
|
||||
static MAX_HISTORY = 50
|
||||
/**
|
||||
* Guard flag to prevent captureCanvasState from running during loadGraphData.
|
||||
* Between rootGraph.configure() and afterLoadNewGraph(), the rootGraph
|
||||
@@ -48,25 +48,25 @@ export class ChangeTracker {
|
||||
* the OLD workflow. Any captureCanvasState call in that window would
|
||||
* serialize the wrong graph into the old workflow's activeState, corrupting it.
|
||||
*/
|
||||
static isLoadingGraph = false;
|
||||
static isLoadingGraph = false
|
||||
/**
|
||||
* The active state of the workflow.
|
||||
*/
|
||||
activeState: ComfyWorkflowJSON;
|
||||
undoQueue: ComfyWorkflowJSON[] = [];
|
||||
redoQueue: ComfyWorkflowJSON[] = [];
|
||||
changeCount: number = 0;
|
||||
activeState: ComfyWorkflowJSON
|
||||
undoQueue: ComfyWorkflowJSON[] = []
|
||||
redoQueue: ComfyWorkflowJSON[] = []
|
||||
changeCount: number = 0
|
||||
/**
|
||||
* Whether the redo/undo restoring is in progress.
|
||||
*/
|
||||
_restoringState: boolean = false;
|
||||
_restoringState: boolean = false
|
||||
|
||||
ds?: { scale: number; offset: [number, number] };
|
||||
nodeOutputs?: Record<string, ExecutedWsMessage["output"]>;
|
||||
ds?: { scale: number; offset: [number, number] }
|
||||
nodeOutputs?: Record<string, ExecutedWsMessage['output']>
|
||||
|
||||
private subgraphState?: {
|
||||
navigation: string[];
|
||||
};
|
||||
navigation: string[]
|
||||
}
|
||||
|
||||
constructor(
|
||||
/**
|
||||
@@ -76,9 +76,9 @@ export class ChangeTracker {
|
||||
/**
|
||||
* The initial state of the workflow
|
||||
*/
|
||||
public initialState: ComfyWorkflowJSON,
|
||||
public initialState: ComfyWorkflowJSON
|
||||
) {
|
||||
this.activeState = initialState;
|
||||
this.activeState = initialState
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,21 +86,21 @@ export class ChangeTracker {
|
||||
*/
|
||||
reset(state?: ComfyWorkflowJSON) {
|
||||
// Do not reset the state if we are restoring.
|
||||
if (this._restoringState) return;
|
||||
if (this._restoringState) return
|
||||
|
||||
if (state) this.activeState = clone(state);
|
||||
this.initialState = clone(this.activeState);
|
||||
if (state) this.activeState = clone(state)
|
||||
this.initialState = clone(this.activeState)
|
||||
}
|
||||
|
||||
store() {
|
||||
this.ds = {
|
||||
scale: app.canvas.ds.scale,
|
||||
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]],
|
||||
};
|
||||
this.nodeOutputs = useNodeOutputStore().snapshotOutputs();
|
||||
const navigation = useSubgraphNavigationStore().exportState();
|
||||
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
|
||||
}
|
||||
this.nodeOutputs = useNodeOutputStore().snapshotOutputs()
|
||||
const navigation = useSubgraphNavigationStore().exportState()
|
||||
// Always store the navigation state, even if empty (root level)
|
||||
this.subgraphState = { navigation };
|
||||
this.subgraphState = { navigation }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,11 +117,11 @@ export class ChangeTracker {
|
||||
*/
|
||||
deactivate() {
|
||||
if (!isActiveTracker(this)) {
|
||||
reportInactiveTrackerCall("deactivate", this.workflow.path);
|
||||
return;
|
||||
reportInactiveTrackerCall('deactivate', this.workflow.path)
|
||||
return
|
||||
}
|
||||
if (!this._restoringState) this.captureCanvasState();
|
||||
this.store();
|
||||
if (!this._restoringState) this.captureCanvasState()
|
||||
this.store()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,46 +132,46 @@ export class ChangeTracker {
|
||||
* @internal Not part of the public extension API.
|
||||
*/
|
||||
prepareForSave() {
|
||||
if (isActiveTracker(this)) this.captureCanvasState();
|
||||
if (isActiveTracker(this)) this.captureCanvasState()
|
||||
}
|
||||
|
||||
restore() {
|
||||
if (this.ds) {
|
||||
app.canvas.ds.scale = this.ds.scale;
|
||||
app.canvas.ds.offset = this.ds.offset;
|
||||
app.canvas.ds.scale = this.ds.scale
|
||||
app.canvas.ds.offset = this.ds.offset
|
||||
}
|
||||
if (this.nodeOutputs) {
|
||||
useNodeOutputStore().restoreOutputs(this.nodeOutputs);
|
||||
useNodeOutputStore().restoreOutputs(this.nodeOutputs)
|
||||
}
|
||||
if (this.subgraphState) {
|
||||
const { navigation } = this.subgraphState;
|
||||
useSubgraphNavigationStore().restoreState(navigation);
|
||||
const { navigation } = this.subgraphState
|
||||
useSubgraphNavigationStore().restoreState(navigation)
|
||||
|
||||
const activeId = navigation.at(-1);
|
||||
const activeId = navigation.at(-1)
|
||||
if (activeId) {
|
||||
// Navigate to the saved subgraph
|
||||
const subgraph = app.rootGraph.subgraphs.get(activeId);
|
||||
const subgraph = app.rootGraph.subgraphs.get(activeId)
|
||||
if (subgraph) {
|
||||
app.canvas.setGraph(subgraph);
|
||||
app.canvas.setGraph(subgraph)
|
||||
}
|
||||
} else {
|
||||
// Empty navigation array means root level
|
||||
app.canvas.setGraph(app.rootGraph);
|
||||
app.canvas.setGraph(app.rootGraph)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateModified() {
|
||||
api.dispatchCustomEvent("graphChanged", this.activeState);
|
||||
api.dispatchCustomEvent('graphChanged', this.activeState)
|
||||
|
||||
// Get the workflow from the store as ChangeTracker is raw object, i.e.
|
||||
// `this.workflow` is not reactive.
|
||||
const workflow = useWorkflowStore().getWorkflowByPath(this.workflow.path);
|
||||
const workflow = useWorkflowStore().getWorkflowByPath(this.workflow.path)
|
||||
if (workflow) {
|
||||
workflow.isModified = !ChangeTracker.graphEqual(
|
||||
this.initialState,
|
||||
this.activeState,
|
||||
);
|
||||
this.activeState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,302 +181,302 @@ export class ChangeTracker {
|
||||
* Calling this on an inactive tracker would capture the wrong graph.
|
||||
*/
|
||||
captureCanvasState() {
|
||||
const isUndoRedoing = this._restoringState;
|
||||
const isInsideChangeTransaction = this.changeCount > 0;
|
||||
const isUndoRedoing = this._restoringState
|
||||
const isInsideChangeTransaction = this.changeCount > 0
|
||||
if (
|
||||
!app.graph ||
|
||||
isInsideChangeTransaction ||
|
||||
isUndoRedoing ||
|
||||
ChangeTracker.isLoadingGraph
|
||||
)
|
||||
return;
|
||||
return
|
||||
|
||||
if (!isActiveTracker(this)) {
|
||||
reportInactiveTrackerCall("captureCanvasState", this.workflow.path);
|
||||
return;
|
||||
reportInactiveTrackerCall('captureCanvasState', this.workflow.path)
|
||||
return
|
||||
}
|
||||
|
||||
const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON;
|
||||
const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON
|
||||
if (!this.activeState) {
|
||||
this.activeState = currentState;
|
||||
return;
|
||||
this.activeState = currentState
|
||||
return
|
||||
}
|
||||
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
||||
this.undoQueue.push(this.activeState);
|
||||
this.undoQueue.push(this.activeState)
|
||||
if (this.undoQueue.length > ChangeTracker.MAX_HISTORY) {
|
||||
this.undoQueue.shift();
|
||||
this.undoQueue.shift()
|
||||
}
|
||||
|
||||
this.activeState = currentState;
|
||||
this.redoQueue.length = 0;
|
||||
this.updateModified();
|
||||
this.activeState = currentState
|
||||
this.redoQueue.length = 0
|
||||
this.updateModified()
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link captureCanvasState} instead. */
|
||||
checkState() {
|
||||
if (!ChangeTracker._checkStateWarned) {
|
||||
ChangeTracker._checkStateWarned = true;
|
||||
ChangeTracker._checkStateWarned = true
|
||||
console.warn(
|
||||
"checkState() is deprecated — use captureCanvasState() instead.",
|
||||
);
|
||||
'checkState() is deprecated — use captureCanvasState() instead.'
|
||||
)
|
||||
}
|
||||
this.captureCanvasState();
|
||||
this.captureCanvasState()
|
||||
}
|
||||
|
||||
private static _checkStateWarned = false;
|
||||
private static _checkStateWarned = false
|
||||
|
||||
async updateState(source: ComfyWorkflowJSON[], target: ComfyWorkflowJSON[]) {
|
||||
const prevState = source.pop();
|
||||
const prevState = source.pop()
|
||||
if (prevState) {
|
||||
target.push(this.activeState);
|
||||
this._restoringState = true;
|
||||
target.push(this.activeState)
|
||||
this._restoringState = true
|
||||
try {
|
||||
await app.loadGraphData(prevState, false, false, this.workflow, {
|
||||
checkForRerouteMigration: false,
|
||||
silentAssetErrors: true,
|
||||
});
|
||||
this.activeState = prevState;
|
||||
this.updateModified();
|
||||
silentAssetErrors: true
|
||||
})
|
||||
this.activeState = prevState
|
||||
this.updateModified()
|
||||
} finally {
|
||||
this._restoringState = false;
|
||||
this._restoringState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async undo() {
|
||||
await this.updateState(this.undoQueue, this.redoQueue);
|
||||
await this.updateState(this.undoQueue, this.redoQueue)
|
||||
}
|
||||
|
||||
async redo() {
|
||||
await this.updateState(this.redoQueue, this.undoQueue);
|
||||
await this.updateState(this.redoQueue, this.undoQueue)
|
||||
}
|
||||
|
||||
async undoRedo(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && !e.altKey) {
|
||||
const key = e.key.toUpperCase();
|
||||
const key = e.key.toUpperCase()
|
||||
// Redo: Ctrl + Y, or Ctrl + Shift + Z
|
||||
if ((key === "Y" && !e.shiftKey) || (key == "Z" && e.shiftKey)) {
|
||||
await this.redo();
|
||||
return true;
|
||||
} else if (key === "Z" && !e.shiftKey) {
|
||||
await this.undo();
|
||||
return true;
|
||||
if ((key === 'Y' && !e.shiftKey) || (key == 'Z' && e.shiftKey)) {
|
||||
await this.redo()
|
||||
return true
|
||||
} else if (key === 'Z' && !e.shiftKey) {
|
||||
await this.undo()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeChange() {
|
||||
this.changeCount++;
|
||||
this.changeCount++
|
||||
}
|
||||
|
||||
afterChange() {
|
||||
if (!--this.changeCount) {
|
||||
this.captureCanvasState();
|
||||
this.captureCanvasState()
|
||||
}
|
||||
}
|
||||
|
||||
static init() {
|
||||
const getCurrentChangeTracker = () =>
|
||||
useWorkflowStore().activeWorkflow?.changeTracker;
|
||||
const captureState = () => getCurrentChangeTracker()?.captureCanvasState();
|
||||
useWorkflowStore().activeWorkflow?.changeTracker
|
||||
const captureState = () => getCurrentChangeTracker()?.captureCanvasState()
|
||||
|
||||
let keyIgnored = false;
|
||||
let keyIgnored = false
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => {
|
||||
// Do not trigger on repeat events (Holding down a key)
|
||||
// This can happen when user is holding down "Space" to pan the canvas.
|
||||
if (e.repeat) return;
|
||||
if (e.repeat) return
|
||||
|
||||
// If the mask editor is opened, we don't want to trigger on key events
|
||||
const comfyApp = app.constructor as typeof ComfyApp;
|
||||
if (comfyApp.maskeditor_is_opended?.()) return;
|
||||
const comfyApp = app.constructor as typeof ComfyApp
|
||||
if (comfyApp.maskeditor_is_opended?.()) return
|
||||
|
||||
const activeEl = document.activeElement;
|
||||
const activeEl = document.activeElement
|
||||
requestAnimationFrame(async () => {
|
||||
let bindInputEl: Element | null = null;
|
||||
let bindInputEl: Element | null = null
|
||||
// If we are auto queue in change mode then we do want to trigger on inputs
|
||||
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
|
||||
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === 'instant') {
|
||||
if (
|
||||
activeEl?.tagName === "INPUT" ||
|
||||
(activeEl && "type" in activeEl && activeEl.type === "textarea")
|
||||
activeEl?.tagName === 'INPUT' ||
|
||||
(activeEl && 'type' in activeEl && activeEl.type === 'textarea')
|
||||
) {
|
||||
// Ignore events on inputs, they have their native history
|
||||
return;
|
||||
return
|
||||
}
|
||||
bindInputEl = activeEl;
|
||||
bindInputEl = activeEl
|
||||
}
|
||||
|
||||
keyIgnored =
|
||||
e.key === "Control" ||
|
||||
e.key === "Shift" ||
|
||||
e.key === "Alt" ||
|
||||
e.key === "Meta";
|
||||
if (keyIgnored) return;
|
||||
e.key === 'Control' ||
|
||||
e.key === 'Shift' ||
|
||||
e.key === 'Alt' ||
|
||||
e.key === 'Meta'
|
||||
if (keyIgnored) return
|
||||
|
||||
const changeTracker = getCurrentChangeTracker();
|
||||
if (!changeTracker) return;
|
||||
const changeTracker = getCurrentChangeTracker()
|
||||
if (!changeTracker) return
|
||||
|
||||
// Check if this is a ctrl+z ctrl+y
|
||||
if (await changeTracker.undoRedo(e)) return;
|
||||
if (await changeTracker.undoRedo(e)) return
|
||||
|
||||
// If our active element is some type of input then handle changes after they're done
|
||||
if (ChangeTracker.bindInput(bindInputEl)) return;
|
||||
changeTracker.captureCanvasState();
|
||||
});
|
||||
if (ChangeTracker.bindInput(bindInputEl)) return
|
||||
changeTracker.captureCanvasState()
|
||||
})
|
||||
},
|
||||
true,
|
||||
);
|
||||
true
|
||||
)
|
||||
|
||||
window.addEventListener("keyup", () => {
|
||||
window.addEventListener('keyup', () => {
|
||||
if (keyIgnored) {
|
||||
keyIgnored = false;
|
||||
captureState();
|
||||
keyIgnored = false
|
||||
captureState()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Handle clicking DOM elements (e.g. widgets)
|
||||
window.addEventListener("mouseup", () => {
|
||||
captureState();
|
||||
});
|
||||
window.addEventListener('mouseup', () => {
|
||||
captureState()
|
||||
})
|
||||
|
||||
// Handle prompt queue event for dynamic widget changes
|
||||
api.addEventListener("promptQueued", () => {
|
||||
captureState();
|
||||
});
|
||||
api.addEventListener('promptQueued', () => {
|
||||
captureState()
|
||||
})
|
||||
|
||||
api.addEventListener("graphCleared", () => {
|
||||
captureState();
|
||||
});
|
||||
api.addEventListener('graphCleared', () => {
|
||||
captureState()
|
||||
})
|
||||
|
||||
// Handle litegraph clicks
|
||||
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
|
||||
const processMouseUp = LGraphCanvas.prototype.processMouseUp
|
||||
LGraphCanvas.prototype.processMouseUp = function (e) {
|
||||
const v = processMouseUp.apply(this, [e]);
|
||||
captureState();
|
||||
return v;
|
||||
};
|
||||
const v = processMouseUp.apply(this, [e])
|
||||
captureState()
|
||||
return v
|
||||
}
|
||||
|
||||
// Handle litegraph dialog popup for number/string widgets
|
||||
const prompt = LGraphCanvas.prototype.prompt;
|
||||
const prompt = LGraphCanvas.prototype.prompt
|
||||
LGraphCanvas.prototype.prompt = function (
|
||||
title: string,
|
||||
value: string | number,
|
||||
callback: (v: string) => void,
|
||||
event: CanvasPointerEvent,
|
||||
event: CanvasPointerEvent
|
||||
) {
|
||||
const extendedCallback = (v: string) => {
|
||||
callback(v);
|
||||
captureState();
|
||||
};
|
||||
return prompt.apply(this, [title, value, extendedCallback, event]);
|
||||
};
|
||||
callback(v)
|
||||
captureState()
|
||||
}
|
||||
return prompt.apply(this, [title, value, extendedCallback, event])
|
||||
}
|
||||
|
||||
// Handle litegraph context menu for COMBO widgets
|
||||
const close = LiteGraph.ContextMenu.prototype.close;
|
||||
const close = LiteGraph.ContextMenu.prototype.close
|
||||
LiteGraph.ContextMenu.prototype.close = function (e: MouseEvent) {
|
||||
const v = close.apply(this, [e]);
|
||||
captureState();
|
||||
return v;
|
||||
};
|
||||
const v = close.apply(this, [e])
|
||||
captureState()
|
||||
return v
|
||||
}
|
||||
|
||||
// Handle multiple commands as a single transaction
|
||||
document.addEventListener("litegraph:canvas", (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (detail.subType === "before-change") {
|
||||
getCurrentChangeTracker()?.beforeChange();
|
||||
} else if (detail.subType === "after-change") {
|
||||
getCurrentChangeTracker()?.afterChange();
|
||||
document.addEventListener('litegraph:canvas', (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail
|
||||
if (detail.subType === 'before-change') {
|
||||
getCurrentChangeTracker()?.beforeChange()
|
||||
} else if (detail.subType === 'after-change') {
|
||||
getCurrentChangeTracker()?.afterChange()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Store node outputs
|
||||
api.addEventListener("executed", (e: CustomEvent<ExecutedWsMessage>) => {
|
||||
const detail = e.detail;
|
||||
api.addEventListener('executed', (e: CustomEvent<ExecutedWsMessage>) => {
|
||||
const detail = e.detail
|
||||
const workflow =
|
||||
useExecutionStore().queuedJobs[detail.prompt_id]?.workflow;
|
||||
const changeTracker = workflow?.changeTracker;
|
||||
if (!changeTracker) return;
|
||||
changeTracker.nodeOutputs ??= {};
|
||||
const nodeOutputs = changeTracker.nodeOutputs;
|
||||
const output = nodeOutputs[detail.node];
|
||||
useExecutionStore().queuedJobs[detail.prompt_id]?.workflow
|
||||
const changeTracker = workflow?.changeTracker
|
||||
if (!changeTracker) return
|
||||
changeTracker.nodeOutputs ??= {}
|
||||
const nodeOutputs = changeTracker.nodeOutputs
|
||||
const output = nodeOutputs[detail.node]
|
||||
if (detail.merge && output) {
|
||||
for (const k in detail.output ?? {}) {
|
||||
const v = output[k];
|
||||
const v = output[k]
|
||||
if (v instanceof Array) {
|
||||
output[k] = v.concat(detail.output[k]);
|
||||
output[k] = v.concat(detail.output[k])
|
||||
} else {
|
||||
output[k] = detail.output[k];
|
||||
output[k] = detail.output[k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nodeOutputs[detail.node] = detail.output;
|
||||
nodeOutputs[detail.node] = detail.output
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
static bindInput(activeEl: Element | null): boolean {
|
||||
if (
|
||||
!activeEl ||
|
||||
activeEl.tagName === "CANVAS" ||
|
||||
activeEl.tagName === "BODY"
|
||||
activeEl.tagName === 'CANVAS' ||
|
||||
activeEl.tagName === 'BODY'
|
||||
) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
for (const evt of ["change", "input", "blur"]) {
|
||||
const htmlElement = activeEl as HTMLElement;
|
||||
for (const evt of ['change', 'input', 'blur']) {
|
||||
const htmlElement = activeEl as HTMLElement
|
||||
if (`on${evt}` in htmlElement) {
|
||||
const listener = () => {
|
||||
useWorkflowStore().activeWorkflow?.changeTracker?.captureCanvasState?.();
|
||||
htmlElement.removeEventListener(evt, listener);
|
||||
};
|
||||
htmlElement.addEventListener(evt, listener);
|
||||
return true;
|
||||
useWorkflowStore().activeWorkflow?.changeTracker?.captureCanvasState?.()
|
||||
htmlElement.removeEventListener(evt, listener)
|
||||
}
|
||||
htmlElement.addEventListener(evt, listener)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
static graphEqual(a: ComfyWorkflowJSON, b: ComfyWorkflowJSON) {
|
||||
if (a === b) return true;
|
||||
if (a === b) return true
|
||||
|
||||
if (typeof a == "object" && a && typeof b == "object" && b) {
|
||||
if (typeof a == 'object' && a && typeof b == 'object' && b) {
|
||||
// Compare nodes ignoring order
|
||||
if (
|
||||
!_.isEqualWith(a.nodes, b.nodes, (arrA, arrB) => {
|
||||
if (Array.isArray(arrA) && Array.isArray(arrB)) {
|
||||
return _.isEqual(new Set(arrA), new Set(arrB));
|
||||
return _.isEqual(new Set(arrA), new Set(arrB))
|
||||
}
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare extra properties ignoring ds
|
||||
if (
|
||||
!_.isEqual(_.omit(a.extra ?? {}, ["ds"]), _.omit(b.extra ?? {}, ["ds"]))
|
||||
!_.isEqual(_.omit(a.extra ?? {}, ['ds']), _.omit(b.extra ?? {}, ['ds']))
|
||||
)
|
||||
return false;
|
||||
return false
|
||||
|
||||
// Compare other properties normally
|
||||
for (const key of [
|
||||
"links",
|
||||
"floatingLinks",
|
||||
"reroutes",
|
||||
"groups",
|
||||
"definitions",
|
||||
"subgraphs",
|
||||
'links',
|
||||
'floatingLinks',
|
||||
'reroutes',
|
||||
'groups',
|
||||
'definitions',
|
||||
'subgraphs'
|
||||
]) {
|
||||
if (!_.isEqual(a[key], b[key])) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user