mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +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 {
|
||||
public readonly sideToolbar: Locator
|
||||
public readonly themeToggleButton: Locator
|
||||
@@ -257,6 +267,10 @@ class ComfyMenu {
|
||||
return new WorkflowsSidebarTab(this.page)
|
||||
}
|
||||
|
||||
get topbar() {
|
||||
return new Topbar(this.page)
|
||||
}
|
||||
|
||||
async toggleTheme() {
|
||||
await this.themeToggleButton.click()
|
||||
await this.page.evaluate(() => {
|
||||
|
||||
@@ -381,6 +381,11 @@ test.describe('Menu', () => {
|
||||
|
||||
test.describe('Workflows sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
|
||||
// Open the sidebar
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
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 }) => {
|
||||
const [defaultSpeed, maxSpeed] = [1.1, 2.5]
|
||||
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
:placeholder="$t('searchWorkflows') + '...'"
|
||||
/>
|
||||
<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" />
|
||||
<TreeExplorer
|
||||
:roots="renderTreeNode(workflowStore.openWorkflowsTree).children"
|
||||
@@ -120,6 +123,12 @@ import { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
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 isSearching = computed(() => searchQuery.value.length > 0)
|
||||
|
||||
@@ -9,17 +9,24 @@
|
||||
rootList: 'gap-0'
|
||||
}"
|
||||
/>
|
||||
<Divider layout="vertical" class="mx-2" />
|
||||
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Menubar from 'primevue/menubar'
|
||||
import Divider from 'primevue/divider'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { useCoreMenuItemStore } from '@/stores/coreMenuItemStore'
|
||||
import { computed } from 'vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
@@ -1,16 +1,135 @@
|
||||
<template>
|
||||
<div class="workflow-tabs">
|
||||
<SelectButton
|
||||
v-model="workflowStore.activeWorkflow"
|
||||
:options="workflowStore.openWorkflows"
|
||||
aria-labelledby="basic"
|
||||
/>
|
||||
class="select-button-group bg-transparent"
|
||||
:modelValue="selectedWorkflow"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComfyWorkflow } from '@/scripts/workflows'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
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>
|
||||
|
||||
<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,
|
||||
type: 'combo',
|
||||
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.Workflow.ModelDownload.AllowedSources': 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.Window.UnloadConfirmation': z.boolean(),
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': zNodeBadgeMode,
|
||||
|
||||
@@ -135,6 +135,11 @@ const init = () => {
|
||||
id: 'workflows',
|
||||
icon: 'pi pi-folder-open',
|
||||
iconBadge: () => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const value = useWorkflowStore().openWorkflows.length.toString()
|
||||
return value === '0' ? null : value
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user