Skip to content

Commit 1217bcf

Browse files
huchenleigithub-actions
andauthored
Workflow sidebar tab (Step1) (#265)
* Fix canvas not init issue (#283) * Fix copy paste of widget value (#284) * Fix copy paste of widget value * Fix ui tests * Allow undefined group font size * Update test expectations [skip ci] * nit --------- Co-authored-by: github-actions <github-actions@github.com> * 1.2.9 (#285) * WIP * Add refresh button * Add context menu * nit * Add selection mode * Editable text * Fix relative path * implement node delete * Dynamic menu items * Fix refresh * Better dynamic handling of menu items * Disable rename / delete for root * Add workflow download * Auto select file name * Create workflow * Generate non-dup name * Fix folder name * Rename workflwoStore to userFileStore * load workflow when leaf node selected * Extract common report error logic * Basic workflows tab test * Auto expand * Add test on add/remove workflow --------- Co-authored-by: github-actions <github-actions@github.com>
1 parent d02b074 commit 1217bcf

21 files changed

+676
-115
lines changed

browser_tests/ComfyPage.ts

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,42 +37,107 @@ class ComfyNodeSearchBox {
3737
}
3838
}
3939

40-
class NodeLibrarySideBarTab {
41-
public readonly tabId: string = 'node-library'
42-
constructor(public readonly page: Page) {}
43-
44-
get tabButton() {
40+
class SideBarTab {
41+
constructor(
42+
public readonly page: Page,
43+
public readonly tabId: string,
44+
public readonly tabName: string
45+
) {}
46+
47+
public get tabButton() {
4548
return this.page.locator(`.${this.tabId}-tab-button`)
4649
}
4750

48-
get selectedTabButton() {
51+
public get selectedTabButton() {
4952
return this.page.locator(
5053
`.${this.tabId}-tab-button.side-bar-button-selected`
5154
)
5255
}
5356

54-
get nodeLibraryTree() {
55-
return this.page.locator('.node-lib-tree')
57+
public get tabHeader() {
58+
return this.page.locator(`.comfy-vue-side-bar-header`)
5659
}
5760

58-
get nodePreview() {
59-
return this.page.locator('.node-lib-node-preview')
61+
public get tabBody() {
62+
return this.page.locator(`.comfy-vue-side-bar-body`)
6063
}
6164

62-
async open() {
63-
if (await this.selectedTabButton.isVisible()) {
65+
public async isOpen() {
66+
return await this.selectedTabButton.isVisible()
67+
}
68+
69+
public async open() {
70+
if (await this.isOpen()) {
6471
return
6572
}
6673

6774
await this.tabButton.click()
68-
await this.nodeLibraryTree.waitFor({ state: 'visible' })
75+
await this.tabBody.waitFor({ state: 'visible' })
76+
}
77+
78+
public async close() {
79+
if (!(await this.isOpen())) {
80+
return
81+
}
82+
83+
await this.tabButton.click()
84+
await this.tabBody.waitFor({ state: 'hidden' })
85+
}
86+
}
87+
88+
class NodeLibrarySideBarTab extends SideBarTab {
89+
constructor(public readonly page: Page) {
90+
super(page, 'node-library', 'Node Library')
91+
}
92+
93+
get nodeLibraryTree() {
94+
return this.page.locator('.node-lib-tree')
95+
}
96+
97+
get nodePreview() {
98+
return this.page.locator('.node-lib-node-preview')
6999
}
70100

71101
async toggleFirstFolder() {
72102
await this.page.locator('.p-tree-node-toggle-button').nth(0).click()
73103
}
74104
}
75105

106+
class WorkflowsSideBarTab extends SideBarTab {
107+
public readonly testWorkflowName = 'test_workflow.json'
108+
109+
constructor(public readonly page: Page) {
110+
super(page, 'workflows', 'Workflows')
111+
}
112+
113+
get workflowTreeRoot() {
114+
return this.page.locator('.p-tree-node[aria-label="workflows"]')
115+
}
116+
117+
get testWorkflowItem() {
118+
return this.page.locator(
119+
`.p-tree-node[aria-label="${this.testWorkflowName}"]`
120+
)
121+
}
122+
123+
async addWorkflow(workflowName: string = this.testWorkflowName) {
124+
await this.workflowTreeRoot.click({ button: 'right' })
125+
await this.page.getByLabel('New Workflow').locator('a').click()
126+
const textbox = this.workflowTreeRoot.getByRole('textbox')
127+
await textbox.fill(workflowName)
128+
await textbox.press('Enter')
129+
await this.page.waitForTimeout(100)
130+
}
131+
132+
async removeWorkflow(workflowName: string = this.testWorkflowName) {
133+
await this.workflowTreeRoot
134+
.getByText(workflowName)
135+
.click({ button: 'right' })
136+
await this.page.getByLabel('Delete').locator('a').click()
137+
await this.page.waitForTimeout(100)
138+
}
139+
}
140+
76141
class ComfyMenu {
77142
public readonly sideToolBar: Locator
78143
public readonly themeToggleButton: Locator
@@ -86,6 +151,10 @@ class ComfyMenu {
86151
return new NodeLibrarySideBarTab(this.page)
87152
}
88153

154+
get workflowsTab() {
155+
return new WorkflowsSideBarTab(this.page)
156+
}
157+
89158
async toggleTheme() {
90159
await this.themeToggleButton.click()
91160
await this.page.evaluate(() => {

browser_tests/copyPaste.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,28 @@ test.describe('Copy Paste', () => {
2222
expect(resultString).toBe(originalString + originalString)
2323
})
2424

25+
test('Can copy and paste widget value', async ({ comfyPage }) => {
26+
// Copy width value (512) from empty latent node to KSampler's seed.
27+
// Empty latent node's width
28+
await comfyPage.canvas.click({
29+
position: {
30+
x: 718,
31+
y: 643
32+
}
33+
})
34+
await comfyPage.ctrlC()
35+
// KSampler's seed
36+
await comfyPage.canvas.click({
37+
position: {
38+
x: 1005,
39+
y: 281
40+
}
41+
})
42+
await comfyPage.ctrlV()
43+
await comfyPage.page.keyboard.press('Enter')
44+
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
45+
})
46+
2547
/**
2648
* https://github.com/Comfy-Org/ComfyUI_frontend/issues/98
2749
*/
Loading

browser_tests/menu.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,24 @@ test.describe('Menu', () => {
9292
// Verify the node is added to the canvas
9393
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
9494
})
95+
96+
test.describe('Workflows sidebar tab', () => {
97+
test('Can open and close workflows tab', async ({ comfyPage }) => {
98+
const tab = comfyPage.menu.workflowsTab
99+
await tab.open()
100+
expect(await tab.isOpen()).toBe(true)
101+
102+
await tab.close()
103+
expect(await tab.isOpen()).toBe(false)
104+
})
105+
106+
test('Can add / remove workflow', async ({ comfyPage }) => {
107+
const tab = comfyPage.menu.workflowsTab
108+
await tab.open()
109+
await tab.addWorkflow()
110+
expect(await tab.testWorkflowItem.isVisible()).toBe(true)
111+
await tab.removeWorkflow()
112+
expect(await tab.testWorkflowItem.isVisible()).toBe(false)
113+
})
114+
})
95115
})

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "comfyui-frontend",
33
"private": true,
4-
"version": "1.2.8",
4+
"version": "1.2.9",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src/App.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
33
<BlockUI full-screen :blocked="isLoading" />
44
<GraphCanvas />
5+
<Toast />
56
</template>
67

78
<script setup lang="ts">
89
import { computed, markRaw, onMounted, watch } from 'vue'
10+
import Toast from 'primevue/toast'
911
import BlockUI from 'primevue/blockui'
1012
import ProgressSpinner from 'primevue/progressspinner'
1113
import GraphCanvas from '@/components/graph/GraphCanvas.vue'
1214
import QueueSideBarTab from '@/components/sidebar/tabs/QueueSideBarTab.vue'
15+
import WorkflowsSideBarTab from '@/components/sidebar/tabs/WorkflowsSideBarTab.vue'
1316
import { app } from './scripts/app'
1417
import { useSettingStore } from './stores/settingStore'
1518
import { useI18n } from 'vue-i18n'
@@ -54,6 +57,14 @@ const init = () => {
5457
component: markRaw(NodeLibrarySideBarTab),
5558
type: 'vue'
5659
})
60+
app.extensionManager.registerSidebarTab({
61+
id: 'workflows',
62+
icon: 'pi pi-copy',
63+
title: t('sideToolBar.workflows'),
64+
tooltip: t('sideToolBar.workflows'),
65+
component: markRaw(WorkflowsSideBarTab),
66+
type: 'vue'
67+
})
5768
}
5869
5970
onMounted(() => {

src/components/graph/GraphCanvas.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const nodeSearchEnabled = computed<boolean>(
3333
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
3434
)
3535
watch(nodeSearchEnabled, (newVal) => {
36-
comfyApp.canvas.allow_searchbox = !newVal
36+
if (comfyApp.canvas) comfyApp.canvas.allow_searchbox = !newVal
3737
})
3838
3939
let dropTargetCleanup = () => {}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<template>
2+
<div class="editable-text">
3+
<span v-if="!props.isEditing">
4+
{{ modelValue }}
5+
</span>
6+
<InputText
7+
v-else
8+
type="text"
9+
size="small"
10+
fluid
11+
v-model:modelValue="inputValue"
12+
ref="inputRef"
13+
@keyup.enter="finishEditing"
14+
:pt="{
15+
root: {
16+
onBlur: finishEditing
17+
}
18+
}"
19+
v-focus
20+
/>
21+
</div>
22+
</template>
23+
24+
<script setup lang="ts">
25+
import InputText from 'primevue/inputtext'
26+
import { nextTick, ref, watch } from 'vue'
27+
28+
const props = defineProps({
29+
modelValue: {
30+
type: String,
31+
required: true
32+
},
33+
isEditing: {
34+
type: Boolean,
35+
default: false
36+
}
37+
})
38+
39+
const emit = defineEmits(['update:modelValue', 'edit'])
40+
41+
const inputValue = ref<string>(props.modelValue)
42+
const isEditingFinished = ref<boolean>(false)
43+
const inputRef = ref(null)
44+
45+
const finishEditing = () => {
46+
if (isEditingFinished.value) {
47+
return
48+
}
49+
isEditingFinished.value = true
50+
emit('edit', inputValue.value)
51+
}
52+
53+
watch(
54+
() => props.isEditing,
55+
(newVal) => {
56+
if (newVal) {
57+
inputValue.value = props.modelValue
58+
isEditingFinished.value = false
59+
60+
nextTick(() => {
61+
if (!inputRef.value) return
62+
const fileName = inputValue.value.split('.').slice(0, -1).join('.')
63+
const start = 0
64+
const end = fileName.length
65+
66+
const inputElement = inputRef.value.$el
67+
inputElement.setSelectionRange(start, end)
68+
})
69+
}
70+
}
71+
)
72+
73+
const vFocus = {
74+
mounted: (el: HTMLElement) => el.focus()
75+
}
76+
</script>
77+
78+
<style scoped>
79+
.editable-text {
80+
display: inline-block;
81+
min-width: 50px;
82+
padding: 2px;
83+
cursor: pointer;
84+
}
85+
86+
.editable-text input {
87+
width: 100%;
88+
box-sizing: border-box;
89+
}
90+
</style>

src/components/sidebar/tabs/QueueSideBarTab.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
}"
3535
>
3636
<template #header>
37-
<Toast />
3837
<ConfirmPopup />
3938
<Button
4039
icon="pi pi-trash"
@@ -72,7 +71,6 @@ import Column from 'primevue/column'
7271
import Tag from 'primevue/tag'
7372
import Button from 'primevue/button'
7473
import ConfirmPopup from 'primevue/confirmpopup'
75-
import Toast from 'primevue/toast'
7674
import Message from 'primevue/message'
7775
import { useConfirm } from 'primevue/useconfirm'
7876
import { useToast } from 'primevue/usetoast'

0 commit comments

Comments
 (0)