mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-02 20:22:08 +00:00
feat: add ownership and base model filtering, unify asset/dropdown types (#8497)
Add ownership and base model filtering to AssetBrowserModal and FormDropdown widgets. ## Changes - **Ownership filter**: Filter by All/My Models/Public Models (uses `is_immutable` field) - **Base model filter**: Multi-select filter with Clear Filters button - **Type unification**: Replace `AssetDropdownItem` with `FormDropdownItem` - **Sorting unification**: Extract shared utilities to `assetSortUtils.ts` - **UI refactor**: Use `Button` component, Vue 3.5 prop shorthand, i18n improvements --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,17 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { capitalize } from 'es-toolkit'
|
||||
import { computed, provide, ref, toRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { t } from '@/i18n'
|
||||
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
|
||||
import {
|
||||
filterItemByBaseModels,
|
||||
filterItemByOwnership
|
||||
} from '@/platform/assets/utils/assetFilterUtils'
|
||||
import {
|
||||
getAssetBaseModels,
|
||||
getAssetDisplayName,
|
||||
getAssetFilename
|
||||
} from '@/platform/assets/utils/assetMetadataUtils'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import FormDropdown from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.vue'
|
||||
import type {
|
||||
FilterOption,
|
||||
OwnershipOption
|
||||
} from '@/platform/assets/types/filterTypes'
|
||||
import { AssetKindKey } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||
import type {
|
||||
DropdownItem,
|
||||
FilterOption,
|
||||
LayoutMode,
|
||||
SelectedKey
|
||||
FormDropdownItem,
|
||||
LayoutMode
|
||||
} from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
|
||||
import WidgetLayoutField from '@/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue'
|
||||
import { useAssetWidgetData } from '@/renderer/extensions/vueNodes/widgets/composables/useAssetWidgetData'
|
||||
@@ -49,6 +61,7 @@ const modelValue = defineModel<string | undefined>({
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const toastStore = useToastStore()
|
||||
const queueStore = useQueueStore()
|
||||
|
||||
@@ -72,16 +85,30 @@ const filterSelected = ref('all')
|
||||
const filterOptions = computed<FilterOption[]>(() => {
|
||||
if (props.isAssetMode) {
|
||||
const categoryName = assetData?.category.value ?? 'All'
|
||||
return [{ id: 'all', name: capitalize(categoryName) }]
|
||||
return [{ name: capitalize(categoryName), value: 'all' }]
|
||||
}
|
||||
return [
|
||||
{ id: 'all', name: 'All' },
|
||||
{ id: 'inputs', name: 'Inputs' },
|
||||
{ id: 'outputs', name: 'Outputs' }
|
||||
{ name: 'All', value: 'all' },
|
||||
{ name: 'Inputs', value: 'inputs' },
|
||||
{ name: 'Outputs', value: 'outputs' }
|
||||
]
|
||||
})
|
||||
|
||||
const selectedSet = ref<Set<SelectedKey>>(new Set())
|
||||
const ownershipSelected = ref<OwnershipOption>('all')
|
||||
const showOwnershipFilter = computed(() => props.isAssetMode)
|
||||
|
||||
const { ownershipOptions, availableBaseModels } = useAssetFilterOptions(
|
||||
() => assetData?.assets.value ?? []
|
||||
)
|
||||
|
||||
const baseModelSelected = ref<Set<string>>(new Set())
|
||||
const showBaseModelFilter = computed(() => props.isAssetMode)
|
||||
const baseModelOptions = computed<FilterOption[]>(() => {
|
||||
if (!props.isAssetMode || !assetData) return []
|
||||
return availableBaseModels.value
|
||||
})
|
||||
|
||||
const selectedSet = ref<Set<string>>(new Set())
|
||||
|
||||
/**
|
||||
* Transforms a value using getOptionLabel if available.
|
||||
@@ -100,7 +127,7 @@ function getDisplayLabel(value: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
const inputItems = computed<DropdownItem[]>(() => {
|
||||
const inputItems = computed<FormDropdownItem[]>(() => {
|
||||
const values = props.widget.options?.values || []
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
@@ -109,13 +136,12 @@ const inputItems = computed<DropdownItem[]>(() => {
|
||||
|
||||
return values.map((value: string, index: number) => ({
|
||||
id: `input-${index}`,
|
||||
mediaSrc: getMediaUrl(value, 'input'),
|
||||
preview_url: getMediaUrl(value, 'input'),
|
||||
name: value,
|
||||
label: getDisplayLabel(value),
|
||||
metadata: ''
|
||||
label: getDisplayLabel(value)
|
||||
}))
|
||||
})
|
||||
const outputItems = computed<DropdownItem[]>(() => {
|
||||
const outputItems = computed<FormDropdownItem[]>(() => {
|
||||
if (!['image', 'video'].includes(props.assetKind ?? '')) return []
|
||||
|
||||
const outputs = new Set<string>()
|
||||
@@ -140,10 +166,9 @@ const outputItems = computed<DropdownItem[]>(() => {
|
||||
|
||||
return Array.from(outputs).map((output) => ({
|
||||
id: `output-${output}`,
|
||||
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||
preview_url: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||
name: output,
|
||||
label: getDisplayLabel(output),
|
||||
metadata: ''
|
||||
label: getDisplayLabel(output)
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -153,23 +178,22 @@ const outputItems = computed<DropdownItem[]>(() => {
|
||||
* where the saved value may not exist in the current server environment.
|
||||
* Works for both local mode (inputItems/outputItems) and cloud mode (assetData).
|
||||
*/
|
||||
const missingValueItem = computed<DropdownItem | undefined>(() => {
|
||||
const missingValueItem = computed<FormDropdownItem | undefined>(() => {
|
||||
const currentValue = modelValue.value
|
||||
if (!currentValue) return undefined
|
||||
|
||||
// Check in cloud mode assets
|
||||
if (props.isAssetMode && assetData) {
|
||||
const existsInAssets = assetData.dropdownItems.value.some(
|
||||
(item) => item.name === currentValue
|
||||
const existsInAssets = assetData.assets.value.some(
|
||||
(asset) => getAssetFilename(asset) === currentValue
|
||||
)
|
||||
if (existsInAssets) return undefined
|
||||
|
||||
return {
|
||||
id: `missing-${currentValue}`,
|
||||
mediaSrc: '',
|
||||
preview_url: '',
|
||||
name: currentValue,
|
||||
label: getDisplayLabel(currentValue),
|
||||
metadata: ''
|
||||
label: getDisplayLabel(currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,20 +214,45 @@ const missingValueItem = computed<DropdownItem | undefined>(() => {
|
||||
|
||||
return {
|
||||
id: `missing-${currentValue}`,
|
||||
mediaSrc: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
||||
preview_url: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
||||
name: currentValue,
|
||||
label: getDisplayLabel(currentValue),
|
||||
metadata: ''
|
||||
label: getDisplayLabel(currentValue)
|
||||
}
|
||||
})
|
||||
|
||||
const allItems = computed<DropdownItem[]>(() => {
|
||||
/**
|
||||
* 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,
|
||||
is_immutable: asset.is_immutable,
|
||||
base_models: getAssetBaseModels(asset)
|
||||
}))
|
||||
})
|
||||
|
||||
const ownershipFilteredAssetItems = computed<FormDropdownItem[]>(() =>
|
||||
filterItemByOwnership(assetItems.value, ownershipSelected.value)
|
||||
)
|
||||
|
||||
const baseModelFilteredAssetItems = computed<FormDropdownItem[]>(() =>
|
||||
filterItemByBaseModels(
|
||||
ownershipFilteredAssetItems.value,
|
||||
baseModelSelected.value
|
||||
)
|
||||
)
|
||||
|
||||
const allItems = computed<FormDropdownItem[]>(() => {
|
||||
if (props.isAssetMode && assetData) {
|
||||
const items = assetData.dropdownItems.value
|
||||
if (missingValueItem.value) {
|
||||
return [missingValueItem.value, ...items]
|
||||
return [missingValueItem.value, ...baseModelFilteredAssetItems.value]
|
||||
}
|
||||
return items
|
||||
return baseModelFilteredAssetItems.value
|
||||
}
|
||||
return [
|
||||
...(missingValueItem.value ? [missingValueItem.value] : []),
|
||||
@@ -212,7 +261,7 @@ const allItems = computed<DropdownItem[]>(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
const dropdownItems = computed<FormDropdownItem[]>(() => {
|
||||
if (props.isAssetMode) {
|
||||
return allItems.value
|
||||
}
|
||||
@@ -290,8 +339,8 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function updateSelectedItems(selectedItems: Set<SelectedKey>) {
|
||||
let id: SelectedKey | undefined = undefined
|
||||
function updateSelectedItems(selectedItems: Set<string>) {
|
||||
let id: string | undefined = undefined
|
||||
if (selectedItems.size > 0) {
|
||||
id = selectedItems.values().next().value!
|
||||
}
|
||||
@@ -307,7 +356,6 @@ function updateSelectedItems(selectedItems: Set<SelectedKey>) {
|
||||
modelValue.value = name
|
||||
}
|
||||
|
||||
// Upload file function (copied from useNodeImageUpload.ts)
|
||||
const uploadFile = async (
|
||||
file: File,
|
||||
isPasted: boolean = false,
|
||||
@@ -339,7 +387,6 @@ const uploadFile = async (
|
||||
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name
|
||||
}
|
||||
|
||||
// Handle multiple file uploads
|
||||
const uploadFiles = async (files: File[]): Promise<string[]> => {
|
||||
const folder = props.uploadFolder ?? 'input'
|
||||
const uploadPromises = files.map((file) =>
|
||||
@@ -363,9 +410,9 @@ async function handleFilesUpdate(files: File[]) {
|
||||
|
||||
// 2. Update widget options to include new files
|
||||
// This simulates what addToComboValues does but for SimplifiedWidget
|
||||
if (props.widget.options?.values) {
|
||||
const values = props.widget.options?.values
|
||||
if (Array.isArray(values)) {
|
||||
uploadedPaths.forEach((path) => {
|
||||
const values = props.widget.options!.values as string[]
|
||||
if (!values.includes(path)) {
|
||||
values.push(path)
|
||||
}
|
||||
@@ -400,12 +447,18 @@ function getMediaUrl(
|
||||
v-model:selected="selectedSet"
|
||||
v-model:filter-selected="filterSelected"
|
||||
v-model:layout-mode="layoutMode"
|
||||
v-model:ownership-selected="ownershipSelected"
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:items="dropdownItems"
|
||||
:placeholder="mediaPlaceholder"
|
||||
:multiple="false"
|
||||
:uploadable="uploadable"
|
||||
:uploadable
|
||||
:accept="acceptTypes"
|
||||
:filter-options="filterOptions"
|
||||
:filter-options
|
||||
:show-ownership-filter
|
||||
:ownership-options
|
||||
:show-base-model-filter
|
||||
:base-model-options
|
||||
v-bind="combinedProps"
|
||||
class="w-full"
|
||||
@update:selected="updateSelectedItems"
|
||||
|
||||
Reference in New Issue
Block a user