mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 01:20:03 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
:on-click="showUploadDialog"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--package-plus]" />
|
||||
<i class="icon-[lucide--folder-input]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user