mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
refactor: replace AssetDropdownItem with FormDropdownItem
- Create minimal FormDropdownItem interface in dropdown/types.ts - Remove dropdownItems computed from useAssetWidgetData - Transform assets in WidgetSelectDropdown using getAssetFilename() - Delete assetDropdownTypes.ts and assetDropdownUtils.ts - Update all FormDropdown components to use FormDropdownItem Amp-Thread-ID: https://ampcode.com/threads/T-019c10c6-5e4c-774a-90ce-cb00178ad6e3 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
import type { OptionId } from './filterTypes'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight display projection of AssetItem for dropdown/selection UIs.
|
|
||||||
* Used by FormDropdown and WidgetSelectDropdown.
|
|
||||||
*/
|
|
||||||
export interface AssetDropdownItem {
|
|
||||||
id: OptionId
|
|
||||||
/** Display name (user-defined filename or asset name) */
|
|
||||||
name: string
|
|
||||||
/** Original filename from asset */
|
|
||||||
label?: string
|
|
||||||
/** Preview image/video URL */
|
|
||||||
previewUrl: string
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { AssetItem } from '../schemas/assetSchema'
|
|
||||||
import type { AssetDropdownItem } from '../types/assetDropdownTypes'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms an AssetItem to AssetDropdownItem for dropdown display.
|
|
||||||
*/
|
|
||||||
export function toAssetDropdownItem(asset: AssetItem): AssetDropdownItem {
|
|
||||||
return {
|
|
||||||
id: asset.id,
|
|
||||||
name: (asset.user_metadata?.filename as string | undefined) ?? asset.name,
|
|
||||||
label: asset.name,
|
|
||||||
previewUrl: asset.preview_url ?? ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import type { AssetSortOption } from '../types/filterTypes'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal interface for sortable items
|
* Minimal interface for sortable items
|
||||||
* Works with both AssetItem and AssetDropdownItem
|
* Works with both AssetItem and FormDropdownItem
|
||||||
*/
|
*/
|
||||||
export interface SortableItem {
|
export interface SortableItem {
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import PrimeVue from 'primevue/config'
|
|||||||
import type { ComponentPublicInstance } from 'vue'
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
import type { FormDropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||||
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||||
|
|
||||||
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
|
import WidgetSelectDropdown from '@/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue'
|
||||||
|
|
||||||
interface WidgetSelectDropdownInstance extends ComponentPublicInstance {
|
interface WidgetSelectDropdownInstance extends ComponentPublicInstance {
|
||||||
inputItems: AssetDropdownItem[]
|
inputItems: FormDropdownItem[]
|
||||||
outputItems: AssetDropdownItem[]
|
outputItems: FormDropdownItem[]
|
||||||
updateSelectedItems: (selectedSet: Set<string>) => void
|
updateSelectedItems: (selectedSet: Set<string>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
|||||||
|
|
||||||
// The missing value should be accessible via dropdownItems when filter is 'all' (default)
|
// The missing value should be accessible via dropdownItems when filter is 'all' (default)
|
||||||
const dropdownItems = (
|
const dropdownItems = (
|
||||||
wrapper.vm as unknown as { dropdownItems: AssetDropdownItem[] }
|
wrapper.vm as unknown as { dropdownItems: FormDropdownItem[] }
|
||||||
).dropdownItems
|
).dropdownItems
|
||||||
expect(
|
expect(
|
||||||
dropdownItems.some((item) => item.name === 'template_image.png')
|
dropdownItems.some((item) => item.name === 'template_image.png')
|
||||||
@@ -248,7 +248,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
|||||||
|
|
||||||
const vmWithFilter = wrapper.vm as unknown as {
|
const vmWithFilter = wrapper.vm as unknown as {
|
||||||
filterSelected: string
|
filterSelected: string
|
||||||
dropdownItems: AssetDropdownItem[]
|
dropdownItems: FormDropdownItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
vmWithFilter.filterSelected = 'inputs'
|
vmWithFilter.filterSelected = 'inputs'
|
||||||
@@ -269,8 +269,8 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
|||||||
|
|
||||||
const vmWithFilter = wrapper.vm as unknown as {
|
const vmWithFilter = wrapper.vm as unknown as {
|
||||||
filterSelected: string
|
filterSelected: string
|
||||||
dropdownItems: AssetDropdownItem[]
|
dropdownItems: FormDropdownItem[]
|
||||||
outputItems: AssetDropdownItem[]
|
outputItems: FormDropdownItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
vmWithFilter.filterSelected = 'outputs'
|
vmWithFilter.filterSelected = 'outputs'
|
||||||
@@ -290,7 +290,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
|||||||
const wrapper = mountComponent(widget, 'img_001.png')
|
const wrapper = mountComponent(widget, 'img_001.png')
|
||||||
|
|
||||||
const dropdownItems = (
|
const dropdownItems = (
|
||||||
wrapper.vm as unknown as { dropdownItems: AssetDropdownItem[] }
|
wrapper.vm as unknown as { dropdownItems: FormDropdownItem[] }
|
||||||
).dropdownItems
|
).dropdownItems
|
||||||
expect(dropdownItems).toHaveLength(2)
|
expect(dropdownItems).toHaveLength(2)
|
||||||
expect(
|
expect(
|
||||||
@@ -305,7 +305,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
|||||||
const wrapper = mountComponent(widget, undefined)
|
const wrapper = mountComponent(widget, undefined)
|
||||||
|
|
||||||
const dropdownItems = (
|
const dropdownItems = (
|
||||||
wrapper.vm as unknown as { dropdownItems: AssetDropdownItem[] }
|
wrapper.vm as unknown as { dropdownItems: FormDropdownItem[] }
|
||||||
).dropdownItems
|
).dropdownItems
|
||||||
expect(dropdownItems).toHaveLength(2)
|
expect(dropdownItems).toHaveLength(2)
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import { computed, provide, ref, toRef, watch } from 'vue'
|
|||||||
|
|
||||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import {
|
||||||
|
getAssetDisplayName,
|
||||||
|
getAssetFilename
|
||||||
|
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue'
|
import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue'
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import type {
|
import type {
|
||||||
FilterOption,
|
FilterOption,
|
||||||
OptionId
|
OptionId
|
||||||
} from '@/platform/assets/types/filterTypes'
|
} from '@/platform/assets/types/filterTypes'
|
||||||
import { AssetKindKey } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types';
|
import { AssetKindKey } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||||
import type { LayoutMode } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types';
|
import type {
|
||||||
|
FormDropdownItem,
|
||||||
|
LayoutMode
|
||||||
|
} from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||||
import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
|
import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
|
||||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||||
@@ -100,7 +106,7 @@ function getDisplayLabel(value: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputItems = computed<AssetDropdownItem[]>(() => {
|
const inputItems = computed<FormDropdownItem[]>(() => {
|
||||||
const values = props.widget.options?.values || []
|
const values = props.widget.options?.values || []
|
||||||
|
|
||||||
if (!Array.isArray(values)) {
|
if (!Array.isArray(values)) {
|
||||||
@@ -109,12 +115,12 @@ const inputItems = computed<AssetDropdownItem[]>(() => {
|
|||||||
|
|
||||||
return values.map((value: string, index: number) => ({
|
return values.map((value: string, index: number) => ({
|
||||||
id: `input-${index}`,
|
id: `input-${index}`,
|
||||||
previewUrl: getMediaUrl(value, 'input'),
|
preview_url: getMediaUrl(value, 'input'),
|
||||||
name: value,
|
name: value,
|
||||||
label: getDisplayLabel(value)
|
label: getDisplayLabel(value)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
const outputItems = computed<AssetDropdownItem[]>(() => {
|
const outputItems = computed<FormDropdownItem[]>(() => {
|
||||||
if (!['image', 'video'].includes(props.assetKind ?? '')) return []
|
if (!['image', 'video'].includes(props.assetKind ?? '')) return []
|
||||||
|
|
||||||
const outputs = new Set<string>()
|
const outputs = new Set<string>()
|
||||||
@@ -139,7 +145,7 @@ const outputItems = computed<AssetDropdownItem[]>(() => {
|
|||||||
|
|
||||||
return Array.from(outputs).map((output) => ({
|
return Array.from(outputs).map((output) => ({
|
||||||
id: `output-${output}`,
|
id: `output-${output}`,
|
||||||
previewUrl: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
preview_url: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||||
name: output,
|
name: output,
|
||||||
label: getDisplayLabel(output)
|
label: getDisplayLabel(output)
|
||||||
}))
|
}))
|
||||||
@@ -151,20 +157,20 @@ const outputItems = computed<AssetDropdownItem[]>(() => {
|
|||||||
* where the saved value may not exist in the current server environment.
|
* where the saved value may not exist in the current server environment.
|
||||||
* Works for both local mode (inputItems/outputItems) and cloud mode (assetData).
|
* Works for both local mode (inputItems/outputItems) and cloud mode (assetData).
|
||||||
*/
|
*/
|
||||||
const missingValueItem = computed<AssetDropdownItem | undefined>(() => {
|
const missingValueItem = computed<FormDropdownItem | undefined>(() => {
|
||||||
const currentValue = modelValue.value
|
const currentValue = modelValue.value
|
||||||
if (!currentValue) return undefined
|
if (!currentValue) return undefined
|
||||||
|
|
||||||
// Check in cloud mode assets
|
// Check in cloud mode assets
|
||||||
if (props.isAssetMode && assetData) {
|
if (props.isAssetMode && assetData) {
|
||||||
const existsInAssets = assetData.dropdownItems.value.some(
|
const existsInAssets = assetData.assets.value.some(
|
||||||
(item) => item.name === currentValue
|
(asset) => asset.name === currentValue
|
||||||
)
|
)
|
||||||
if (existsInAssets) return undefined
|
if (existsInAssets) return undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `missing-${currentValue}`,
|
id: `missing-${currentValue}`,
|
||||||
previewUrl: '',
|
preview_url: '',
|
||||||
name: currentValue,
|
name: currentValue,
|
||||||
label: getDisplayLabel(currentValue)
|
label: getDisplayLabel(currentValue)
|
||||||
}
|
}
|
||||||
@@ -187,19 +193,32 @@ const missingValueItem = computed<AssetDropdownItem | undefined>(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: `missing-${currentValue}`,
|
id: `missing-${currentValue}`,
|
||||||
previewUrl: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
preview_url: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
||||||
name: currentValue,
|
name: currentValue,
|
||||||
label: getDisplayLabel(currentValue)
|
label: getDisplayLabel(currentValue)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const allItems = computed<AssetDropdownItem[]>(() => {
|
/**
|
||||||
|
* Transforms AssetItem[] to FormDropdownItem[] for cloud mode.
|
||||||
|
* Uses getAssetFilename for display name, asset.name for label.
|
||||||
|
*/
|
||||||
|
const assetItems = computed<FormDropdownItem[]>(() => {
|
||||||
|
if (!props.isAssetMode || !assetData) return []
|
||||||
|
return assetData.assets.value.map((asset) => ({
|
||||||
|
id: asset.id,
|
||||||
|
name: getAssetFilename(asset),
|
||||||
|
label: getAssetDisplayName(asset),
|
||||||
|
preview_url: asset.preview_url
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const allItems = computed<FormDropdownItem[]>(() => {
|
||||||
if (props.isAssetMode && assetData) {
|
if (props.isAssetMode && assetData) {
|
||||||
const items = assetData.dropdownItems.value
|
|
||||||
if (missingValueItem.value) {
|
if (missingValueItem.value) {
|
||||||
return [missingValueItem.value, ...items]
|
return [missingValueItem.value, ...assetItems.value]
|
||||||
}
|
}
|
||||||
return items
|
return assetItems.value
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
...(missingValueItem.value ? [missingValueItem.value] : []),
|
...(missingValueItem.value ? [missingValueItem.value] : []),
|
||||||
@@ -208,7 +227,7 @@ const allItems = computed<AssetDropdownItem[]>(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const dropdownItems = computed<AssetDropdownItem[]>(() => {
|
const dropdownItems = computed<FormDropdownItem[]>(() => {
|
||||||
if (props.isAssetMode) {
|
if (props.isAssetMode) {
|
||||||
return allItems.value
|
return allItems.value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { computed, ref, useTemplateRef } from 'vue'
|
|||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
|
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import type {
|
import type {
|
||||||
FilterOption,
|
FilterOption,
|
||||||
OptionId
|
OptionId
|
||||||
@@ -14,10 +13,10 @@ import type {
|
|||||||
import FormDropdownInput from './FormDropdownInput.vue'
|
import FormDropdownInput from './FormDropdownInput.vue'
|
||||||
import FormDropdownMenu from './FormDropdownMenu.vue'
|
import FormDropdownMenu from './FormDropdownMenu.vue'
|
||||||
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
||||||
import type { LayoutMode, SortOption } from './types'
|
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: AssetDropdownItem[]
|
items: FormDropdownItem[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
/**
|
/**
|
||||||
* If true, allows multiple selections. If a number is provided,
|
* If true, allows multiple selections. If a number is provided,
|
||||||
@@ -32,14 +31,14 @@ interface Props {
|
|||||||
sortOptions?: SortOption[]
|
sortOptions?: SortOption[]
|
||||||
isSelected?: (
|
isSelected?: (
|
||||||
selected: Set<OptionId>,
|
selected: Set<OptionId>,
|
||||||
item: AssetDropdownItem,
|
item: FormDropdownItem,
|
||||||
index: number
|
index: number
|
||||||
) => boolean
|
) => boolean
|
||||||
searcher?: (
|
searcher?: (
|
||||||
query: string,
|
query: string,
|
||||||
items: AssetDropdownItem[],
|
items: FormDropdownItem[],
|
||||||
onCleanup: (cleanupFn: () => void) => void
|
onCleanup: (cleanupFn: () => void) => void
|
||||||
) => Promise<AssetDropdownItem[]>
|
) => Promise<FormDropdownItem[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -79,7 +78,7 @@ const maxSelectable = computed(() => {
|
|||||||
|
|
||||||
const itemsKey = computed(() => props.items.map((item) => item.id).join('|'))
|
const itemsKey = computed(() => props.items.map((item) => item.id).join('|'))
|
||||||
|
|
||||||
const filteredItems = ref<AssetDropdownItem[]>([])
|
const filteredItems = ref<FormDropdownItem[]>([])
|
||||||
|
|
||||||
const defaultSorter = computed<SortOption['sorter']>(() => {
|
const defaultSorter = computed<SortOption['sorter']>(() => {
|
||||||
const sorter = props.sortOptions.find(
|
const sorter = props.sortOptions.find(
|
||||||
@@ -98,7 +97,7 @@ const sortedItems = computed(() => {
|
|||||||
return selectedSorter.value({ items: filteredItems.value }) || []
|
return selectedSorter.value({ items: filteredItems.value }) || []
|
||||||
})
|
})
|
||||||
|
|
||||||
function internalIsSelected(item: AssetDropdownItem, index: number): boolean {
|
function internalIsSelected(item: FormDropdownItem, index: number): boolean {
|
||||||
return props.isSelected?.(selected.value, item, index) ?? false
|
return props.isSelected?.(selected.value, item, index) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +126,7 @@ function handleFileChange(event: Event) {
|
|||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelection(item: AssetDropdownItem, index: number) {
|
function handleSelection(item: FormDropdownItem, index: number) {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
const sel = selected.value
|
const sel = selected.value
|
||||||
if (internalIsSelected(item, index)) {
|
if (internalIsSelected(item, index)) {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import type { OptionId } from '@/platform/assets/types/filterTypes'
|
import type { OptionId } from '@/platform/assets/types/filterTypes'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import { WidgetInputBaseClass } from '../../layout'
|
import { WidgetInputBaseClass } from '../../layout'
|
||||||
|
import type { FormDropdownItem } from './types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen?: boolean
|
isOpen?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
items: AssetDropdownItem[]
|
items: FormDropdownItem[]
|
||||||
selected: Set<OptionId>
|
selected: Set<OptionId>
|
||||||
maxSelectable: number
|
maxSelectable: number
|
||||||
uploadable: boolean
|
uploadable: boolean
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { MaybeRefOrGetter } from 'vue'
|
|||||||
|
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import type {
|
import type {
|
||||||
FilterOption,
|
FilterOption,
|
||||||
OptionId
|
OptionId
|
||||||
@@ -12,11 +11,11 @@ import type {
|
|||||||
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
|
import FormDropdownMenuActions from './FormDropdownMenuActions.vue'
|
||||||
import FormDropdownMenuFilter from './FormDropdownMenuFilter.vue'
|
import FormDropdownMenuFilter from './FormDropdownMenuFilter.vue'
|
||||||
import FormDropdownMenuItem from './FormDropdownMenuItem.vue'
|
import FormDropdownMenuItem from './FormDropdownMenuItem.vue'
|
||||||
import type { LayoutMode, SortOption } from './types'
|
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: AssetDropdownItem[]
|
items: FormDropdownItem[]
|
||||||
isSelected: (item: AssetDropdownItem, index: number) => boolean
|
isSelected: (item: FormDropdownItem, index: number) => boolean
|
||||||
filterOptions: FilterOption[]
|
filterOptions: FilterOption[]
|
||||||
sortOptions: SortOption[]
|
sortOptions: SortOption[]
|
||||||
searcher?: (
|
searcher?: (
|
||||||
@@ -28,7 +27,7 @@ interface Props {
|
|||||||
|
|
||||||
defineProps<Props>()
|
defineProps<Props>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'item-click', item: AssetDropdownItem, index: number): void
|
(e: 'item-click', item: FormDropdownItem, index: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Define models for two-way binding
|
// Define models for two-way binding
|
||||||
@@ -90,7 +89,7 @@ const searchQuery = defineModel<string>('searchQuery')
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:index="index"
|
:index="index"
|
||||||
:selected="isSelected(item, index)"
|
:selected="isSelected(item, index)"
|
||||||
:preview-url="item.previewUrl"
|
:preview-url="item.preview_url ?? ''"
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:layout="layoutMode"
|
:layout="layoutMode"
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
|
|
||||||
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
||||||
|
import type { FormDropdownItem } from './types'
|
||||||
|
|
||||||
function createItem(name: string, label?: string): AssetDropdownItem {
|
function createItem(name: string, label?: string): FormDropdownItem {
|
||||||
return {
|
return {
|
||||||
id: name,
|
id: name,
|
||||||
previewUrl: '',
|
preview_url: '',
|
||||||
name,
|
name,
|
||||||
label
|
label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('defaultSearcher', () => {
|
describe('defaultSearcher', () => {
|
||||||
const items: AssetDropdownItem[] = [
|
const items: FormDropdownItem[] = [
|
||||||
createItem('apple.png'),
|
createItem('apple.png'),
|
||||||
createItem('banana.jpg'),
|
createItem('banana.jpg'),
|
||||||
createItem('cherry.gif')
|
createItem('cherry.gif')
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import type { AssetSortOption } from '@/platform/assets/types/filterTypes'
|
import type { AssetSortOption } from '@/platform/assets/types/filterTypes'
|
||||||
import { sortAssets } from '@/platform/assets/utils/assetSortUtils'
|
import { sortAssets } from '@/platform/assets/utils/assetSortUtils'
|
||||||
|
|
||||||
import type { SortOption } from './types'
|
import type { FormDropdownItem, SortOption } from './types'
|
||||||
|
|
||||||
export async function defaultSearcher(
|
export async function defaultSearcher(
|
||||||
query: string,
|
query: string,
|
||||||
items: AssetDropdownItem[]
|
items: FormDropdownItem[]
|
||||||
) {
|
) {
|
||||||
if (query.trim() === '') return items
|
if (query.trim() === '') return items
|
||||||
const words = query.trim().toLowerCase().split(' ')
|
const words = query.trim().toLowerCase().split(' ')
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import type { ComputedRef, InjectionKey } from 'vue'
|
import type { ComputedRef, InjectionKey } from 'vue'
|
||||||
|
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import type { OptionId } from '@/platform/assets/types/filterTypes'
|
import type { OptionId } from '@/platform/assets/types/filterTypes'
|
||||||
import type { AssetKind } from '@/types/widgetTypes'
|
import type { AssetKind } from '@/types/widgetTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal interface for items in FormDropdown.
|
||||||
|
* Both AssetItem (from cloud API) and local file items satisfy this contract.
|
||||||
|
*/
|
||||||
|
export interface FormDropdownItem {
|
||||||
|
id: OptionId
|
||||||
|
/** Display name shown in the dropdown */
|
||||||
|
name: string
|
||||||
|
/** Original/alternate label (e.g., original filename) */
|
||||||
|
label?: string
|
||||||
|
/** Preview image/video URL */
|
||||||
|
preview_url?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SortOption<TId extends OptionId = OptionId> {
|
export interface SortOption<TId extends OptionId = OptionId> {
|
||||||
id: TId
|
id: TId
|
||||||
name: string
|
name: string
|
||||||
sorter: (ctx: { items: readonly AssetDropdownItem[] }) => AssetDropdownItem[]
|
sorter: (ctx: { items: readonly FormDropdownItem[] }) => FormDropdownItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
||||||
|
|||||||
@@ -29,12 +29,10 @@ vi.mock('@/stores/modelToNodeStore', () => ({
|
|||||||
describe('useAssetWidgetData (desktop/isCloud=false)', () => {
|
describe('useAssetWidgetData (desktop/isCloud=false)', () => {
|
||||||
it('returns empty/default values without calling stores', () => {
|
it('returns empty/default values without calling stores', () => {
|
||||||
const nodeType = ref('CheckpointLoaderSimple')
|
const nodeType = ref('CheckpointLoaderSimple')
|
||||||
const { category, assets, dropdownItems, isLoading, error } =
|
const { category, assets, isLoading, error } = useAssetWidgetData(nodeType)
|
||||||
useAssetWidgetData(nodeType)
|
|
||||||
|
|
||||||
expect(category.value).toBeUndefined()
|
expect(category.value).toBeUndefined()
|
||||||
expect(assets.value).toEqual([])
|
expect(assets.value).toEqual([])
|
||||||
expect(dropdownItems.value).toEqual([])
|
|
||||||
expect(isLoading.value).toBe(false)
|
expect(isLoading.value).toBe(false)
|
||||||
expect(error.value).toBeNull()
|
expect(error.value).toBeNull()
|
||||||
expect(mockUpdateModelsForNodeType).not.toHaveBeenCalled()
|
expect(mockUpdateModelsForNodeType).not.toHaveBeenCalled()
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fetches assets and transforms to dropdown items', async () => {
|
it('fetches assets for a given node type', async () => {
|
||||||
const mockAssets: AssetItem[] = [
|
const mockAssets: AssetItem[] = [
|
||||||
createMockAsset(
|
createMockAsset(
|
||||||
'asset-1',
|
'asset-1',
|
||||||
@@ -87,8 +87,7 @@ describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const nodeType = ref('CheckpointLoaderSimple')
|
const nodeType = ref('CheckpointLoaderSimple')
|
||||||
const { category, assets, dropdownItems, isLoading } =
|
const { category, assets, isLoading } = useAssetWidgetData(nodeType)
|
||||||
useAssetWidgetData(nodeType)
|
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await vi.waitFor(() => !isLoading.value)
|
await vi.waitFor(() => !isLoading.value)
|
||||||
@@ -98,13 +97,10 @@ describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
|||||||
)
|
)
|
||||||
expect(category.value).toBe('checkpoints')
|
expect(category.value).toBe('checkpoints')
|
||||||
expect(assets.value).toEqual(mockAssets)
|
expect(assets.value).toEqual(mockAssets)
|
||||||
|
expect(assets.value).toHaveLength(2)
|
||||||
expect(dropdownItems.value).toHaveLength(2)
|
expect(assets.value[0].id).toBe('asset-1')
|
||||||
const item = dropdownItems.value[0]
|
expect(assets.value[0].name).toBe('Beautiful Model')
|
||||||
expect(item.id).toBe('asset-1')
|
expect(assets.value[0].preview_url).toBe('/api/preview/asset-1')
|
||||||
expect(item.name).toBe('models/beautiful_model.safetensors')
|
|
||||||
expect(item.label).toBe('Beautiful Model')
|
|
||||||
expect(item.previewUrl).toBe('/api/preview/asset-1')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles API errors gracefully', async () => {
|
it('handles API errors gracefully', async () => {
|
||||||
@@ -238,7 +234,7 @@ describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('handles undefined node type gracefully', async () => {
|
it('handles undefined node type gracefully', async () => {
|
||||||
const { category, assets, dropdownItems, isLoading, error } =
|
const { category, assets, isLoading, error } =
|
||||||
useAssetWidgetData(undefined)
|
useAssetWidgetData(undefined)
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -246,7 +242,6 @@ describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
|||||||
expect(mockUpdateModelsForNodeType).not.toHaveBeenCalled()
|
expect(mockUpdateModelsForNodeType).not.toHaveBeenCalled()
|
||||||
expect(category.value).toBeUndefined()
|
expect(category.value).toBeUndefined()
|
||||||
expect(assets.value).toEqual([])
|
expect(assets.value).toEqual([])
|
||||||
expect(dropdownItems.value).toEqual([])
|
|
||||||
expect(isLoading.value).toBe(false)
|
expect(isLoading.value).toBe(false)
|
||||||
expect(error.value).toBeNull()
|
expect(error.value).toBeNull()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { computed, toValue, watch } from 'vue'
|
|||||||
import type { MaybeRefOrGetter } from 'vue'
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||||
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
|
||||||
import { toAssetDropdownItem } from '@/platform/assets/utils/assetDropdownUtils'
|
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import { useAssetsStore } from '@/stores/assetsStore'
|
import { useAssetsStore } from '@/stores/assetsStore'
|
||||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||||
@@ -48,10 +46,6 @@ export function useAssetWidgetData(
|
|||||||
return resolvedType ? (assetsStore.getError(resolvedType) ?? null) : null
|
return resolvedType ? (assetsStore.getError(resolvedType) ?? null) : null
|
||||||
})
|
})
|
||||||
|
|
||||||
const dropdownItems = computed<AssetDropdownItem[]>(() => {
|
|
||||||
return (assets.value ?? []).map(toAssetDropdownItem)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => toValue(nodeType),
|
() => toValue(nodeType),
|
||||||
async (currentNodeType) => {
|
async (currentNodeType) => {
|
||||||
@@ -72,7 +66,6 @@ export function useAssetWidgetData(
|
|||||||
return {
|
return {
|
||||||
category,
|
category,
|
||||||
assets,
|
assets,
|
||||||
dropdownItems,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
@@ -80,8 +73,7 @@ export function useAssetWidgetData(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
category: computed(() => undefined),
|
category: computed(() => undefined),
|
||||||
assets: computed(() => []),
|
assets: computed<AssetItem[]>(() => []),
|
||||||
dropdownItems: computed(() => []),
|
|
||||||
isLoading: computed(() => false),
|
isLoading: computed(() => false),
|
||||||
error: computed(() => null)
|
error: computed(() => null)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user