Skip to content

Commit ffb4ae3

Browse files
pythongosssssgithub-actionsclaude
authored
feat: App mode UI updates (#13170)
## Summary App mode now uses the same sidenav as the graph view instead of its own custom navigation, and switching between modes happens through a new animated graph/app toggle shared by both views. ## Changes - **What**: - LinearView hosts the standard `SideToolbar`, filtered to the Assets and Apps tabs via new `visibleTabIds`/`forceConnected` props. The bottom-panel and shortcut toggles hide in app mode, and GraphCanvas/SubgraphBreadcrumb hide their instances so only one toolbar and toggle render at a time. - The workflow actions dropdown trigger is now a two-segment graph/app toggle: the inactive segment switches modes directly, the active one opens the actions menu as before, with a FLIP animation on switch. - Each view renders its own toggle, so switching unmounts one and mounts the other. A `displayLinearMode` ref in canvasStore trails `linearMode` by two frames so the incoming toggle animates from the old mode instead of popping in already-switched. - AppModeToolbar slims down to the toggle plus a "Build an app" button; the per-icon Assets/Apps/Share buttons are covered by the sidenav. - The Apps tab gains "Create" header and empty-state actions wired to `Comfy.NewBlankWorkflow`. - App-mode feedback moves into `SidebarHelpCenterIcon` as a Typeform popover with a load-error fallback, replacing `TypeformPopoverButton` and `LinearFeedback` and simplifying LinearPreview to a single `OutputHistory`. - Tests for the toggle, sidebar tabs, help-center fallback, preview, and view; screenshots regenerated. ## Review Focus - The two-frame `displayLinearMode` lag — timing-sensitive; rapid toggling cancels the pending frame chain so the wrong mode never flashes. - `SideToolbar` now serves both views: confirm only one instance renders and the new props behave. ## Screenshots (if applicable) https://github.com/user-attachments/assets/9ad1eb4f-9370-4975-b3f1-b271482f2bf7 --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 59341a4 commit ffb4ae3

63 files changed

Lines changed: 2082 additions & 408 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

browser_tests/fixtures/ComfyPage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ModelLibrarySidebarTab,
2929
NodeLibrarySidebarTab,
3030
NodeLibrarySidebarTabV2,
31+
SidebarTab,
3132
WorkflowsSidebarTab
3233
} from '@e2e/fixtures/components/SidebarTab'
3334
import { Topbar } from '@e2e/fixtures/components/Topbar'
@@ -70,6 +71,7 @@ class ComfyPropertiesPanel {
7071
}
7172

