feat: SmartSuite-style FilterPanel with multi-condition AND/OR, all contact fields, context-sensitive ordering

This commit is contained in:
Agent Zero
2026-07-28 09:19:02 +02:00
parent 7962d34fcf
commit e95875464b
4 changed files with 589 additions and 27 deletions
+39 -26
View File
@@ -19,7 +19,8 @@ import { SavedFilters } from '@/components/SavedFilters';
import { SavedFilterBar } from '@/components/common/SavedFilterBar';
import { TagSelector } from '@/components/tags/TagSelector';
import type { Tag } from '@/api/tags';
import { ArrowDownAZ, ArrowUpZA, Bookmark, ChevronLeft, ExternalLink, Filter, LayoutGrid, List, Plus, Printer, Table2 } from 'lucide-react';
import { ArrowDownAZ, ArrowUpZA, Bookmark, ChevronLeft, ExternalLink, LayoutGrid, List, Plus, Printer, Table2 } from 'lucide-react';
import { FilterPanel, applyFilters, emptyFilterState, type FilterState } from '@/components/contacts/FilterPanel';
import {
useUnifiedContacts,
useUnifiedContact,
@@ -42,6 +43,7 @@ export function ContactsListPage() {
const [activeView, setActiveView] = useState<'folders' | 'list' | 'detail'>('folders');
const [savedFiltersOpen, setSavedFiltersOpen] = useState(false);
const [selectedTags, setSelectedTags] = useState<Tag[]>([]);
const [filterState, setFilterState] = useState<FilterState>(emptyFilterState);
const openWindow = useWindowStore((s) => s.openWindow);
// Debounce search
@@ -100,14 +102,19 @@ export function ContactsListPage() {
return Array.from(tagSet).sort();
}, [contacts]);
// Filter by tag client-side if tag filter is active
// Filter by tag client-side if tag filter is active, then apply FilterPanel conditions
const filteredContacts = useMemo(() => {
if (!tagFilter) return contacts;
return contacts.filter((c) => {
if (!c.tags) return false;
return c.tags.split(',').map((t) => t.trim()).includes(tagFilter);
});
}, [contacts, tagFilter]);
let result = contacts;
if (tagFilter) {
result = result.filter((c) => {
if (!c.tags) return false;
return c.tags.split(',').map((t) => t.trim()).includes(tagFilter);
});
}
// Apply FilterPanel conditions
result = applyFilters(result, filterState);
return result;
}, [contacts, tagFilter, filterState]);
// Handle folder selection
const handleSelectFilter = useCallback((filter: ContactFilter) => {
@@ -209,32 +216,38 @@ export function ContactsListPage() {
onSearch: handleSearch,
onClick: () => {},
},
// Unified filter dropdown — filter + sort in one panel
// FilterPanel — SmartSuite-style multi-condition filter
{
id: 'filter-sort',
id: 'filter-panel',
plugin: 'contacts',
label: t('common.filter', 'Filter'),
type: 'dropdown' as const,
label: 'Filter',
type: 'custom' as const,
group: 'filter',
icon: <Filter className="w-3.5 h-3.5" strokeWidth={2} />,
iconOnly: true,
menuWidth: '240px',
customComponent: (
<FilterPanel
filters={filterState}
onFiltersChange={setFilterState}
contactType={contactType}
/>
),
onClick: () => {},
},
// Sort dropdown
{
id: 'sort-by',
plugin: 'contacts',
label: t('common.sort', 'Sortieren'),
type: 'dropdown' as const,
group: 'sort',
icon: <ArrowDownAZ className="w-3.5 h-3.5" strokeWidth={2} />,
menuWidth: '200px',
menuOptions: [
// Section: Filter
{ value: 'all', label: t('common.all'), section: t('common.filter', 'Filter'), active: selectedFilter === 'all', onClick: () => handleSelectFilter('all') },
{ value: 'company', label: t('contacts.companies'), active: selectedFilter === 'company', onClick: () => handleSelectFilter('company') },
{ value: 'person', label: t('contacts.persons'), active: selectedFilter === 'person', onClick: () => handleSelectFilter('person') },
// Separator
{ value: 'sep-sort', label: '', separator: true, onClick: () => {} },
// Section: Sort by
...sortOptions.map((opt) => ({
value: `sort-${opt.value}`, label: opt.label, section: t('common.sortBy', 'Sortieren nach'),
active: sortBy === opt.value, onClick: () => { setSortBy(opt.value); setPage(1); },
})),
// Separator
{ value: 'sep-order', label: '', separator: true, onClick: () => {} },
// Section: Sort order
{ value: 'sort-asc', label: t('common.sortAsc', 'Aufsteigend'), section: t('common.sortOrder', 'Sortierreihenfolge'),
{ value: 'sort-asc', label: t('common.sortAsc', 'Aufsteigend'), section: t('common.sortOrder', 'Reihenfolge'),
icon: <ArrowDownAZ className="w-3.5 h-3.5" strokeWidth={2} />, active: sortOrder === 'asc', onClick: () => handleSortChange(sortBy, 'asc') },
{ value: 'sort-desc', label: t('common.sortDesc', 'Absteigend'),
icon: <ArrowUpZA className="w-3.5 h-3.5" strokeWidth={2} />, active: sortOrder === 'desc', onClick: () => handleSortChange(sortBy, 'desc') },
@@ -300,7 +313,7 @@ export function ContactsListPage() {
];
registerItems('contacts', items);
return () => unregisterPlugin('contacts');
}, [handleSearch, handleCreate, handleSelectFilter, handleSortChange, t, registerItems, unregisterPlugin, sortBy, sortOrder, selectedFilter, viewMode, sortOptions, viewModeOptions]);
}, [handleSearch, handleCreate, handleSelectFilter, handleSortChange, t, registerItems, unregisterPlugin, sortBy, sortOrder, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType]);
return (
<div className="flex flex-col h-full" data-testid="contacts-list-page">