mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
Show opened workflows as topbar tabs (#952)
* Basic tab switching * Closing tabs * Style buttons * wip * Fix scroll style * Add setting * Add playwright test * Add unsaved status * nit
This commit is contained in:
@@ -221,6 +221,16 @@ class WorkflowsSidebarTab extends SidebarTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Topbar {
|
||||||
|
constructor(public readonly page: Page) {}
|
||||||
|
|
||||||
|
async getTabNames(): Promise<string[]> {
|
||||||
|
return await this.page
|
||||||
|
.locator('.workflow-tabs .workflow-label')
|
||||||
|
.allInnerTexts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ComfyMenu {
|
class ComfyMenu {
|
||||||
public readonly sideToolbar: Locator
|
public readonly sideToolbar: Locator
|
||||||
public readonly themeToggleButton: Locator
|
public readonly themeToggleButton: Locator
|
||||||
@@ -257,6 +267,10 @@ class ComfyMenu {
|
|||||||
return new WorkflowsSidebarTab(this.page)
|
return new WorkflowsSidebarTab(this.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get topbar() {
|
||||||
|
return new Topbar(this.page)
|
||||||
|
}
|
||||||
|
|
||||||
async toggleTheme() {
|
async toggleTheme() {
|
||||||
await this.themeToggleButton.click()
|
await this.themeToggleButton.click()
|
||||||
await this.page.evaluate(() => {
|
await this.page.evaluate(() => {
|
||||||
|
|||||||
@@ -381,6 +381,11 @@ test.describe('Menu', () => {
|
|||||||
|
|
||||||
test.describe('Workflows sidebar', () => {
|
test.describe('Workflows sidebar', () => {
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting(
|
||||||
|
'Comfy.Workflow.WorkflowTabsPosition',
|
||||||
|
'Sidebar'
|
||||||
|
)
|
||||||
|
|
||||||
// Open the sidebar
|
// Open the sidebar
|
||||||
const tab = comfyPage.menu.workflowsTab
|
const tab = comfyPage.menu.workflowsTab
|
||||||
await tab.open()
|
await tab.open()
|
||||||
@@ -434,6 +439,21 @@ test.describe('Menu', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Workflows topbar tabs', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting(
|
||||||
|
'Comfy.Workflow.WorkflowTabsPosition',
|
||||||
|
'Topbar'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can show opened workflows', async ({ comfyPage }) => {
|
||||||
|
expect(await comfyPage.menu.topbar.getTabNames()).toEqual([
|
||||||
|
'Unsaved Workflow'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||||
const [defaultSpeed, maxSpeed] = [1.1, 2.5]
|
const [defaultSpeed, maxSpeed] = [1.1, 2.5]
|
||||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
|
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
|
||||||
|
|||||||
@@ -38,7 +38,10 @@
|
|||||||
:placeholder="$t('searchWorkflows') + '...'"
|
:placeholder="$t('searchWorkflows') + '...'"
|
||||||
/>
|
/>
|
||||||
<div class="comfyui-workflows-panel" v-if="!isSearching">
|
<div class="comfyui-workflows-panel" v-if="!isSearching">
|
||||||
<div class="comfyui-workflows-open">
|
<div
|
||||||
|
class="comfyui-workflows-open"
|
||||||
|
v-if="workflowTabsPosition === 'Sidebar'"
|
||||||
|
>
|
||||||
<TextDivider text="Open" type="dashed" class="ml-2" />
|
<TextDivider text="Open" type="dashed" class="ml-2" />
|
||||||
<TreeExplorer
|
<TreeExplorer
|
||||||
:roots="renderTreeNode(workflowStore.openWorkflowsTree).children"
|
:roots="renderTreeNode(workflowStore.openWorkflowsTree).children"
|
||||||
@@ -120,6 +123,12 @@ import { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
|||||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const workflowTabsPosition = computed(() =>
|
||||||
|
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||||
|
)
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const isSearching = computed(() => searchQuery.value.length > 0)
|
const isSearching = computed(() => searchQuery.value.length > 0)
|
||||||
|
|||||||
@@ -9,17 +9,24 @@
|
|||||||
rootList: 'gap-0'
|
rootList: 'gap-0'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<Divider layout="vertical" class="mx-2" />
|
||||||
|
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
||||||
</div>
|
</div>
|
||||||
</teleport>
|
</teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Menubar from 'primevue/menubar'
|
import Menubar from 'primevue/menubar'
|
||||||
|
import Divider from 'primevue/divider'
|
||||||
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
import { useCoreMenuItemStore } from '@/stores/coreMenuItemStore'
|
import { useCoreMenuItemStore } from '@/stores/coreMenuItemStore'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const workflowTabsPosition = computed(() =>
|
||||||
|
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||||
|
)
|
||||||
const betaMenuEnabled = computed(
|
const betaMenuEnabled = computed(
|
||||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="workflow-tabs">
|
<div class="workflow-tabs">
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="workflowStore.activeWorkflow"
|
class="select-button-group bg-transparent"
|
||||||
:options="workflowStore.openWorkflows"
|
:modelValue="selectedWorkflow"
|
||||||
aria-labelledby="basic"
|
@update:modelValue="onWorkflowChange"
|
||||||
/>
|
:options="options"
|
||||||
|
optionLabel="label"
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
<template #option="{ option }">
|
||||||
|
<span
|
||||||
|
class="workflow-label text-sm max-w-[150px] truncate inline-block"
|
||||||
|
>{{ option.label }}</span
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="status-indicator" v-if="option.unsaved">•</span>
|
||||||
|
<Button
|
||||||
|
class="close-button p-0 w-auto"
|
||||||
|
icon="pi pi-times"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
size="small"
|
||||||
|
@click.stop="onCloseWorkflow(option)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SelectButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||||
import SelectButton from 'primevue/selectbutton'
|
import SelectButton from 'primevue/selectbutton'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
|
interface WorkflowOption {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
unsaved: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowToOption = (workflow: ComfyWorkflow): WorkflowOption => ({
|
||||||
|
label: workflow.name,
|
||||||
|
value: workflow.key,
|
||||||
|
unsaved: workflow.unsaved
|
||||||
|
})
|
||||||
|
|
||||||
|
const optionToWorkflow = (option: WorkflowOption): ComfyWorkflow =>
|
||||||
|
workflowStore.workflowLookup[option.value]
|
||||||
|
|
||||||
|
const options = computed<WorkflowOption[]>(() =>
|
||||||
|
workflowStore.openWorkflows.map(workflowToOption)
|
||||||
|
)
|
||||||
|
const selectedWorkflow = computed<WorkflowOption | null>(() =>
|
||||||
|
workflowStore.activeWorkflow
|
||||||
|
? workflowToOption(workflowStore.activeWorkflow as ComfyWorkflow)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
const onWorkflowChange = (option: WorkflowOption) => {
|
||||||
|
// Prevent unselecting the current workflow
|
||||||
|
if (!option) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Prevent reloading the current workflow
|
||||||
|
if (selectedWorkflow.value?.value === option.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = optionToWorkflow(option)
|
||||||
|
workflow.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCloseWorkflow = (option: WorkflowOption) => {
|
||||||
|
const workflow = optionToWorkflow(option)
|
||||||
|
app.workflowManager.closeWorkflow(workflow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this new function to check if a workflow is unsaved
|
||||||
|
const isWorkflowUnsaved = (option: WorkflowOption): boolean => {
|
||||||
|
const workflow = optionToWorkflow(option)
|
||||||
|
return workflow.unsaved
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.select-button-group {
|
||||||
|
/* TODO: Make this dynamic. Take rest of space after all tool buttons */
|
||||||
|
max-width: 70vw;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-togglebutton::before) {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-togglebutton) {
|
||||||
|
@apply px-2 bg-transparent rounded-none flex-shrink-0 relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-togglebutton.p-togglebutton-checked) {
|
||||||
|
@apply border-b-2;
|
||||||
|
border-bottom-color: var(--p-button-text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-togglebutton-checked) .close-button,
|
||||||
|
:deep(.p-togglebutton:hover) .close-button {
|
||||||
|
@apply visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
@apply absolute font-bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-togglebutton:hover) .status-indicator {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-togglebutton) .close-button {
|
||||||
|
@apply invisible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -380,5 +380,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
experimental: true,
|
experimental: true,
|
||||||
type: 'combo',
|
type: 'combo',
|
||||||
options: ['Disabled', 'Floating']
|
options: ['Disabled', 'Floating']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.Workflow.WorkflowTabsPosition',
|
||||||
|
name: 'Opened workflows position',
|
||||||
|
type: 'combo',
|
||||||
|
options: ['Sidebar', 'Topbar'],
|
||||||
|
defaultValue: 'Sidebar'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ const zSettings = z.record(z.any()).and(
|
|||||||
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
||||||
'Comfy.Workflow.ModelDownload.AllowedSources': z.array(z.string()),
|
'Comfy.Workflow.ModelDownload.AllowedSources': z.array(z.string()),
|
||||||
'Comfy.Workflow.ModelDownload.AllowedSuffixes': z.array(z.string()),
|
'Comfy.Workflow.ModelDownload.AllowedSuffixes': z.array(z.string()),
|
||||||
|
'Comfy.Workflow.WorkflowTabsPosition': z.enum(['Sidebar', 'Topbar']),
|
||||||
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
'Comfy.Node.DoubleClickTitleToEdit': z.boolean(),
|
||||||
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
'Comfy.Window.UnloadConfirmation': z.boolean(),
|
||||||
'Comfy.NodeBadge.NodeSourceBadgeMode': zNodeBadgeMode,
|
'Comfy.NodeBadge.NodeSourceBadgeMode': zNodeBadgeMode,
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ const init = () => {
|
|||||||
id: 'workflows',
|
id: 'workflows',
|
||||||
icon: 'pi pi-folder-open',
|
icon: 'pi pi-folder-open',
|
||||||
iconBadge: () => {
|
iconBadge: () => {
|
||||||
|
if (
|
||||||
|
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const value = useWorkflowStore().openWorkflows.length.toString()
|
const value = useWorkflowStore().openWorkflows.length.toString()
|
||||||
return value === '0' ? null : value
|
return value === '0' ? null : value
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user