7273
class ComfyMenu {
74+
private _appsTab: SidebarTab | null = null
7375
private _assetsTab: AssetsSidebarTab | null = null
7476
private _modelLibraryTab: ModelLibrarySidebarTab | null = null
7577
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
@@ -104,6 +106,11 @@ class ComfyMenu {
104106
return this._nodeLibraryTabV2
105107
}
106108

109+
get appsTab() {
110+
this._appsTab ??= new SidebarTab(this.page, 'apps')
111+
return this._appsTab
112+
}
113+
107114
get assetsTab() {
108115
this._assetsTab ??= new AssetsSidebarTab(this.page)
109116
return this._assetsTab

browser_tests/fixtures/components/SidebarTab.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { expect } from '@playwright/test'
44
import type { WorkspaceStore } from '@e2e/types/globals'
55
import { TestIds } from '@e2e/fixtures/selectors'
66

7-
class SidebarTab {
7+
export class SidebarTab {
88
public readonly tabButton: Locator
99
public readonly selectedTabButton: Locator
1010

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Locator, Page } from '@playwright/test'
2+
3+
import { TestIds } from '@e2e/fixtures/selectors'
4+
5+
/**
6+
* The graph/app view-mode toggle and its workflow actions dropdown.
7+
* A separate instance mounts in each host - the subgraph breadcrumb (graph
8+
* mode) and the app-mode center panel - and unmounts as the mode flips.
9+
*/
10+
export class WorkflowActionsDropdown {
11+
/** The segmented graph/app toggle hosting the workflow actions trigger. */
12+
public readonly viewModeToggle: Locator
13+
/** The active segment; opens the workflow actions menu. */
14+
public readonly trigger: Locator
15+
/** The inactive segment that switches into app mode. */
16+
public readonly enterAppModeSegment: Locator
17+
/** The inactive segment that switches back to the node graph. */
18+
public readonly enterGraphSegment: Locator
19+
/** The workflow actions dropdown menu. */
20+
public readonly menu: Locator
21+
22+
constructor(page: Page) {
23+
this.viewModeToggle = page.getByTestId(
24+
TestIds.workflowActions.viewModeToggle
25+
)
26+
this.trigger = this.triggerIn(this.viewModeToggle)
27+
this.enterAppModeSegment = this.viewModeToggle.getByRole('button', {
28+
name: 'Enter app mode'
29+
})
30+
this.enterGraphSegment = this.viewModeToggle.getByRole('button', {
31+
name: 'Enter node graph'
32+
})
33+
this.menu = page.getByRole('menu', { name: 'Workflow actions' })
34+
}
35+
36+
/** The trigger as rendered inside a specific mode's host. */
37+
triggerIn(host: Locator): Locator {
38+
return host.getByRole('button', { name: 'Workflow actions' })
39+
}
40+
}

browser_tests/fixtures/helpers/AppModeHelper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
44
import { TestIds } from '@e2e/fixtures/selectors'
55

66
import { OutputHistoryComponent } from '@e2e/fixtures/components/OutputHistory'
7+
import { WorkflowActionsDropdown } from '@e2e/fixtures/components/WorkflowActionsDropdown'
78
import { AppModeWidgetHelper } from '@e2e/fixtures/helpers/AppModeWidgetHelper'
89
import { BuilderFooterHelper } from '@e2e/fixtures/helpers/BuilderFooterHelper'
910
import { BuilderSaveAsHelper } from '@e2e/fixtures/helpers/BuilderSaveAsHelper'
@@ -19,6 +20,7 @@ export class AppModeHelper {
1920
readonly outputHistory: OutputHistoryComponent
2021
readonly steps: BuilderStepsHelper
2122
readonly widgets: AppModeWidgetHelper
23+
readonly workflowActions: WorkflowActionsDropdown
2224

2325
/** The "Connect an output" popover shown when saving without outputs. */
2426
public readonly connectOutputPopover: Locator
@@ -77,6 +79,7 @@ export class AppModeHelper {
7779
this.outputHistory = new OutputHistoryComponent(comfyPage.page)
7880
this.steps = new BuilderStepsHelper(comfyPage)
7981
this.widgets = new AppModeWidgetHelper(comfyPage)
82+
this.workflowActions = new WorkflowActionsDropdown(comfyPage.page)
8083

8184
this.connectOutputPopover = this.page.getByTestId(
8285
TestIds.builder.connectOutputPopover
@@ -185,10 +188,7 @@ export class AppModeHelper {
185188
.waitFor({ state: 'hidden', timeout: 5000 })
186189
.catch(() => {})
187190

188-
await this.page
189-
.getByRole('button', { name: 'Workflow actions' })
190-
.first()
191-
.click()
191+
await this.workflowActions.trigger.click()
192192
await this.page
193193
.getByRole('menuitem', { name: /Build app|Edit app/ })
194194
.click()

browser_tests/fixtures/selectors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ export const TestIds = {
239239
renameInput: 'subgraph-breadcrumb-rename-input',
240240
menu: (key: string) => `subgraph-breadcrumb-menu-${key}`
241241
},
242+
workflowActions: {
243+
viewModeToggle: 'view-mode-toggle'
244+
},
242245
templates: {
243246
content: 'template-workflows-content',
244247
workflowCard: (id: string) => `template-workflow-${id}`

browser_tests/tests/appMode.spec.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import { mergeTests } from '@playwright/test'
2+
13
import {
2-
comfyPageFixture as test,
3-
comfyExpect as expect
4+
comfyExpect as expect,
5+
comfyPageFixture
46
} from '@e2e/fixtures/ComfyPage'
7+
import { subgraphBreadcrumbFixture } from '@e2e/fixtures/helpers/SubgraphBreadcrumbHelper'
58
import { TestIds } from '@e2e/fixtures/selectors'
69

10+
const test = mergeTests(comfyPageFixture, subgraphBreadcrumbFixture)
11+
712
test.describe('App mode usage', () => {
813
test('Drag and Drop @vue-nodes', async ({ comfyPage, comfyFiles }) => {
914
const { centerPanel } = comfyPage.appMode
@@ -137,6 +142,117 @@ test.describe('App mode usage', () => {
137142
await expect.poll(() => fileComboWidget.getValue()).toBe(targetImage)
138143
})
139144

145+
test('Shows a single side toolbar per mode, filtered to assets + apps in app mode', async ({
146+
comfyPage
147+
}) => {
148+
const { sideToolbar, nodeLibraryTab, assetsTab, appsTab } = comfyPage.menu
149+
150+
await test.step('Graph mode shows the full toolbar', async () => {
151+
await expect(sideToolbar).toHaveCount(1)
152+
await expect(nodeLibraryTab.tabButton).toBeVisible()
153+
})
154+
155+
await test.step('App mode shows only assets + apps', async () => {
156+
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
157+
await expect(comfyPage.appMode.centerPanel).toBeVisible()
158+
159+
await expect(sideToolbar).toHaveCount(1)
160+
await expect(assetsTab.tabButton).toBeVisible()
161+
await expect(appsTab.tabButton).toBeVisible()
162+
await expect(nodeLibraryTab.tabButton).toBeHidden()
163+
})
164+
})
165+
166+
test('Workflow actions menu keeps the same position across graph/app mode', async ({
167+
comfyPage,
168+
subgraphBreadcrumb
169+
}) => {
170+
const { workflowActions, centerPanel } = comfyPage.appMode
171+
172+
// Toggling graph<->app mode happens from this control, so it must not move
173+
// out from under the cursor as the mode flips.
174+
const graphActions = workflowActions.triggerIn(
175+
subgraphBreadcrumb.panel.root
176+
)
177+
await expect(graphActions).toBeVisible()
178+
const graphBox = await graphActions.boundingBox()
179+
180+
expect(graphBox).not.toBeNull()
181+
182+
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
183+
await expect(centerPanel).toBeVisible()
184+
185+
const appActions = workflowActions.triggerIn(centerPanel)
186+
await expect(appActions).toBeVisible()
187+
188+
// The toggle segments reorder (morph) as the mode flips, so poll until the
189+
// active control settles at the same x it occupied in graph mode.
190+
await expect
191+
.poll(async () => {
192+
const box = await appActions.boundingBox()
193+
return box ? Math.abs(box.x - graphBox!.x) : Infinity
194+
})
195+
.toBeLessThanOrEqual(1)
196+
})
197+
198+
test('Toggle segment flips mode without opening the menu', async ({
199+
comfyPage
200+
}) => {
201+
const { workflowActions } = comfyPage.appMode
202+
await expect(workflowActions.viewModeToggle).toBeVisible()
203+
204+
await workflowActions.enterAppModeSegment.click()
205+
206+
await expect(comfyPage.appMode.centerPanel).toBeVisible()
207+
// The inactive segment switches mode; it must not also open the actions menu.
208+
await expect(workflowActions.menu).toBeHidden()
209+
await expect(workflowActions.viewModeToggle).toBeVisible()
210+
})
211+
212+
test('Toggle segment flips mode via keyboard without opening the menu', async ({
213+
comfyPage
214+
}) => {
215+
const { workflowActions } = comfyPage.appMode
216+
await workflowActions.enterAppModeSegment.focus()
217+
await workflowActions.enterAppModeSegment.press('Enter')
218+
219+
await expect(comfyPage.appMode.centerPanel).toBeVisible()
220+
await expect(workflowActions.menu).toBeHidden()
221+
await expect(workflowActions.trigger).toBeFocused()
222+
})
223+
224+
test('Mode toggle re-appears after exiting the builder to graph mode', async ({
225+
comfyPage
226+
}) => {
227+
const toggle = comfyPage.appMode.workflowActions.viewModeToggle
228+
await comfyPage.appMode.enableLinearMode()
229+
await expect(toggle).toBeVisible()
230+
231+
await comfyPage.appMode.enterBuilder()
232+
await expect(toggle).toBeHidden()
233+
await expect(comfyPage.appMode.centerPanel).toBeHidden()
234+
235+
await comfyPage.appMode.footer.exitButton.click()
236+
// Exiting the builder lands in graph mode: the app-mode-only center panel
237+
// stays hidden while the graph-mode toggle host re-mounts and the toggle
238+
// re-appears.
239+
await expect(toggle).toBeVisible()
240+
await expect(comfyPage.appMode.centerPanel).toBeHidden()
241+
})
242+
243+
test('Mode toggle survives a sidebar tab remounting the app panel', async ({
244+
comfyPage
245+
}) => {
246+
const toggle = comfyPage.appMode.workflowActions.viewModeToggle
247+
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
248+
await expect(comfyPage.appMode.centerPanel).toBeVisible()
249+
await expect(toggle).toBeVisible()
250+
251+
// Opening a sidebar tab remounts the app panel; the toggle re-renders with it.
252+
await comfyPage.menu.assetsTab.tabButton.click()
253+
await expect(toggle).toBeVisible()
254+
})
255+
140256
test.describe('Mobile', { tag: ['@mobile'] }, () => {
141257
test('panel navigation', async ({ comfyPage }) => {
142258
const { mobile } = comfyPage.appMode
-222 Bytes
Loading
-32 Bytes
Loading
59 Bytes
Loading
53 Bytes
Loading

0 commit comments

Comments
 (0)