Skip to content

Commit

Permalink
Show opened workflows as topbar tabs (#952)
Browse files Browse the repository at this point in the history
* Basic tab switching

* Closing tabs

* Style buttons

* wip

* Fix scroll style

* Add setting

* Add playwright test

* Add unsaved status

* nit
  • Loading branch information
huchenlei committed Sep 24, 2024
1 parent f8a8f1c commit db610ae
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 5 deletions.
14 changes: 14 additions & 0 deletions browser_tests/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,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
Expand Down Expand Up @@ -248,6 +258,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(() => {
Expand Down
20 changes: 20 additions & 0 deletions browser_tests/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 10 additions & 1 deletion src/components/sidebar/tabs/WorkflowsSidebarTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/components/topbar/TopMenubar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
)
Expand Down
127 changes: 123 additions & 4 deletions src/components/topbar/WorkflowTabs.vue
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 7 additions & 0 deletions src/stores/coreSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
1 change: 1 addition & 0 deletions src/types/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/views/GraphView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down

0 comments on commit db610ae

Please sign in to comment.