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:
Alexander Brown
2026-02-01 20:01:18 -08:00
committed by GitHub
parent 4e20b7522b
commit eaa3ff1579
30 changed files with 1200 additions and 368 deletions

View File

@@ -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"