feat: save/load/delete filters directly in FilterPanel dropdown

This commit is contained in:
Agent Zero
2026-07-28 13:59:15 +02:00
parent 2796bebb12
commit cd1e15eb09
2 changed files with 88 additions and 11 deletions
@@ -4,7 +4,7 @@
*/
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Filter, Plus, X, ChevronDown } from 'lucide-react';
import { Filter, Plus, X, ChevronDown, Bookmark } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
// ─── Field definitions ───────────────────────────────────────────────────────
@@ -215,10 +215,20 @@ function getOrderedFields(contactType?: 'company' | 'person' | undefined): Filte
// ─── Component ────────────────────────────────────────────────────────────────
export interface SavedFilter {
id: string;
name: string;
filterState: FilterState;
}
interface FilterPanelProps {
filters: FilterState;
onFiltersChange: (filters: FilterState) => void;
contactType?: 'company' | 'person' | undefined;
savedFilters?: SavedFilter[];
onSaveFilter?: (name: string, filterState: FilterState) => void;
onLoadFilter?: (filterState: FilterState) => void;
onDeleteFilter?: (id: string) => void;
}
let condIdCounter = 0;
@@ -226,7 +236,7 @@ function newConditionId() {
return `cond-${Date.now()}-${++condIdCounter}`;
}
export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPanelProps) {
export function FilterPanel({ filters, onFiltersChange, contactType, savedFilters = [], onSaveFilter, onLoadFilter, onDeleteFilter }: FilterPanelProps) {
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -290,6 +300,14 @@ export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPan
onFiltersChange({ ...filters, logic: filters.logic === 'AND' ? 'OR' : 'AND' });
};
const handleSaveFilter = () => {
if (!onSaveFilter) return;
if (filters.conditions.length === 0) return;
const name = window.prompt('Name für diesen Filter:', 'Mein Filter');
if (!name) return;
onSaveFilter(name, { ...filters, conditions: filters.conditions.map((c) => ({ ...c })) });
};
// Group fields by group for the dropdown
const groupedFields = useMemo(() => {
const groups: Record<string, FilterFieldDef[]> = {};
@@ -423,6 +441,32 @@ export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPan
</div>
)}
{/* Saved filters */}
{savedFilters.length > 0 && (
<div className="px-4 py-2 border-b border-secondary-100">
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">Gespeicherte Filter</div>
<div className="space-y-0.5">
{savedFilters.map((sf) => (
<div key={sf.id} className="flex items-center gap-1 group">
<button
onClick={() => onLoadFilter?.(sf.filterState)}
className="flex-1 flex items-center gap-1.5 px-2 py-1 text-xs text-left rounded text-secondary-700 hover:bg-secondary-100 transition-colors"
>
<Bookmark className="w-3 h-3 text-primary-500 flex-shrink-0" />
<span className="truncate">{sf.name}</span>
</button>
<button
onClick={() => onDeleteFilter?.(sf.id)}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-secondary-400 hover:text-red-600 transition-opacity"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
</div>
)}
{/* Filter rows */}
<div className="px-4 py-3 space-y-2">
{filters.conditions.length === 0 && (
@@ -526,13 +570,24 @@ export function FilterPanel({ filters, onFiltersChange, contactType }: FilterPan
{/* Footer */}
<div className="flex items-center justify-between px-4 py-3 border-t border-secondary-100">
<button
onClick={addCondition}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Bedingung hinzufügen
</button>
<div className="flex items-center gap-2">
<button
onClick={addCondition}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Bedingung hinzufügen
</button>
{activeCount > 0 && onSaveFilter && (
<button
onClick={handleSaveFilter}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Bookmark className="w-3.5 h-3.5" />
Filter speichern
</button>
)}
</div>
<button
onClick={() => setOpen(false)}
className="px-3 py-1 text-xs font-medium text-white bg-primary-600 hover:bg-primary-700 rounded transition-colors"
+24 -2
View File
@@ -20,7 +20,7 @@ import { SavedFilterBar } from '@/components/common/SavedFilterBar';
import { TagSelector } from '@/components/tags/TagSelector';
import type { Tag } from '@/api/tags';
import { ArrowDownAZ, ArrowUpZA, Bookmark, ChevronLeft, ExternalLink, LayoutGrid, List, Plus, Printer, Table2, X } from 'lucide-react';
import { FilterPanel, applyFilters, emptyFilterState, type FilterState } from '@/components/contacts/FilterPanel';
import { FilterPanel, applyFilters, emptyFilterState, type FilterState, type SavedFilter } from '@/components/contacts/FilterPanel';
import { SortPanel, applySorting, emptySortState, type SortState } from '@/components/contacts/SortPanel';
import { GroupPanel, applyGrouping, emptyGroupState, type GroupState, type GroupedContacts } from '@/components/contacts/GroupPanel';
import {
@@ -53,6 +53,7 @@ export function ContactsListPage() {
const [multiSelectFolders, setMultiSelectFolders] = useState<string[]>([]);
const [savedViews, setSavedViews] = useState<{ id: string; name: string; filterState: FilterState; sortState: SortState; groupState: GroupState; selectedFilter: ContactFilter; viewMode: ContactViewMode }[]>([]);
const [activeViewId, setActiveViewId] = useState<string | null>(null);
const [savedFilters, setSavedFilters] = useState<SavedFilter[]>([]);
const openWindow = useWindowStore((s) => s.openWindow);
// Debounce search
@@ -235,6 +236,23 @@ export function ContactsListPage() {
if (activeViewId === id) setActiveViewId(null);
}, [activeViewId]);
// Save current filter configuration
const handleSaveFilter = useCallback((name: string, filterState: FilterState) => {
const id = `filter-${Date.now()}`;
setSavedFilters((prev) => [...prev, { id, name, filterState: { ...filterState, conditions: filterState.conditions.map((c) => ({ ...c })) } }]);
}, []);
// Load a saved filter
const handleLoadFilter = useCallback((filterState: FilterState) => {
setFilterState({ ...filterState, conditions: filterState.conditions.map((c) => ({ ...c })) });
setPage(1);
}, []);
// Delete a saved filter
const handleDeleteFilter = useCallback((id: string) => {
setSavedFilters((prev) => prev.filter((f) => f.id !== id));
}, []);
// Sort options
const sortOptions = useMemo(() => [
{ value: 'displayname', label: t('contacts.fullName') },
@@ -305,6 +323,10 @@ export function ContactsListPage() {
filters={filterState}
onFiltersChange={setFilterState}
contactType={contactType}
savedFilters={savedFilters}
onSaveFilter={handleSaveFilter}
onLoadFilter={handleLoadFilter}
onDeleteFilter={handleDeleteFilter}
/>
),
onClick: () => {},
@@ -376,7 +398,7 @@ export function ContactsListPage() {
];
registerItems('contacts', items);
return () => unregisterPlugin('contacts');
}, [handleSearch, handleCreate, handleSelectFilter, handleSaveView, applySavedView, deleteSavedView, t, registerItems, unregisterPlugin, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType, sortState, groupState, savedViews, activeViewId, multiSelectFolders]);
}, [handleSearch, handleCreate, handleSelectFilter, handleSaveView, applySavedView, deleteSavedView, handleSaveFilter, handleLoadFilter, handleDeleteFilter, t, registerItems, unregisterPlugin, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType, sortState, groupState, savedViews, activeViewId, multiSelectFolders, savedFilters]);
return (
<div className="flex flex-col h-full" data-testid="contacts-list-page">