Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
:on-click="showUploadDialog"
>
<template #icon>
<i class="icon-[lucide--package-plus]" />
<i class="icon-[lucide--folder-input]" />
</template>
</IconTextButton>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/platform/assets/components/AssetCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
/>
Expand Down
45 changes: 23 additions & 22 deletions src/platform/assets/components/AssetFilterBar.vue
Original file line number Diff line number Diff line change
@@ -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"
/>
Expand All @@ -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"
>
Expand All @@ -48,43 +54,38 @@ import type { SelectOption } from '@/components/input/types'
import { t } from '@/i18n'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but it's definitely a mistake that we are using @/i18n here instead of the useI18n. Basically forgoing reactivity, right?

Another thing we could add a linter rule for ("prefer reactive translations in component setup functions").

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[]
baseModels: string[]
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),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,49 @@
<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'
)
"
@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"
>
Expand Down
17 changes: 0 additions & 17 deletions tests-ui/platform/assets/components/AssetFilterBar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down