feat: dropdown widgets vue node ui (#5624)

- Load media dropdown widgets
- Load models dropdown widgets

I added a lot of feedback effects during interactions.
I tried my best to break the Dropdown into small components.
To make it more flexible, I provided many configurable props and
v-model.

<img width="1000" alt="CleanShot 2025-09-18 at 01 54 38"
src="https://github.com/user-attachments/assets/1a413078-1547-44b8-8b48-1ce8f8e764b5"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5624-feat-dropdown-widgets-vue-node-ui-2716d73d36508115a52bc1fb6d6376d0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
Rizumu Ayaka
2025-09-27 03:04:39 +08:00
committed by GitHub
parent 9f19d8fb4b
commit c96f719f91
19 changed files with 1270 additions and 51 deletions

View File

@@ -4,9 +4,12 @@ import Select from 'primevue/select'
import type { SelectProps } from 'primevue/select'
import { describe, expect, it } from 'vitest'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetSelect from './WidgetSelect.vue'
import WidgetSelectDefault from './WidgetSelectDefault.vue'
import WidgetSelectDropdown from './WidgetSelectDropdown.vue'
describe('WidgetSelect Value Binding', () => {
const createMockWidget = (
@@ -14,7 +17,8 @@ describe('WidgetSelect Value Binding', () => {
options: Partial<
SelectProps & { values?: string[]; return_index?: boolean }
> = {},
callback?: (value: string | number | undefined) => void
callback?: (value: string | number | undefined) => void,
spec?: ComboInputSpec
): SimplifiedWidget<string | number | undefined> => ({
name: 'test_select',
type: 'combo',
@@ -23,7 +27,8 @@ describe('WidgetSelect Value Binding', () => {
values: ['option1', 'option2', 'option3'],
...options
},
callback
callback,
spec
})
const mountComponent = (
@@ -184,4 +189,44 @@ describe('WidgetSelect Value Binding', () => {
expect(emitted![0]).toContain('100')
})
})
describe('Spec-aware rendering', () => {
it('uses dropdown variant when combo spec enables image uploads', () => {
const spec: ComboInputSpec = {
type: 'COMBO',
name: 'test_select',
image_upload: true
}
const widget = createMockWidget('option1', {}, undefined, spec)
const wrapper = mountComponent(widget, 'option1')
expect(wrapper.findComponent(WidgetSelectDropdown).exists()).toBe(true)
expect(wrapper.findComponent(WidgetSelectDefault).exists()).toBe(false)
})
it('uses dropdown variant for audio uploads', () => {
const spec: ComboInputSpec = {
type: 'COMBO',
name: 'test_select',
audio_upload: true
}
const widget = createMockWidget('clip.wav', {}, undefined, spec)
const wrapper = mountComponent(widget, 'clip.wav')
const dropdown = wrapper.findComponent(WidgetSelectDropdown)
expect(dropdown.exists()).toBe(true)
expect(dropdown.props('assetKind')).toBe('audio')
expect(dropdown.props('allowUpload')).toBe(false)
})
it('keeps default select when no spec or media hints are present', () => {
const widget = createMockWidget('plain', {
values: ['plain', 'text']
})
const wrapper = mountComponent(widget, 'plain')
expect(wrapper.findComponent(WidgetSelectDefault).exists()).toBe(true)
expect(wrapper.findComponent(WidgetSelectDropdown).exists()).toBe(false)
})
})
})