Design: Model management (#7190)

## Summary

Assorted updates to the components involved in uploading personal
models.

## Changes

- Standardize Import buttons
- Let the images fill the space on the card
- Order by recent by default
- Nicer display on the model select popover

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7190-Design-Model-management-2c06d73d365081e7b9fed7a83b730c0f)
by [Unito](https://www.unito.io)
This commit is contained in:
Alexander Brown
2025-12-05 14:53:28 -08:00
committed by GitHub
parent ec1a7f1da4
commit 57523a0c57
6 changed files with 43 additions and 52 deletions

View File

@@ -2091,7 +2091,7 @@
"connectionError": "Please check your connection and try again",
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
"noModelsInFolder": "No {type} available in this folder",
"uploadModel": "Import model",
"uploadModel": "Import",
"uploadModelFromCivitai": "Import a model from Civitai",
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",

View File

@@ -39,7 +39,7 @@
:on-click="showUploadDialog"
>
<template #icon>
<i class="icon-[lucide--package-plus]" />
<i class="icon-[lucide--folder-input]" />
</template>
</IconTextButton>
</div>

View File

@@ -26,7 +26,7 @@
v-else
:src="asset.preview_url"
:alt="displayName"
class="size-full object-contain cursor-pointer"
class="size-full object-cover cursor-pointer"
role="button"
@click.self="interactive && $emit('select', asset)"
/>

View File

@@ -1,12 +1,18 @@
<template>
<div :class="containerClasses" data-component-id="asset-filter-bar">
<div :class="leftSideClasses" data-component-id="asset-filter-bar-left">
<div
class="flex gap-4 items-center justify-between px-6 pt-2 pb-6"
data-component-id="asset-filter-bar"
>
<div
class="flex gap-4 items-center"
data-component-id="asset-filter-bar-left"
>
<MultiSelect
v-if="availableFileFormats.length > 0"
v-model="fileFormats"
:label="$t('assetBrowser.fileFormats')"
:options="availableFileFormats"
:class="selectClasses"
class="min-w-32"
data-component-id="asset-filter-file-formats"
@update:model-value="handleFilterChange"
/>
@@ -16,18 +22,18 @@
v-model="baseModels"
:label="$t('assetBrowser.baseModels')"
:options="availableBaseModels"
:class="selectClasses"
class="min-w-32"
data-component-id="asset-filter-base-models"
@update:model-value="handleFilterChange"
/>
</div>
<div :class="rightSideClasses" data-component-id="asset-filter-bar-right">
<div class="flex items-center" data-component-id="asset-filter-bar-right">
<SingleSelect
v-model="sortBy"
:label="$t('assetBrowser.sortBy')"
:options="sortOptions"
:class="selectClasses"
class="min-w-32"
data-component-id="asset-filter-sort"
@update:model-value="handleFilterChange"
>
@@ -48,7 +54,6 @@ import type { SelectOption } from '@/components/input/types'
import { t } from '@/i18n'
import { useAssetFilterOptions } from '@/platform/assets/composables/useAssetFilterOptions'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { cn } from '@/utils/tailwindUtil'
export interface FilterState {
fileFormats: string[]
@@ -56,35 +61,31 @@ export interface FilterState {
sortBy: string
}
const SORT_OPTIONS = [
{ name: t('assetBrowser.sortRecent'), value: 'recent' },
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
{ name: t('assetBrowser.sortZA'), value: 'name-desc' }
] as const
type SortOption = (typeof SORT_OPTIONS)[number]['value']
const sortOptions = [...SORT_OPTIONS]
const { assets = [] } = defineProps<{
assets?: AssetItem[]
}>()
const fileFormats = ref<SelectOption[]>([])
const baseModels = ref<SelectOption[]>([])
const sortBy = ref('name-asc')
const sortBy = ref<SortOption>('recent')
const { availableFileFormats, availableBaseModels } =
useAssetFilterOptions(assets)
const sortOptions = [
{ name: t('assetBrowser.sortAZ'), value: 'name-asc' },
{ name: t('assetBrowser.sortZA'), value: 'name-desc' },
{ name: t('assetBrowser.sortRecent'), value: 'recent' }
]
const emit = defineEmits<{
filterChange: [filters: FilterState]
}>()
const containerClasses = cn(
'flex gap-4 items-center justify-between',
'px-6 pt-2 pb-6'
)
const leftSideClasses = cn('flex gap-4 items-center')
const rightSideClasses = cn('flex items-center')
const selectClasses = cn('min-w-32')
function handleFilterChange() {
emit('filterChange', {
fileFormats: fileFormats.value.map((option: SelectOption) => option.value),

View File

@@ -1,31 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
import { cn } from '@/utils/tailwindUtil'
import type { FilterOption, OptionId } from './types'
defineProps<{
const { filterOptions } = defineProps<{
filterOptions: FilterOption[]
}>()
const filterSelected = defineModel<OptionId>('filterSelected')
const { isUploadButtonEnabled, showUploadDialog } = useModelUpload()
// TODO: Add real check to differentiate between the Model dialogs and Load Image
const singleFilterOption = computed(() => filterOptions.length === 1)
</script>
<template>
<div class="text-secondary mb-4 flex gap-1 px-4 justify-between">
<div
<div class="text-secondary mb-4 flex gap-1 px-4 justify-start">
<button
v-for="option in filterOptions"
:key="option.id"
type="button"
:disabled="singleFilterOption"
:class="
cn(
'px-4 py-2 rounded-md inline-flex justify-center items-center cursor-pointer select-none',
'transition-all duration-150',
'hover:text-base-foreground hover:bg-interface-menu-component-surface-hovered',
'active:scale-95',
filterSelected === option.id
'px-4 py-2 rounded-md inline-flex justify-center items-center select-none appearance-none border-0 text-base-foreground',
!singleFilterOption &&
'transition-all duration-150 hover:text-base-foreground hover:bg-interface-menu-component-surface-hovered cursor-pointer active:scale-95',
!singleFilterOption && filterSelected === option.id
? '!bg-interface-menu-component-surface-selected text-base-foreground'
: 'bg-transparent'
)
@@ -33,10 +39,11 @@ const { isUploadButtonEnabled, showUploadDialog } = useModelUpload()
@click="filterSelected = option.id"
>
{{ option.name }}
</div>
</button>
<IconTextButton
v-if="isUploadButtonEnabled"
v-if="isUploadButtonEnabled && singleFilterOption"
:label="$t('g.import')"
class="ml-auto"
type="secondary"
@click="showUploadDialog"
>

View File

@@ -75,23 +75,6 @@ function mountAssetFilterBar(props = {}) {
describe('AssetFilterBar', () => {
describe('Filter State Management', () => {
it('maintains correct initial state', () => {
// Provide assets with options so filters are visible
const assets = [
createAssetWithSpecificExtension('safetensors'),
createAssetWithSpecificBaseModel('sd15')
]
const wrapper = mountAssetFilterBar({ assets })
// Test initial state through component props
const multiSelects = wrapper.findAllComponents({ name: 'MultiSelect' })
const singleSelect = wrapper.findComponent({ name: 'SingleSelect' })
expect(multiSelects[0].props('modelValue')).toEqual([])
expect(multiSelects[1].props('modelValue')).toEqual([])
expect(singleSelect.props('modelValue')).toBe('name-asc')
})
it('handles multiple simultaneous filter changes correctly', async () => {
// Provide assets with options so filters are visible
const assets = [