mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 04:31:58 +00:00
## Summary Unify the search bar + action buttons layout across all left sidebar panels (Node Library, Workflows, Model Library, Media Assets) using a shared `SidebarTopArea` presentation component. ## Changes - **What**: - Add `SidebarTopArea.vue` — layout component with `flex-1` default slot (search) and `#actions` slot (buttons), plus optional `bottomDivider` prop - Replace raw `<button>` elements in Node Library with `<Button variant="secondary" size="icon">` - Replace reka-ui `TabsTrigger` with shared `Tab/TabList` component in Node Library - Move Media Assets tab list from hover-only `#tool-buttons` to always-visible header below search area - Unify spacing (`gap-2`, `p-2 2xl:px-4`) and divider styles across all sidebar panels - Remove unused `assetType` prop and header from `AssetsSidebarGridView`/`AssetsSidebarListView` ## Review Focus - `SidebarTopArea` API simplicity — just slots + one optional prop - Node Library still requires `TabsRoot` in the body for reka-ui `TabsContent` in child panels - Media Assets tabs are now always visible instead of hover-only [screen-capture (1).webm](https://github.com/user-attachments/assets/fe1d8f7b-5674-4bb3-9842-569e4c3af6c9) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9740-feat-unify-sidebar-panel-header-layout-with-SidebarTopArea-component-3206d73d365081ea8ba7fd6ac54e0169) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
135 lines
3.8 KiB
TypeScript
135 lines
3.8 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { defineComponent } from 'vue'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
|
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
|
|
|
import AssetsSidebarListView from './AssetsSidebarListView.vue'
|
|
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: () => ({
|
|
t: (key: string) => key
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/stores/assetsStore', () => ({
|
|
useAssetsStore: () => ({
|
|
isAssetDeleting: () => false
|
|
})
|
|
}))
|
|
|
|
const VirtualGridStub = defineComponent({
|
|
name: 'VirtualGrid',
|
|
props: {
|
|
items: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
},
|
|
template:
|
|
'<div><slot v-for="item in items" :key="item.key" name="item" :item="item" /></div>'
|
|
})
|
|
|
|
const buildAsset = (id: string, name: string): AssetItem =>
|
|
({
|
|
id,
|
|
name,
|
|
tags: []
|
|
}) satisfies AssetItem
|
|
|
|
const buildOutputItem = (asset: AssetItem): OutputStackListItem => ({
|
|
key: `asset-${asset.id}`,
|
|
asset
|
|
})
|
|
|
|
const mountListView = (assetItems: OutputStackListItem[] = []) =>
|
|
mount(AssetsSidebarListView, {
|
|
props: {
|
|
assetItems,
|
|
selectableAssets: [],
|
|
isSelected: () => false,
|
|
isStackExpanded: () => false,
|
|
toggleStack: async () => {}
|
|
},
|
|
global: {
|
|
stubs: {
|
|
VirtualGrid: VirtualGridStub
|
|
}
|
|
}
|
|
})
|
|
|
|
describe('AssetsSidebarListView', () => {
|
|
it('marks mp4 assets as video previews', () => {
|
|
const videoAsset = {
|
|
...buildAsset('video-asset', 'clip.mp4'),
|
|
preview_url: '/api/view/clip.mp4',
|
|
user_metadata: {}
|
|
} satisfies AssetItem
|
|
|
|
const wrapper = mountListView([buildOutputItem(videoAsset)])
|
|
|
|
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
|
const assetListItem = listItems.at(-1)
|
|
|
|
expect(assetListItem).toBeDefined()
|
|
expect(assetListItem?.props('previewUrl')).toBe('/api/view/clip.mp4')
|
|
expect(assetListItem?.props('isVideoPreview')).toBe(true)
|
|
})
|
|
|
|
it('uses icon fallback for text assets even when preview_url exists', () => {
|
|
const textAsset = {
|
|
...buildAsset('text-asset', 'notes.txt'),
|
|
preview_url: '/api/view/notes.txt',
|
|
user_metadata: {}
|
|
} satisfies AssetItem
|
|
|
|
const wrapper = mountListView([buildOutputItem(textAsset)])
|
|
|
|
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
|
const assetListItem = listItems.at(-1)
|
|
|
|
expect(assetListItem).toBeDefined()
|
|
expect(assetListItem?.props('previewUrl')).toBe('')
|
|
expect(assetListItem?.props('isVideoPreview')).toBe(false)
|
|
})
|
|
|
|
it('emits preview-asset when item preview is clicked', async () => {
|
|
const imageAsset = {
|
|
...buildAsset('image-asset', 'image.png'),
|
|
preview_url: '/api/view/image.png',
|
|
user_metadata: {}
|
|
} satisfies AssetItem
|
|
|
|
const wrapper = mountListView([buildOutputItem(imageAsset)])
|
|
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
|
const assetListItem = listItems.at(-1)
|
|
|
|
expect(assetListItem).toBeDefined()
|
|
|
|
assetListItem!.vm.$emit('preview-click')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.emitted('preview-asset')).toEqual([[imageAsset]])
|
|
})
|
|
|
|
it('emits preview-asset when item is double-clicked', async () => {
|
|
const imageAsset = {
|
|
...buildAsset('image-asset-dbl', 'image.png'),
|
|
preview_url: '/api/view/image.png',
|
|
user_metadata: {}
|
|
} satisfies AssetItem
|
|
|
|
const wrapper = mountListView([buildOutputItem(imageAsset)])
|
|
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
|
const assetListItem = listItems.at(-1)
|
|
|
|
expect(assetListItem).toBeDefined()
|
|
|
|
await assetListItem!.trigger('dblclick')
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.emitted('preview-asset')).toEqual([[imageAsset]])
|
|
})
|
|
})
|