mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
refactor: unify DropdownItem with AssetDropdownItem
- Create AssetDropdownItem in platform/assets/types - Create toAssetDropdownItem() transform utility - Remove deprecated DropdownItem, SelectedKey aliases - Rename mediaSrc to previewUrl, remove unused metadata field - Import types directly from source, remove re-exports Amp-Thread-ID: https://ampcode.com/threads/T-019c10b4-cabd-779d-a787-1ebf5dc8a067 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
15
src/platform/assets/types/assetDropdownTypes.ts
Normal file
15
src/platform/assets/types/assetDropdownTypes.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
14
src/platform/assets/utils/assetDropdownUtils.ts
Normal file
14
src/platform/assets/utils/assetDropdownUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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 DropdownItem
|
* Works with both AssetItem and AssetDropdownItem
|
||||||
*/
|
*/
|
||||||
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 { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
import type { DropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
|
||||||
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: DropdownItem[]
|
inputItems: AssetDropdownItem[]
|
||||||
outputItems: DropdownItem[]
|
outputItems: AssetDropdownItem[]
|
||||||
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: DropdownItem[] }
|
wrapper.vm as unknown as { dropdownItems: AssetDropdownItem[] }
|
||||||
).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: DropdownItem[]
|
dropdownItems: AssetDropdownItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
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: DropdownItem[]
|
dropdownItems: AssetDropdownItem[]
|
||||||
outputItems: DropdownItem[]
|
outputItems: AssetDropdownItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
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: DropdownItem[] }
|
wrapper.vm as unknown as { dropdownItems: AssetDropdownItem[] }
|
||||||
).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: DropdownItem[] }
|
wrapper.vm as unknown as { dropdownItems: AssetDropdownItem[] }
|
||||||
).dropdownItems
|
).dropdownItems
|
||||||
expect(dropdownItems).toHaveLength(2)
|
expect(dropdownItems).toHaveLength(2)
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import { useTransformCompatOverlayProps } from '@/composables/useTransformCompat
|
|||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
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 { AssetKindKey } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
||||||
import type {
|
import type {
|
||||||
DropdownItem,
|
|
||||||
FilterOption,
|
FilterOption,
|
||||||
LayoutMode,
|
OptionId
|
||||||
SelectedKey
|
} from '@/platform/assets/types/filterTypes'
|
||||||
} 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 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'
|
||||||
@@ -81,7 +81,7 @@ const filterOptions = computed<FilterOption[]>(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedSet = ref<Set<SelectedKey>>(new Set())
|
const selectedSet = ref<Set<OptionId>>(new Set())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms a value using getOptionLabel if available.
|
* Transforms a value using getOptionLabel if available.
|
||||||
@@ -100,7 +100,7 @@ function getDisplayLabel(value: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputItems = computed<DropdownItem[]>(() => {
|
const inputItems = computed<AssetDropdownItem[]>(() => {
|
||||||
const values = props.widget.options?.values || []
|
const values = props.widget.options?.values || []
|
||||||
|
|
||||||
if (!Array.isArray(values)) {
|
if (!Array.isArray(values)) {
|
||||||
@@ -109,13 +109,12 @@ const inputItems = computed<DropdownItem[]>(() => {
|
|||||||
|
|
||||||
return values.map((value: string, index: number) => ({
|
return values.map((value: string, index: number) => ({
|
||||||
id: `input-${index}`,
|
id: `input-${index}`,
|
||||||
mediaSrc: getMediaUrl(value, 'input'),
|
previewUrl: getMediaUrl(value, 'input'),
|
||||||
name: value,
|
name: value,
|
||||||
label: getDisplayLabel(value),
|
label: getDisplayLabel(value)
|
||||||
metadata: ''
|
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
const outputItems = computed<DropdownItem[]>(() => {
|
const outputItems = computed<AssetDropdownItem[]>(() => {
|
||||||
if (!['image', 'video'].includes(props.assetKind ?? '')) return []
|
if (!['image', 'video'].includes(props.assetKind ?? '')) return []
|
||||||
|
|
||||||
const outputs = new Set<string>()
|
const outputs = new Set<string>()
|
||||||
@@ -140,10 +139,9 @@ const outputItems = computed<DropdownItem[]>(() => {
|
|||||||
|
|
||||||
return Array.from(outputs).map((output) => ({
|
return Array.from(outputs).map((output) => ({
|
||||||
id: `output-${output}`,
|
id: `output-${output}`,
|
||||||
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
previewUrl: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||||
name: output,
|
name: output,
|
||||||
label: getDisplayLabel(output),
|
label: getDisplayLabel(output)
|
||||||
metadata: ''
|
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -153,7 +151,7 @@ const outputItems = computed<DropdownItem[]>(() => {
|
|||||||
* 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<DropdownItem | undefined>(() => {
|
const missingValueItem = computed<AssetDropdownItem | undefined>(() => {
|
||||||
const currentValue = modelValue.value
|
const currentValue = modelValue.value
|
||||||
if (!currentValue) return undefined
|
if (!currentValue) return undefined
|
||||||
|
|
||||||
@@ -166,10 +164,9 @@ const missingValueItem = computed<DropdownItem | undefined>(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: `missing-${currentValue}`,
|
id: `missing-${currentValue}`,
|
||||||
mediaSrc: '',
|
previewUrl: '',
|
||||||
name: currentValue,
|
name: currentValue,
|
||||||
label: getDisplayLabel(currentValue),
|
label: getDisplayLabel(currentValue)
|
||||||
metadata: ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,14 +187,13 @@ const missingValueItem = computed<DropdownItem | undefined>(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: `missing-${currentValue}`,
|
id: `missing-${currentValue}`,
|
||||||
mediaSrc: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
previewUrl: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
||||||
name: currentValue,
|
name: currentValue,
|
||||||
label: getDisplayLabel(currentValue),
|
label: getDisplayLabel(currentValue)
|
||||||
metadata: ''
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const allItems = computed<DropdownItem[]>(() => {
|
const allItems = computed<AssetDropdownItem[]>(() => {
|
||||||
if (props.isAssetMode && assetData) {
|
if (props.isAssetMode && assetData) {
|
||||||
const items = assetData.dropdownItems.value
|
const items = assetData.dropdownItems.value
|
||||||
if (missingValueItem.value) {
|
if (missingValueItem.value) {
|
||||||
@@ -212,7 +208,7 @@ const allItems = computed<DropdownItem[]>(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const dropdownItems = computed<DropdownItem[]>(() => {
|
const dropdownItems = computed<AssetDropdownItem[]>(() => {
|
||||||
if (props.isAssetMode) {
|
if (props.isAssetMode) {
|
||||||
return allItems.value
|
return allItems.value
|
||||||
}
|
}
|
||||||
@@ -290,8 +286,8 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
function updateSelectedItems(selectedItems: Set<SelectedKey>) {
|
function updateSelectedItems(selectedItems: Set<OptionId>) {
|
||||||
let id: SelectedKey | undefined = undefined
|
let id: OptionId | undefined = undefined
|
||||||
if (selectedItems.size > 0) {
|
if (selectedItems.size > 0) {
|
||||||
id = selectedItems.values().next().value!
|
id = selectedItems.values().next().value!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,19 @@ 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 {
|
||||||
|
FilterOption,
|
||||||
|
OptionId
|
||||||
|
} from '@/platform/assets/types/filterTypes'
|
||||||
|
|
||||||
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 {
|
import type { LayoutMode, SortOption } from './types'
|
||||||
DropdownItem,
|
|
||||||
FilterOption,
|
|
||||||
LayoutMode,
|
|
||||||
OptionId,
|
|
||||||
SelectedKey,
|
|
||||||
SortOption
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: DropdownItem[]
|
items: AssetDropdownItem[]
|
||||||
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,15 +31,15 @@ interface Props {
|
|||||||
filterOptions?: FilterOption[]
|
filterOptions?: FilterOption[]
|
||||||
sortOptions?: SortOption[]
|
sortOptions?: SortOption[]
|
||||||
isSelected?: (
|
isSelected?: (
|
||||||
selected: Set<SelectedKey>,
|
selected: Set<OptionId>,
|
||||||
item: DropdownItem,
|
item: AssetDropdownItem,
|
||||||
index: number
|
index: number
|
||||||
) => boolean
|
) => boolean
|
||||||
searcher?: (
|
searcher?: (
|
||||||
query: string,
|
query: string,
|
||||||
items: DropdownItem[],
|
items: AssetDropdownItem[],
|
||||||
onCleanup: (cleanupFn: () => void) => void
|
onCleanup: (cleanupFn: () => void) => void
|
||||||
) => Promise<DropdownItem[]>
|
) => Promise<AssetDropdownItem[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -54,7 +53,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
searcher: defaultSearcher
|
searcher: defaultSearcher
|
||||||
})
|
})
|
||||||
|
|
||||||
const selected = defineModel<Set<SelectedKey>>('selected', {
|
const selected = defineModel<Set<OptionId>>('selected', {
|
||||||
default: new Set()
|
default: new Set()
|
||||||
})
|
})
|
||||||
const filterSelected = defineModel<OptionId>('filterSelected', { default: '' })
|
const filterSelected = defineModel<OptionId>('filterSelected', { default: '' })
|
||||||
@@ -80,7 +79,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<DropdownItem[]>([])
|
const filteredItems = ref<AssetDropdownItem[]>([])
|
||||||
|
|
||||||
const defaultSorter = computed<SortOption['sorter']>(() => {
|
const defaultSorter = computed<SortOption['sorter']>(() => {
|
||||||
const sorter = props.sortOptions.find(
|
const sorter = props.sortOptions.find(
|
||||||
@@ -99,7 +98,7 @@ const sortedItems = computed(() => {
|
|||||||
return selectedSorter.value({ items: filteredItems.value }) || []
|
return selectedSorter.value({ items: filteredItems.value }) || []
|
||||||
})
|
})
|
||||||
|
|
||||||
function internalIsSelected(item: DropdownItem, index: number): boolean {
|
function internalIsSelected(item: AssetDropdownItem, index: number): boolean {
|
||||||
return props.isSelected?.(selected.value, item, index) ?? false
|
return props.isSelected?.(selected.value, item, index) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +127,7 @@ function handleFileChange(event: Event) {
|
|||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelection(item: DropdownItem, index: number) {
|
function handleSelection(item: AssetDropdownItem, 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,17 @@
|
|||||||
<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 { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import { WidgetInputBaseClass } from '../../layout'
|
import { WidgetInputBaseClass } from '../../layout'
|
||||||
import type { DropdownItem, SelectedKey } from './types'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen?: boolean
|
isOpen?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
items: DropdownItem[]
|
items: AssetDropdownItem[]
|
||||||
selected: Set<SelectedKey>
|
selected: Set<OptionId>
|
||||||
maxSelectable: number
|
maxSelectable: number
|
||||||
uploadable: boolean
|
uploadable: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ 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 {
|
||||||
|
FilterOption,
|
||||||
|
OptionId
|
||||||
|
} from '@/platform/assets/types/filterTypes'
|
||||||
|
|
||||||
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 {
|
import type { LayoutMode, SortOption } from './types'
|
||||||
DropdownItem,
|
|
||||||
FilterOption,
|
|
||||||
LayoutMode,
|
|
||||||
OptionId,
|
|
||||||
SortOption
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: DropdownItem[]
|
items: AssetDropdownItem[]
|
||||||
isSelected: (item: DropdownItem, index: number) => boolean
|
isSelected: (item: AssetDropdownItem, index: number) => boolean
|
||||||
filterOptions: FilterOption[]
|
filterOptions: FilterOption[]
|
||||||
sortOptions: SortOption[]
|
sortOptions: SortOption[]
|
||||||
searcher?: (
|
searcher?: (
|
||||||
@@ -28,7 +28,7 @@ interface Props {
|
|||||||
|
|
||||||
defineProps<Props>()
|
defineProps<Props>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'item-click', item: DropdownItem, index: number): void
|
(e: 'item-click', item: AssetDropdownItem, index: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Define models for two-way binding
|
// Define models for two-way binding
|
||||||
@@ -90,10 +90,9 @@ const searchQuery = defineModel<string>('searchQuery')
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:index="index"
|
:index="index"
|
||||||
:selected="isSelected(item, index)"
|
:selected="isSelected(item, index)"
|
||||||
:media-src="item.mediaSrc"
|
:preview-url="item.previewUrl"
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
:metadata="item.metadata"
|
|
||||||
:layout="layoutMode"
|
:layout="layoutMode"
|
||||||
@click="emit('item-click', item, index)"
|
@click="emit('item-click', item, index)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import type { MaybeRefOrGetter } from 'vue'
|
|||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
import { ref, useTemplateRef } from 'vue'
|
import { ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
|
import type { OptionId } from '@/platform/assets/types/filterTypes'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import FormSearchInput from '../FormSearchInput.vue'
|
import FormSearchInput from '../FormSearchInput.vue'
|
||||||
import type { LayoutMode, OptionId, SortOption } from './types'
|
import type { LayoutMode, SortOption } from './types'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
searcher?: (
|
searcher?: (
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { computed } from 'vue'
|
|||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
|
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
|
||||||
|
import type {
|
||||||
|
FilterOption,
|
||||||
|
OptionId
|
||||||
|
} from '@/platform/assets/types/filterTypes'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import type { FilterOption, OptionId } from './types'
|
|
||||||
|
|
||||||
const { filterOptions } = defineProps<{
|
const { filterOptions } = defineProps<{
|
||||||
filterOptions: FilterOption[]
|
filterOptions: FilterOption[]
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import type { LayoutMode } from './types'
|
|||||||
interface Props {
|
interface Props {
|
||||||
index: number
|
index: number
|
||||||
selected: boolean
|
selected: boolean
|
||||||
mediaSrc: string
|
previewUrl: string
|
||||||
name: string
|
name: string
|
||||||
label?: string
|
label?: string
|
||||||
metadata?: string
|
|
||||||
layout?: LayoutMode
|
layout?: LayoutMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,16 +101,16 @@ function handleVideoLoad(event: Event) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<video
|
<video
|
||||||
v-if="mediaSrc && isVideo"
|
v-if="previewUrl && isVideo"
|
||||||
:src="mediaSrc"
|
:src="previewUrl"
|
||||||
class="size-full object-cover"
|
class="size-full object-cover"
|
||||||
preload="metadata"
|
preload="metadata"
|
||||||
muted
|
muted
|
||||||
@loadeddata="handleVideoLoad"
|
@loadeddata="handleVideoLoad"
|
||||||
/>
|
/>
|
||||||
<LazyImage
|
<LazyImage
|
||||||
v-else-if="mediaSrc"
|
v-else-if="previewUrl"
|
||||||
:src="mediaSrc"
|
:src="previewUrl"
|
||||||
:alt="name"
|
:alt="name"
|
||||||
image-class="size-full object-cover"
|
image-class="size-full object-cover"
|
||||||
@load="handleImageLoad"
|
@load="handleImageLoad"
|
||||||
@@ -146,9 +145,9 @@ function handleVideoLoad(event: Event) {
|
|||||||
{{ label ?? name }}
|
{{ label ?? name }}
|
||||||
</span>
|
</span>
|
||||||
<!-- Meta Data -->
|
<!-- Meta Data -->
|
||||||
<span class="text-secondary block text-xs">{{
|
<span v-if="actualDimensions" class="text-secondary block text-xs">
|
||||||
metadata || actualDimensions
|
{{ actualDimensions }}
|
||||||
}}</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
||||||
import type { DropdownItem } from './types'
|
|
||||||
|
|
||||||
function createItem(name: string, label?: string): DropdownItem {
|
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
||||||
|
|
||||||
|
function createItem(name: string, label?: string): AssetDropdownItem {
|
||||||
return {
|
return {
|
||||||
id: name,
|
id: name,
|
||||||
mediaSrc: '',
|
previewUrl: '',
|
||||||
name,
|
name,
|
||||||
label,
|
label
|
||||||
metadata: ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('defaultSearcher', () => {
|
describe('defaultSearcher', () => {
|
||||||
const items: DropdownItem[] = [
|
const items: AssetDropdownItem[] = [
|
||||||
createItem('apple.png'),
|
createItem('apple.png'),
|
||||||
createItem('banana.jpg'),
|
createItem('banana.jpg'),
|
||||||
createItem('cherry.gif')
|
createItem('cherry.gif')
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
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 { DropdownItem, SortOption } from './types'
|
import type { SortOption } from './types'
|
||||||
|
|
||||||
export async function defaultSearcher(query: string, items: DropdownItem[]) {
|
export async function defaultSearcher(
|
||||||
|
query: string,
|
||||||
|
items: AssetDropdownItem[]
|
||||||
|
) {
|
||||||
if (query.trim() === '') return items
|
if (query.trim() === '') return items
|
||||||
const words = query.trim().toLowerCase().split(' ')
|
const words = query.trim().toLowerCase().split(' ')
|
||||||
return items.filter((item) => {
|
return items.filter((item) => {
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
import type { ComputedRef, InjectionKey } from 'vue'
|
import type { ComputedRef, InjectionKey } from 'vue'
|
||||||
|
|
||||||
import type {
|
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
||||||
FilterOption,
|
import type { OptionId } from '@/platform/assets/types/filterTypes'
|
||||||
OptionId
|
|
||||||
} from '@/platform/assets/types/filterTypes'
|
|
||||||
import type { AssetKind } from '@/types/widgetTypes'
|
import type { AssetKind } from '@/types/widgetTypes'
|
||||||
|
|
||||||
export type { FilterOption, OptionId }
|
|
||||||
|
|
||||||
export type SelectedKey = OptionId
|
|
||||||
|
|
||||||
export interface DropdownItem {
|
|
||||||
id: SelectedKey
|
|
||||||
mediaSrc: string // URL for image, video, or other media
|
|
||||||
name: string
|
|
||||||
label?: string
|
|
||||||
metadata: 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 DropdownItem[] }) => DropdownItem[]
|
sorter: (ctx: { items: readonly AssetDropdownItem[] }) => AssetDropdownItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ describe('useAssetWidgetData (cloud mode, isCloud=true)', () => {
|
|||||||
expect(item.id).toBe('asset-1')
|
expect(item.id).toBe('asset-1')
|
||||||
expect(item.name).toBe('models/beautiful_model.safetensors')
|
expect(item.name).toBe('models/beautiful_model.safetensors')
|
||||||
expect(item.label).toBe('Beautiful Model')
|
expect(item.label).toBe('Beautiful Model')
|
||||||
expect(item.mediaSrc).toBe('/api/preview/asset-1')
|
expect(item.previewUrl).toBe('/api/preview/asset-1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles API errors gracefully', async () => {
|
it('handles API errors gracefully', async () => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { computed, toValue, watch } from 'vue'
|
import { computed, toValue, watch } from 'vue'
|
||||||
import type { MaybeRefOrGetter } from 'vue'
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
|
||||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||||
import type { DropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
import type { AssetDropdownItem } from '@/platform/assets/types/assetDropdownTypes'
|
||||||
|
import { toAssetDropdownItem } from '@/platform/assets/utils/assetDropdownUtils'
|
||||||
|
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'
|
||||||
|
|
||||||
@@ -47,15 +48,8 @@ export function useAssetWidgetData(
|
|||||||
return resolvedType ? (assetsStore.getError(resolvedType) ?? null) : null
|
return resolvedType ? (assetsStore.getError(resolvedType) ?? null) : null
|
||||||
})
|
})
|
||||||
|
|
||||||
const dropdownItems = computed<DropdownItem[]>(() => {
|
const dropdownItems = computed<AssetDropdownItem[]>(() => {
|
||||||
return (assets.value ?? []).map((asset) => ({
|
return (assets.value ?? []).map(toAssetDropdownItem)
|
||||||
id: asset.id,
|
|
||||||
name:
|
|
||||||
(asset.user_metadata?.filename as string | undefined) ?? asset.name,
|
|
||||||
label: asset.name,
|
|
||||||
mediaSrc: asset.preview_url ?? '',
|
|
||||||
metadata: ''
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
Reference in New Issue
Block a user