mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-22 15:54:09 +00:00
[feat] Add enhanced filter UI components with search and clear functionality (#5119)
* [feat] Add enhanced filter UI components with search and clear functionality - Add SearchBox, clear all button, and item count to MultiSelect header - Add 'fit-content' size option to button types for flexible sizing - Update SingleSelect and ModelSelector components for consistency - Add localization strings for item selection and clear all functionality Co-Authored-By: Claude <noreply@anthropic.com> * Update locales [skip ci] --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -9,6 +9,41 @@
|
||||
:max-selected-labels="0"
|
||||
:pt="pt"
|
||||
>
|
||||
<template
|
||||
v-if="hasSearchBox || showSelectedCount || hasClearButton"
|
||||
#header
|
||||
>
|
||||
<div class="p-2 flex flex-col gap-y-4 pb-0">
|
||||
<SearchBox
|
||||
v-if="hasSearchBox"
|
||||
v-model="searchQuery"
|
||||
:has-border="true"
|
||||
:place-holder="searchPlaceholder"
|
||||
/>
|
||||
<div class="flex items-center justify-between">
|
||||
<span
|
||||
v-if="showSelectedCount"
|
||||
class="text-sm text-neutral-400 dark-theme:text-zinc-500 px-1"
|
||||
>
|
||||
{{
|
||||
selectedCount > 0
|
||||
? $t('g.itemsSelected', { selectedCount })
|
||||
: $t('g.itemSelected', { selectedCount })
|
||||
}}
|
||||
</span>
|
||||
<TextButton
|
||||
v-if="hasClearButton"
|
||||
:label="$t('g.clearAll')"
|
||||
type="transparent"
|
||||
size="fit-content"
|
||||
class="text-sm !text-blue-500 !dark-theme:text-blue-600"
|
||||
@click.stop="selectedItems = []"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Trigger value (keep text scale identical) -->
|
||||
<template #value>
|
||||
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
|
||||
@@ -42,7 +77,7 @@
|
||||
</template>
|
||||
</MultiSelect>
|
||||
|
||||
<!-- Selected count badge (unchanged) -->
|
||||
<!-- Selected count badge -->
|
||||
<div
|
||||
v-if="selectedCount > 0"
|
||||
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
|
||||
@@ -58,22 +93,41 @@ import MultiSelect, {
|
||||
} from 'primevue/multiselect'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { label, options } = defineProps<{
|
||||
label?: string
|
||||
options: { name: string; value: string }[]
|
||||
}>()
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
|
||||
const selectedItems = defineModel<{ name: string; value: string }[]>({
|
||||
import TextButton from '../button/TextButton.vue'
|
||||
|
||||
type Option = { name: string; value: string }
|
||||
|
||||
interface Props {
|
||||
/** Input label shown on the trigger button */
|
||||
label?: string
|
||||
/** Static options for the multiselect (when not using async search) */
|
||||
options: Option[]
|
||||
/** Show search box in the panel header */
|
||||
hasSearchBox?: boolean
|
||||
/** Show selected count text in the panel header */
|
||||
showSelectedCount?: boolean
|
||||
/** Show "Clear all" action in the panel header */
|
||||
hasClearButton?: boolean
|
||||
/** Placeholder for the search input */
|
||||
searchPlaceholder?: string
|
||||
}
|
||||
const {
|
||||
label,
|
||||
options,
|
||||
hasSearchBox = false,
|
||||
showSelectedCount = false,
|
||||
hasClearButton = false,
|
||||
searchPlaceholder = 'Search...'
|
||||
} = defineProps<Props>()
|
||||
|
||||
const selectedItems = defineModel<Option[]>({
|
||||
required: true
|
||||
})
|
||||
|
||||
const searchQuery = defineModel<string>('searchQuery')
|
||||
const selectedCount = computed(() => selectedItems.value.length)
|
||||
|
||||
/**
|
||||
* Pure unstyled mode using only the PrimeVue PT API.
|
||||
* All PrimeVue built-in checkboxes/headers are hidden via PT (no :deep hacks).
|
||||
* Visual output matches the previous version exactly.
|
||||
*/
|
||||
const pt = computed(() => ({
|
||||
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
||||
class: [
|
||||
@@ -97,19 +151,19 @@ const pt = computed(() => ({
|
||||
dropdown: {
|
||||
class: 'flex shrink-0 cursor-pointer items-center justify-center px-3'
|
||||
},
|
||||
header: { class: 'hidden' },
|
||||
|
||||
header: () => ({
|
||||
class:
|
||||
hasSearchBox || showSelectedCount || hasClearButton ? 'block' : 'hidden'
|
||||
}),
|
||||
// Overlay & list visuals unchanged
|
||||
overlay:
|
||||
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100',
|
||||
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100 dark-theme:border-zinc-700',
|
||||
list: {
|
||||
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
|
||||
},
|
||||
|
||||
// Option row hover tone identical
|
||||
option:
|
||||
'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
||||
|
||||
// Hide built-in checkboxes entirely via PT (no :deep)
|
||||
pcHeaderCheckbox: {
|
||||
root: { class: 'hidden' },
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full items-center rounded-lg px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800"
|
||||
>
|
||||
<i-lucide:search class="text-neutral" />
|
||||
<div :class="wrapperStyle">
|
||||
<i-lucide:search :class="iconColorStyle" />
|
||||
<InputText
|
||||
v-model="searchQuery"
|
||||
:placeholder="placeHolder || 'Search...'"
|
||||
@@ -15,10 +13,21 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { defineModel } from 'vue'
|
||||
import { computed, defineModel } from 'vue'
|
||||
|
||||
const { placeHolder } = defineProps<{
|
||||
const { placeHolder, hasBorder = false } = defineProps<{
|
||||
placeHolder?: string
|
||||
hasBorder?: boolean
|
||||
}>()
|
||||
const searchQuery = defineModel<string>('')
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
return hasBorder
|
||||
? 'flex w-full items-center rounded gap-2 bg-white dark-theme:bg-zinc-800 p-1 border border-solid border-zinc-200 dark-theme:border-zinc-700'
|
||||
: 'flex w-full items-center rounded px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800'
|
||||
})
|
||||
|
||||
const iconColorStyle = computed(() => {
|
||||
return !hasBorder ? 'text-neutral' : 'text-zinc-300 dark-theme:text-zinc-700'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -99,7 +99,7 @@ const pt = computed(() => ({
|
||||
overlay: {
|
||||
class: [
|
||||
// dropdown panel
|
||||
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg'
|
||||
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100 dark-theme:border-zinc-700'
|
||||
]
|
||||
},
|
||||
list: {
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
<div class="relative px-6 pt-2 pb-4 flex gap-2">
|
||||
<MultiSelect
|
||||
v-model="selectedFrameworks"
|
||||
class="w-[250px]"
|
||||
:has-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:has-clear-button="true"
|
||||
label="Select Frameworks"
|
||||
:options="frameworkOptions"
|
||||
/>
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "الفئة",
|
||||
"choose_file_to_upload": "اختر ملفاً للرفع",
|
||||
"clear": "مسح",
|
||||
"clearAll": "مسح الكل",
|
||||
"clearFilters": "مسح الفلاتر",
|
||||
"close": "إغلاق",
|
||||
"color": "اللون",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "مثبت",
|
||||
"installing": "جارٍ التثبيت",
|
||||
"interrupted": "تمت المقاطعة",
|
||||
"itemSelected": "تم تحديد عنصر واحد",
|
||||
"itemsSelected": "تم تحديد {selectedCount} عناصر",
|
||||
"keybinding": "اختصار لوحة المفاتيح",
|
||||
"keybindingAlreadyExists": "الاختصار موجود بالفعل في",
|
||||
"learnMore": "اعرف المزيد",
|
||||
|
||||
@@ -137,8 +137,11 @@
|
||||
"copy": "Copy",
|
||||
"imageUrl": "Image URL",
|
||||
"clear": "Clear",
|
||||
"clearAll": "Clear all",
|
||||
"copyURL": "Copy URL",
|
||||
"releaseTitle": "{package} {version} Release",
|
||||
"itemSelected": "{selectedCount} item selected",
|
||||
"itemsSelected": "{selectedCount} items selected",
|
||||
"progressCountOf": "of",
|
||||
"keybindingAlreadyExists": "Keybinding already exists on",
|
||||
"startRecording": "Start Recording",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "Categoría",
|
||||
"choose_file_to_upload": "elige archivo para subir",
|
||||
"clear": "Limpiar",
|
||||
"clearAll": "Borrar todo",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"close": "Cerrar",
|
||||
"color": "Color",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "Instalado",
|
||||
"installing": "Instalando",
|
||||
"interrupted": "Interrumpido",
|
||||
"itemSelected": "{selectedCount} elemento seleccionado",
|
||||
"itemsSelected": "{selectedCount} elementos seleccionados",
|
||||
"keybinding": "Combinación de teclas",
|
||||
"keybindingAlreadyExists": "La combinación de teclas ya existe en",
|
||||
"learnMore": "Aprende más",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "Catégorie",
|
||||
"choose_file_to_upload": "choisissez le fichier à télécharger",
|
||||
"clear": "Effacer",
|
||||
"clearAll": "Tout effacer",
|
||||
"clearFilters": "Effacer les filtres",
|
||||
"close": "Fermer",
|
||||
"color": "Couleur",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "Installé",
|
||||
"installing": "Installation",
|
||||
"interrupted": "Interrompu",
|
||||
"itemSelected": "{selectedCount} élément sélectionné",
|
||||
"itemsSelected": "{selectedCount} éléments sélectionnés",
|
||||
"keybinding": "Raccourci clavier",
|
||||
"keybindingAlreadyExists": "Le raccourci clavier existe déjà",
|
||||
"learnMore": "En savoir plus",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "カテゴリ",
|
||||
"choose_file_to_upload": "アップロードするファイルを選択",
|
||||
"clear": "クリア",
|
||||
"clearAll": "すべてクリア",
|
||||
"clearFilters": "フィルターをクリア",
|
||||
"close": "閉じる",
|
||||
"color": "色",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "インストール済み",
|
||||
"installing": "インストール中",
|
||||
"interrupted": "中断されました",
|
||||
"itemSelected": "{selectedCount}件選択済み",
|
||||
"itemsSelected": "{selectedCount}件選択済み",
|
||||
"keybinding": "キーバインディング",
|
||||
"keybindingAlreadyExists": "このキー割り当てはすでに存在します",
|
||||
"learnMore": "詳細を学ぶ",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "카테고리",
|
||||
"choose_file_to_upload": "업로드할 파일 선택",
|
||||
"clear": "지우기",
|
||||
"clearAll": "모두 지우기",
|
||||
"clearFilters": "필터 지우기",
|
||||
"close": "닫기",
|
||||
"color": "색상",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "설치됨",
|
||||
"installing": "설치 중",
|
||||
"interrupted": "중단됨",
|
||||
"itemSelected": "{selectedCount}개 선택됨",
|
||||
"itemsSelected": "{selectedCount}개 선택됨",
|
||||
"keybinding": "키 바인딩",
|
||||
"keybindingAlreadyExists": "단축키가 이미 존재합니다",
|
||||
"learnMore": "더 알아보기",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "Категория",
|
||||
"choose_file_to_upload": "выберите файл для загрузки",
|
||||
"clear": "Очистить",
|
||||
"clearAll": "Очистить всё",
|
||||
"clearFilters": "Сбросить фильтры",
|
||||
"close": "Закрыть",
|
||||
"color": "Цвет",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "Установлено",
|
||||
"installing": "Установка",
|
||||
"interrupted": "Прервано",
|
||||
"itemSelected": "Выбран {selectedCount} элемент",
|
||||
"itemsSelected": "Выбрано {selectedCount} элементов",
|
||||
"keybinding": "Привязка клавиш",
|
||||
"keybindingAlreadyExists": "Горячая клавиша уже существует",
|
||||
"learnMore": "Узнать больше",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "分類",
|
||||
"choose_file_to_upload": "選擇要上傳的檔案",
|
||||
"clear": "清除",
|
||||
"clearAll": "全部清除",
|
||||
"clearFilters": "清除篩選",
|
||||
"close": "關閉",
|
||||
"color": "顏色",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "已安裝",
|
||||
"installing": "安裝中",
|
||||
"interrupted": "已中斷",
|
||||
"itemSelected": "已選取 {selectedCount} 項",
|
||||
"itemsSelected": "已選取 {selectedCount} 項",
|
||||
"keybinding": "快捷鍵",
|
||||
"keybindingAlreadyExists": "快捷鍵已存在於",
|
||||
"learnMore": "了解更多",
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
"category": "类别",
|
||||
"choose_file_to_upload": "选择要上传的文件",
|
||||
"clear": "清除",
|
||||
"clearAll": "全部清除",
|
||||
"clearFilters": "清除筛选",
|
||||
"close": "关闭",
|
||||
"color": "颜色",
|
||||
@@ -327,6 +328,8 @@
|
||||
"installed": "已安装",
|
||||
"installing": "正在安装",
|
||||
"interrupted": "已中断",
|
||||
"itemSelected": "已选择 {selectedCount} 项",
|
||||
"itemsSelected": "已选择 {selectedCount} 项",
|
||||
"keybinding": "按键绑定",
|
||||
"keybindingAlreadyExists": "快捷键已存在",
|
||||
"learnMore": "了解更多",
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
export interface BaseButtonProps {
|
||||
size?: 'sm' | 'md'
|
||||
size?: 'fit-content' | 'sm' | 'md'
|
||||
type?: 'primary' | 'secondary' | 'transparent'
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
|
||||
const sizeClasses = {
|
||||
'fit-content': '',
|
||||
sm: 'px-2 py-1.5 text-xs',
|
||||
md: 'px-2.5 py-2 text-sm'
|
||||
}
|
||||
@@ -31,6 +32,7 @@ export const getIconButtonSizeClasses = (
|
||||
size: BaseButtonProps['size'] = 'md'
|
||||
) => {
|
||||
const sizeClasses = {
|
||||
'fit-content': 'w-auto h-auto',
|
||||
sm: 'w-6 h-6 text-xs !rounded-md',
|
||||
md: 'w-8 h-8 text-sm'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user