From 4ff14b5eb9ad1366c3f09a0fc213f98dca540411 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:02:19 +0000 Subject: [PATCH] feat/fix: App mode QA updates (#9439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Various fixes from app mode QA ## Changes - **What**: - fix: prevent inserting nodes from workflow/apps sidebar tabs - fix: hide json extension in workflow tab - fix: hide apps nav button in apps tab when already in apps mode - fix: center text on arrange page - fix: prevent IoItems from "jumping" due to stale transform after drag and drop op - fix: refactor side panels and add custom stable pixel based sizing - fix: make outputs/inputs lists in app builder scrollable - fix: fix rerun not working correctly - feat: add text to interrupt button - feat: add enter app mode button to builder toolbar - feat: add tooltip to download button on linear view - feat: show last output of workflow in arrange tab if available - feat: show download count in download all button, hide if only 1 asset to download ## Review Focus - Rerun - I am not sure why it was triggering widget actions, removing it seemed like the correct fix - useStablePrimeVueSplitter - this is a workaround for the fact it uses percent sizing, I also tried switching to reka-ui splitters, but they also only support % sizing in our version [pixel based looks to have been added in a newer version, will log an issue to upgrade & replace splitters with this] ## Screenshots (if applicable) image image image image image image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9439-feat-fix-App-mode-QA-updates-31a6d73d365081b38337d63207b88817) by [Unito](https://www.unito.io) --- browser_tests/tests/interaction.spec.ts | 11 +- browser_tests/tests/rerouteNode.spec.ts | 4 +- browser_tests/tests/sidebar/workflows.spec.ts | 98 +++---- src/components/builder/AppBuilder.vue | 262 +++++++++--------- src/components/builder/BuilderMenu.vue | 64 +++-- .../sidebar/tabs/AppsSidebarTab.vue | 17 +- .../sidebar/tabs/BaseWorkflowsSidebarTab.vue | 41 +-- .../useStablePrimeVueSplitterSizer.test.ts | 133 +++++++++ .../useStablePrimeVueSplitterSizer.ts | 64 +++++ src/locales/en/main.json | 7 +- .../extensions/linearMode/ImagePreview.vue | 9 +- .../extensions/linearMode/LinearArrange.vue | 37 ++- .../extensions/linearMode/LinearPreview.vue | 71 ++--- .../linearMode/MediaOutputPreview.vue | 55 ++++ src/scripts/ui/draggableList.ts | 14 +- src/views/LinearView.vue | 61 ++-- 16 files changed, 626 insertions(+), 322 deletions(-) create mode 100644 src/composables/useStablePrimeVueSplitterSizer.test.ts create mode 100644 src/composables/useStablePrimeVueSplitterSizer.ts create mode 100644 src/renderer/extensions/linearMode/MediaOutputPreview.vue diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index 5b54ccaba2..73459065f8 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -819,16 +819,13 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { await comfyPage.menu.workflowsTab.getOpenedWorkflowNames() const activeWorkflowName = await comfyPage.menu.workflowsTab.getActiveWorkflowName() - const workflowPathA = `${workflowA}.json` - const workflowPathB = `${workflowB}.json` - expect(openWorkflows).toEqual( - expect.arrayContaining([workflowPathA, workflowPathB]) + expect.arrayContaining([workflowA, workflowB]) ) - expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan( - openWorkflows.indexOf(workflowPathB) + expect(openWorkflows.indexOf(workflowA)).toBeLessThan( + openWorkflows.indexOf(workflowB) ) - expect(activeWorkflowName).toEqual(workflowPathB) + expect(activeWorkflowName).toEqual(workflowB) }) }) diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 93f92d1d50..19c919117c 100644 --- a/browser_tests/tests/rerouteNode.spec.ts +++ b/browser_tests/tests/rerouteNode.spec.ts @@ -13,9 +13,9 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => { }) test('loads from inserted workflow', async ({ comfyPage }) => { - const workflowName = 'single_connected_reroute_node.json' + const workflowName = 'single_connected_reroute_node' await comfyPage.workflow.setupWorkflowsDirectory({ - [workflowName]: 'links/single_connected_reroute_node.json' + [`${workflowName}.json`]: `links/${workflowName}.json` }) await comfyPage.setup() await comfyPage.menu.topbar.triggerTopbarCommand(['New']) diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts index d663b402bc..4b698432e5 100644 --- a/browser_tests/tests/sidebar/workflows.spec.ts +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -21,14 +21,12 @@ test.describe('Workflows sidebar', () => { test('Can create new blank workflow', async ({ comfyPage }) => { const tab = comfyPage.menu.workflowsTab - expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' - ]) + expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow']) await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json', - '*Unsaved Workflow (2).json' + '*Unsaved Workflow', + '*Unsaved Workflow (2)' ]) }) @@ -41,37 +39,37 @@ test.describe('Workflows sidebar', () => { const tab = comfyPage.menu.workflowsTab await tab.open() expect(await tab.getTopLevelSavedWorkflowNames()).toEqual( - expect.arrayContaining(['workflow1.json', 'workflow2.json']) + expect.arrayContaining(['workflow1', 'workflow2']) ) }) test('Can duplicate workflow', async ({ comfyPage }) => { const tab = comfyPage.menu.workflowsTab - await comfyPage.menu.topbar.saveWorkflow('workflow1.json') + await comfyPage.menu.topbar.saveWorkflow('workflow1') expect(await tab.getTopLevelSavedWorkflowNames()).toEqual( - expect.arrayContaining(['workflow1.json']) + expect.arrayContaining(['workflow1']) ) await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ - 'workflow1.json', - '*workflow1 (Copy).json' + 'workflow1', + '*workflow1 (Copy)' ]) await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ - 'workflow1.json', - '*workflow1 (Copy).json', - '*workflow1 (Copy) (2).json' + 'workflow1', + '*workflow1 (Copy)', + '*workflow1 (Copy) (2)' ]) await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ - 'workflow1.json', - '*workflow1 (Copy).json', - '*workflow1 (Copy) (2).json', - '*workflow1 (Copy) (3).json' + 'workflow1', + '*workflow1 (Copy)', + '*workflow1 (Copy) (2)', + '*workflow1 (Copy) (3)' ]) }) @@ -85,12 +83,12 @@ test.describe('Workflows sidebar', () => { await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow') const originalNodeCount = await comfyPage.nodeOps.getNodeCount() - await tab.insertWorkflow(tab.getPersistedItem('workflow1.json')) + await tab.insertWorkflow(tab.getPersistedItem('workflow1')) await expect .poll(() => comfyPage.nodeOps.getNodeCount()) .toEqual(originalNodeCount + 1) - await tab.getPersistedItem('workflow1.json').click() + await tab.getPersistedItem('workflow1').click() await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1) }) @@ -113,22 +111,22 @@ test.describe('Workflows sidebar', () => { const openedWorkflow = tab.getOpenedItem('foo/bar') await tab.renameWorkflow(openedWorkflow, 'foo/baz') expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json', - 'foo/baz.json' + '*Unsaved Workflow', + 'foo/baz' ]) }) test('Can save workflow as', async ({ comfyPage }) => { await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') - await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json') + await comfyPage.menu.topbar.saveWorkflowAs('workflow3') await expect .poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames()) - .toEqual(['*Unsaved Workflow.json', 'workflow3.json']) + .toEqual(['*Unsaved Workflow', 'workflow3']) - await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json') + await comfyPage.menu.topbar.saveWorkflowAs('workflow4') await expect .poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames()) - .toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json']) + .toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4']) }) test('Exported workflow does not contain localized slot names', async ({ @@ -184,15 +182,15 @@ test.describe('Workflows sidebar', () => { }) test('Can save workflow as with same name', async ({ comfyPage }) => { - await comfyPage.menu.topbar.saveWorkflow('workflow5.json') + await comfyPage.menu.topbar.saveWorkflow('workflow5') expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ - 'workflow5.json' + 'workflow5' ]) - await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json') + await comfyPage.menu.topbar.saveWorkflowAs('workflow5') await comfyPage.confirmDialog.click('overwrite') expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ - 'workflow5.json' + 'workflow5' ]) }) @@ -212,25 +210,25 @@ test.describe('Workflows sidebar', () => { test('Can overwrite other workflows with save as', async ({ comfyPage }) => { const topbar = comfyPage.menu.topbar - await topbar.saveWorkflow('workflow1.json') - await topbar.saveWorkflowAs('workflow2.json') + await topbar.saveWorkflow('workflow1') + await topbar.saveWorkflowAs('workflow2') await comfyPage.nextFrame() await expect .poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames()) - .toEqual(['workflow1.json', 'workflow2.json']) + .toEqual(['workflow1', 'workflow2']) await expect .poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName()) - .toEqual('workflow2.json') + .toEqual('workflow2') - await topbar.saveWorkflowAs('workflow1.json') + await topbar.saveWorkflowAs('workflow1') await comfyPage.confirmDialog.click('overwrite') - // The old workflow1.json should be deleted and the new one should be saved. + // The old workflow1 should be deleted and the new one should be saved. await expect .poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames()) - .toEqual(['workflow2.json', 'workflow1.json']) + .toEqual(['workflow2', 'workflow1']) await expect .poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName()) - .toEqual('workflow1.json') + .toEqual('workflow1') }) test('Does not report warning when switching between opened workflows', async ({ @@ -266,17 +264,15 @@ test.describe('Workflows sidebar', () => { ) await closeButton.click() expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' + '*Unsaved Workflow' ]) }) test('Can close saved workflow with command', async ({ comfyPage }) => { const tab = comfyPage.menu.workflowsTab - await comfyPage.menu.topbar.saveWorkflow('workflow1.json') + await comfyPage.menu.topbar.saveWorkflow('workflow1') await comfyPage.command.executeCommand('Workspace.CloseWorkflow') - expect(await tab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' - ]) + expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow']) }) test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => { @@ -284,7 +280,7 @@ test.describe('Workflows sidebar', () => { const { topbar, workflowsTab } = comfyPage.menu - const filename = 'workflow18.json' + const filename = 'workflow18' await topbar.saveWorkflow(filename) expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) @@ -295,14 +291,14 @@ test.describe('Workflows sidebar', () => { await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' + '*Unsaved Workflow' ]) }) test('Can delete workflows', async ({ comfyPage }) => { const { topbar, workflowsTab } = comfyPage.menu - const filename = 'workflow18.json' + const filename = 'workflow18' await topbar.saveWorkflow(filename) expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) @@ -314,7 +310,7 @@ test.describe('Workflows sidebar', () => { await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ - '*Unsaved Workflow.json' + '*Unsaved Workflow' ]) }) @@ -326,13 +322,11 @@ test.describe('Workflows sidebar', () => { const { workflowsTab } = comfyPage.menu await workflowsTab.open() - await workflowsTab - .getPersistedItem('workflow1.json') - .click({ button: 'right' }) + await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' }) await comfyPage.contextMenu.clickMenuItem('Duplicate') await expect .poll(() => workflowsTab.getOpenedWorkflowNames()) - .toEqual(['*Unsaved Workflow.json', '*workflow1 (Copy).json']) + .toEqual(['*Unsaved Workflow', '*workflow1 (Copy)']) }) test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => { @@ -344,7 +338,7 @@ test.describe('Workflows sidebar', () => { // Wait for workflow to appear in Browse section after sync const workflowItem = - comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json') + comfyPage.menu.workflowsTab.getPersistedItem('workflow1') await expect(workflowItem).toBeVisible({ timeout: 3000 }) const nodeCount = await comfyPage.nodeOps.getGraphNodesCount() @@ -361,7 +355,7 @@ test.describe('Workflows sidebar', () => { } await comfyPage.page.dragAndDrop( - '.comfyui-workflows-browse .node-label:has-text("workflow1.json")', + '.comfyui-workflows-browse .node-label:has-text("workflow1")', '#graph-canvas', { targetPosition } ) diff --git a/src/components/builder/AppBuilder.vue b/src/components/builder/AppBuilder.vue index eec084b203..68d0f512cb 100644 --- a/src/components/builder/AppBuilder.vue +++ b/src/components/builder/AppBuilder.vue @@ -204,131 +204,145 @@ const renderedInputs = computed<[string, MaybeRef | undefined][]>( )