mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
feat/fix: App mode QA updates (#9439)
## 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) <img width="1314" height="1129" alt="image" src="https://github.com/user-attachments/assets/c430f9d6-7c29-4853-803e-5b6fe7086fca" /> <img width="511" height="283" alt="image" src="https://github.com/user-attachments/assets/b7e594d4-70a1-41e3-8ba1-78512f2a5c8b" /> <img width="254" height="232" alt="image" src="https://github.com/user-attachments/assets/1d146399-39ea-4b0e-928c-340b74957535" /> <img width="487" height="198" alt="image" src="https://github.com/user-attachments/assets/e2ba7f5d-8ff5-47f4-9526-61ebb99514b8" /> <img width="378" height="647" alt="image" src="https://github.com/user-attachments/assets/a47a3054-9320-4327-bdc0-b0a16e19f83d" /> <img width="1016" height="476" alt="image" src="https://github.com/user-attachments/assets/479ae50e-d380-4d56-a5c9-5df142b14ed0" /> ┆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)
This commit is contained in:
133
src/composables/useStablePrimeVueSplitterSizer.test.ts
Normal file
133
src/composables/useStablePrimeVueSplitterSizer.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { SplitterResizeEndEvent } from 'primevue/splitter'
|
||||
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useStablePrimeVueSplitterSizer } from './useStablePrimeVueSplitterSizer'
|
||||
|
||||
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as object),
|
||||
useStorage: <T>(_key: string, defaultValue: T) => ref(defaultValue)
|
||||
}
|
||||
})
|
||||
|
||||
function createPanel(width: number) {
|
||||
const el = document.createElement('div')
|
||||
Object.defineProperty(el, 'offsetWidth', { value: width })
|
||||
return ref(el)
|
||||
}
|
||||
|
||||
function resizeEndEvent(): SplitterResizeEndEvent {
|
||||
return { originalEvent: new Event('mouseup'), sizes: [] }
|
||||
}
|
||||
|
||||
async function flushWatcher() {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
describe('useStablePrimeVueSplitterSizer', () => {
|
||||
it('captures pixel widths on resize end and applies on trigger', async () => {
|
||||
const panelRef = createPanel(400)
|
||||
const trigger = ref(0)
|
||||
|
||||
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
[{ ref: panelRef, storageKey: 'test-capture' }],
|
||||
[trigger]
|
||||
)
|
||||
await flushWatcher()
|
||||
|
||||
onResizeEnd(resizeEndEvent())
|
||||
|
||||
trigger.value++
|
||||
await flushWatcher()
|
||||
|
||||
expect(panelRef.value!.style.flexBasis).toBe('400px')
|
||||
expect(panelRef.value!.style.flexGrow).toBe('0')
|
||||
expect(panelRef.value!.style.flexShrink).toBe('0')
|
||||
})
|
||||
|
||||
it('does not apply styles when no stored width exists', async () => {
|
||||
const panelRef = createPanel(300)
|
||||
const trigger = ref(0)
|
||||
|
||||
useStablePrimeVueSplitterSizer(
|
||||
[{ ref: panelRef, storageKey: 'test-no-stored' }],
|
||||
[trigger]
|
||||
)
|
||||
await flushWatcher()
|
||||
|
||||
expect(panelRef.value!.style.flexBasis).toBe('')
|
||||
})
|
||||
|
||||
it('re-applies stored widths when watch sources change', async () => {
|
||||
const panelRef = createPanel(500)
|
||||
const trigger = ref(0)
|
||||
|
||||
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
[{ ref: panelRef, storageKey: 'test-reapply' }],
|
||||
[trigger]
|
||||
)
|
||||
await flushWatcher()
|
||||
|
||||
onResizeEnd(resizeEndEvent())
|
||||
|
||||
panelRef.value!.style.flexBasis = ''
|
||||
panelRef.value!.style.flexGrow = ''
|
||||
panelRef.value!.style.flexShrink = ''
|
||||
|
||||
trigger.value++
|
||||
await flushWatcher()
|
||||
|
||||
expect(panelRef.value!.style.flexBasis).toBe('500px')
|
||||
expect(panelRef.value!.style.flexGrow).toBe('0')
|
||||
expect(panelRef.value!.style.flexShrink).toBe('0')
|
||||
})
|
||||
|
||||
it('handles multiple panels independently', async () => {
|
||||
const leftRef = createPanel(300)
|
||||
const rightRef = createPanel(250)
|
||||
const trigger = ref(0)
|
||||
|
||||
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
[
|
||||
{ ref: leftRef, storageKey: 'test-multi-left' },
|
||||
{ ref: rightRef, storageKey: 'test-multi-right' }
|
||||
],
|
||||
[trigger]
|
||||
)
|
||||
await flushWatcher()
|
||||
|
||||
onResizeEnd(resizeEndEvent())
|
||||
|
||||
trigger.value++
|
||||
await flushWatcher()
|
||||
|
||||
expect(leftRef.value!.style.flexBasis).toBe('300px')
|
||||
expect(rightRef.value!.style.flexBasis).toBe('250px')
|
||||
})
|
||||
|
||||
it('skips panels with null refs', async () => {
|
||||
const nullRef = ref(null)
|
||||
const validRef = createPanel(200)
|
||||
const trigger = ref(0)
|
||||
|
||||
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
[
|
||||
{ ref: nullRef, storageKey: 'test-null' },
|
||||
{ ref: validRef, storageKey: 'test-valid' }
|
||||
],
|
||||
[trigger]
|
||||
)
|
||||
await flushWatcher()
|
||||
|
||||
onResizeEnd(resizeEndEvent())
|
||||
|
||||
trigger.value++
|
||||
await flushWatcher()
|
||||
|
||||
expect(validRef.value!.style.flexBasis).toBe('200px')
|
||||
})
|
||||
})
|
||||
64
src/composables/useStablePrimeVueSplitterSizer.ts
Normal file
64
src/composables/useStablePrimeVueSplitterSizer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { SplitterResizeEndEvent } from 'primevue/splitter'
|
||||
import type { WatchSource } from 'vue'
|
||||
|
||||
import { unrefElement, useStorage } from '@vueuse/core'
|
||||
import type { MaybeComputedElementRef } from '@vueuse/core'
|
||||
import { nextTick, watch } from 'vue'
|
||||
|
||||
interface PanelConfig {
|
||||
ref: MaybeComputedElementRef
|
||||
storageKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Works around PrimeVue Splitter not properly initializing flexBasis
|
||||
* when panels are conditionally rendered. Captures pixel widths on
|
||||
* resize end and re-applies them as rigid flex values (flex: 0 0 Xpx)
|
||||
* when watched sources change (e.g. tab switch, panel toggle).
|
||||
*
|
||||
* @param panels - array of panel configs with template ref and storage key
|
||||
* @param watchSources - reactive sources that trigger re-application
|
||||
*/
|
||||
export function useStablePrimeVueSplitterSizer(
|
||||
panels: PanelConfig[],
|
||||
watchSources: WatchSource[]
|
||||
) {
|
||||
const storedWidths = panels.map((panel) => ({
|
||||
ref: panel.ref,
|
||||
width: useStorage<number | null>(panel.storageKey, null)
|
||||
}))
|
||||
|
||||
function resolveElement(
|
||||
ref: MaybeComputedElementRef
|
||||
): HTMLElement | undefined {
|
||||
return unrefElement(ref) as HTMLElement | undefined
|
||||
}
|
||||
|
||||
function applyStoredWidths() {
|
||||
for (const { ref, width } of storedWidths) {
|
||||
const el = resolveElement(ref)
|
||||
if (!el || width.value === null) continue
|
||||
el.style.flexBasis = `${width.value}px`
|
||||
el.style.flexGrow = '0'
|
||||
el.style.flexShrink = '0'
|
||||
}
|
||||
}
|
||||
|
||||
function onResizeEnd(_event: SplitterResizeEndEvent) {
|
||||
for (const { ref, width } of storedWidths) {
|
||||
const el = resolveElement(ref)
|
||||
if (el) width.value = el.offsetWidth
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
watchSources,
|
||||
async () => {
|
||||
await nextTick()
|
||||
applyStoredWidths()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return { onResizeEnd }
|
||||
}
|
||||
Reference in New Issue
Block a user