feat: SmartSuite-style FilterPanel with multi-condition AND/OR, all contact fields, context-sensitive ordering
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user