diff --git a/browser_tests/colorPalette.spec.ts b/browser_tests/colorPalette.spec.ts
index 8caca6600..2d80544eb 100644
--- a/browser_tests/colorPalette.spec.ts
+++ b/browser_tests/colorPalette.spec.ts
@@ -138,6 +138,7 @@ test.describe('Color Palette', () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', {})
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
+ await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
})
test('Can show custom color palette', async ({ comfyPage }) => {
diff --git a/browser_tests/menu.spec.ts b/browser_tests/menu.spec.ts
index 9eec3e97b..e1f48422d 100644
--- a/browser_tests/menu.spec.ts
+++ b/browser_tests/menu.spec.ts
@@ -85,7 +85,21 @@ test.describe('Menu', () => {
const count = await comfyPage.getGraphNodesCount()
// Drag the node onto the canvas
const canvasSelector = '#graph-canvas'
- await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector)
+
+ // Get the bounding box of the canvas element
+ const canvasBoundingBox = (await comfyPage.page
+ .locator(canvasSelector)
+ .boundingBox())!
+
+ // Calculate the center position of the canvas
+ const targetPosition = {
+ x: canvasBoundingBox.x + canvasBoundingBox.width / 2,
+ y: canvasBoundingBox.y + canvasBoundingBox.height / 2
+ }
+
+ await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector, {
+ targetPosition
+ })
// Verify the node is added to the canvas
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
diff --git a/index.html b/index.html
index f60011061..07f29df3e 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
-
+
diff --git a/src/assets/css/style.css b/src/assets/css/style.css
index 2f284ff55..6116f8423 100644
--- a/src/assets/css/style.css
+++ b/src/assets/css/style.css
@@ -82,7 +82,8 @@ body {
grid-column: 1/-1;
/* Position at the first row */
grid-row: 1;
- z-index: 10;
+ /* Top menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */
+ z-index: 1000;
display: flex;
flex-direction: column;
}
diff --git a/src/components/appMenu/AppMenu.vue b/src/components/appMenu/AppMenu.vue
index 048a6eb44..a4bcc41fd 100644
--- a/src/components/appMenu/AppMenu.vue
+++ b/src/components/appMenu/AppMenu.vue
@@ -65,7 +65,7 @@
icon="pi pi-times"
severity="secondary"
:disabled="!executingPrompt"
- @click="actions.interrupt"
+ @click="commandStore.getCommand('Comfy.Interrupt')"
>
@@ -73,25 +73,19 @@
v-tooltip.bottom="$t('menu.refresh')"
icon="pi pi-refresh"
severity="secondary"
- @click="actions.refresh"
+ @click="commandStore.getCommand('Comfy.RefreshNodeDefinitions')"
/>
-
@@ -115,12 +109,12 @@ import {
useQueueSettingsStore
} from '@/stores/queueStore'
import { app } from '@/scripts/app'
-import { api } from '@/scripts/api'
import { storeToRefs } from 'pinia'
import { useSettingStore } from '@/stores/settingStore'
-import { useToastStore } from '@/stores/toastStore'
+import { useCommandStore } from '@/stores/commandStore'
const settingsStore = useSettingStore()
+const commandStore = useCommandStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { batchCount, mode: queueMode } = storeToRefs(useQueueSettingsStore())
@@ -147,31 +141,6 @@ const executingPrompt = computed(() => !!queueCountStore.count.value)
const queuePrompt = (e: MouseEvent) => {
app.queuePrompt(e.shiftKey ? -1 : 0, batchCount.value)
}
-
-const actions = {
- interrupt: async () => {
- await api.interrupt()
- useToastStore().add({
- severity: 'info',
- summary: 'Interrupted',
- detail: 'Execution has been interrupted',
- life: 1000
- })
- },
- clearWorkflow: () => {
- if (
- !(settingsStore.get('Comfy.ComfirmClear') ?? true) ||
- confirm('Clear workflow?')
- ) {
- app.clean()
- app.graph.clear()
- api.dispatchEvent(new CustomEvent('graphCleared'))
- }
- },
- resetView: () => app.resetView(),
- openClipspace: () => app['openClipspace'](),
- refresh: () => app.refreshComboInNodes()
-}
+
+
diff --git a/src/components/topbar/WorkflowTabs.vue b/src/components/topbar/WorkflowTabs.vue
new file mode 100644
index 000000000..ef4b86902
--- /dev/null
+++ b/src/components/topbar/WorkflowTabs.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/src/scripts/changeTracker.ts b/src/scripts/changeTracker.ts
index cfc4d1b27..c6cc0c45b 100644
--- a/src/scripts/changeTracker.ts
+++ b/src/scripts/changeTracker.ts
@@ -7,8 +7,8 @@ import { ComfyWorkflow } from './workflows'
export class ChangeTracker {
static MAX_HISTORY = 50
#app: ComfyApp
- undo = []
- redo = []
+ undoQueue = []
+ redoQueue = []
activeState = null
isOurLoad = false
workflow: ComfyWorkflow | null
@@ -54,12 +54,12 @@ export class ChangeTracker {
return
}
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
- this.undo.push(this.activeState)
- if (this.undo.length > ChangeTracker.MAX_HISTORY) {
- this.undo.shift()
+ this.undoQueue.push(this.activeState)
+ if (this.undoQueue.length > ChangeTracker.MAX_HISTORY) {
+ this.undoQueue.shift()
}
this.activeState = clone(currentState)
- this.redo.length = 0
+ this.redoQueue.length = 0
this.workflow.unsaved = true
api.dispatchEvent(
new CustomEvent('graphChanged', { detail: this.activeState })
@@ -80,13 +80,21 @@ export class ChangeTracker {
}
}
+ async undo() {
+ await this.updateState(this.undoQueue, this.redoQueue)
+ }
+
+ async redo() {
+ await this.updateState(this.redoQueue, this.undoQueue)
+ }
+
async undoRedo(e) {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'y') {
- this.updateState(this.redo, this.undo)
+ await this.redo()
return true
} else if (e.key === 'z') {
- this.updateState(this.undo, this.redo)
+ await this.undo()
return true
}
}
@@ -276,4 +284,4 @@ export class ChangeTracker {
}
}
-const globalTracker = new ChangeTracker({} as ComfyWorkflow)
+export const globalTracker = new ChangeTracker({} as ComfyWorkflow)
diff --git a/src/scripts/ui/menu/menu.css b/src/scripts/ui/menu/menu.css
index c01dab45e..4a203d8b5 100644
--- a/src/scripts/ui/menu/menu.css
+++ b/src/scripts/ui/menu/menu.css
@@ -122,24 +122,6 @@
}
/* Menu */
-.comfyui-menu {
- width: 100vw;
- background: var(--comfy-menu-bg);
- color: var(--fg-color);
- font-family: Arial, Helvetica, sans-serif;
- font-size: 0.8em;
- display: flex;
- padding: 4px 8px;
- align-items: center;
- gap: 8px;
- box-sizing: border-box;
- z-index: 1000;
- order: 0;
- grid-column: 1/-1;
- overflow: auto;
- max-height: 90vh;
-}
-
.comfyui-menu>* {
flex-shrink: 0;
}
@@ -183,13 +165,6 @@
flex: auto;
}
-.comfyui-logo {
- font-size: 1.2em;
- margin: 0;
- user-select: none;
- cursor: default;
-}
-
/** Send to workflow widget selection dialog */
.comfy-widget-selection-dialog {
border: none;
diff --git a/src/stores/commandStore.ts b/src/stores/commandStore.ts
new file mode 100644
index 000000000..c299c406b
--- /dev/null
+++ b/src/stores/commandStore.ts
@@ -0,0 +1,87 @@
+import { app } from '@/scripts/app'
+import { api } from '@/scripts/api'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { globalTracker } from '@/scripts/changeTracker'
+import { useSettingStore } from '@/stores/settingStore'
+import { useToastStore } from '@/stores/toastStore'
+import { showTemplateWorkflowsDialog } from '@/services/dialogService'
+
+type Command = () => void | Promise
+
+const getTracker = () =>
+ app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker
+
+export const useCommandStore = defineStore('command', () => {
+ const settingStore = useSettingStore()
+ const commands = ref>({
+ 'Comfy.NewBlankWorkflow': () => {
+ app.workflowManager.setWorkflow(null)
+ app.clean()
+ app.graph.clear()
+ app.workflowManager.activeWorkflow.track()
+ },
+ 'Comfy.OpenWorkflow': () => {
+ app.ui.loadFile()
+ },
+ 'Comfy.LoadDefaultWorkflow': async () => {
+ await app.loadGraphData()
+ },
+ 'Comfy.SaveWorkflow': () => {
+ app.workflowManager.activeWorkflow.save()
+ },
+ 'Comfy.SaveWorkflowAs': () => {
+ app.workflowManager.activeWorkflow.save(true)
+ },
+ 'Comfy.ExportWorkflow': () => {
+ app.menu.exportWorkflow('workflow', 'workflow')
+ },
+ 'Comfy.ExportWorkflowAPI': () => {
+ app.menu.exportWorkflow('workflow_api', 'output')
+ },
+ 'Comfy.Undo': async () => {
+ await getTracker().undo()
+ },
+ 'Comfy.Redo': async () => {
+ await getTracker().redo()
+ },
+ 'Comfy.ClearWorkflow': () => {
+ if (
+ !settingStore.get('Comfy.ComfirmClear') ||
+ confirm('Clear workflow?')
+ ) {
+ app.clean()
+ app.graph.clear()
+ api.dispatchEvent(new CustomEvent('graphCleared'))
+ }
+ },
+ 'Comfy.ResetView': () => {
+ app.resetView()
+ },
+ 'Comfy.OpenClipspace': () => {
+ app['openClipspace']?.()
+ },
+ 'Comfy.RefreshNodeDefinitions': async () => {
+ await app.refreshComboInNodes()
+ },
+ 'Comfy.Interrupt': async () => {
+ await api.interrupt()
+ useToastStore().add({
+ severity: 'info',
+ summary: 'Interrupted',
+ detail: 'Execution has been interrupted',
+ life: 1000
+ })
+ },
+ 'Comfy.BrowseTemplates': showTemplateWorkflowsDialog
+ })
+
+ const getCommand = (command: string) => {
+ return commands.value[command] ?? (() => {})
+ }
+
+ return {
+ commands,
+ getCommand
+ }
+})
diff --git a/src/stores/coreMenuItemStore.ts b/src/stores/coreMenuItemStore.ts
new file mode 100644
index 000000000..e63bf9612
--- /dev/null
+++ b/src/stores/coreMenuItemStore.ts
@@ -0,0 +1,85 @@
+import { defineStore } from 'pinia'
+import type { MenuItem } from 'primevue/menuitem'
+import { computed } from 'vue'
+import { useCommandStore } from './commandStore'
+
+export const useCoreMenuItemStore = defineStore('coreMenuItem', () => {
+ const commandStore = useCommandStore()
+ const menuItems = computed