feat: custom views accordion - save/apply/delete named views with filter+sort+group config

This commit is contained in:
Agent Zero
2026-07-28 12:14:45 +02:00
parent 5f02330b2f
commit 77284cbf10
+100 -20
View File
@@ -19,7 +19,7 @@ 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, LayoutGrid, List, Plus, Printer, Table2 } from 'lucide-react';
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 { SortPanel, applySorting, emptySortState, type SortState } from '@/components/contacts/SortPanel';
import { GroupPanel, applyGrouping, emptyGroupState, type GroupState, type GroupedContacts } from '@/components/contacts/GroupPanel';
@@ -50,6 +50,8 @@ export function ContactsListPage() {
const [groupState, setGroupState] = useState<GroupState>(emptyGroupState);
const [viewsOpen, setViewsOpen] = useState(true);
const [foldersOpen, setFoldersOpen] = useState(true);
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 openWindow = useWindowStore((s) => s.openWindow);
// Debounce search
@@ -195,6 +197,40 @@ export function ContactsListPage() {
setActiveView('list');
}, []);
// Save current view configuration as a custom view
const handleSaveView = useCallback(() => {
const name = window.prompt('Name für diese Ansicht:', 'Meine Ansicht');
if (!name) return;
const id = `view-${Date.now()}`;
setSavedViews((prev) => [...prev, {
id,
name,
filterState: { ...filterState },
sortState: { ...sortState },
groupState: { ...groupState },
selectedFilter,
viewMode,
}]);
setActiveViewId(id);
}, [filterState, sortState, groupState, selectedFilter, viewMode]);
// Apply a saved custom view
const applySavedView = useCallback((view: { id: string; filterState: FilterState; sortState: SortState; groupState: GroupState; selectedFilter: ContactFilter; viewMode: ContactViewMode }) => {
setFilterState({ ...view.filterState });
setSortState({ ...view.sortState });
setGroupState({ ...view.groupState });
setSelectedFilter(view.selectedFilter);
setViewMode(view.viewMode);
setActiveViewId(view.id);
setPage(1);
}, []);
// Delete a saved custom view
const deleteSavedView = useCallback((id: string) => {
setSavedViews((prev) => prev.filter((v) => v.id !== id));
if (activeViewId === id) setActiveViewId(null);
}, [activeViewId]);
// Sort options
const sortOptions = useMemo(() => [
{ value: 'displayname', label: t('contacts.fullName') },
@@ -334,7 +370,7 @@ export function ContactsListPage() {
];
registerItems('contacts', items);
return () => unregisterPlugin('contacts');
}, [handleSearch, handleCreate, handleSelectFilter, t, registerItems, unregisterPlugin, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType, sortState, groupState]);
}, [handleSearch, handleCreate, handleSelectFilter, handleSaveView, applySavedView, deleteSavedView, t, registerItems, unregisterPlugin, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType, sortState, groupState, savedViews, activeViewId]);
return (
<div className="flex flex-col h-full" data-testid="contacts-list-page">
@@ -349,7 +385,7 @@ export function ContactsListPage() {
data-testid="contact-folder-pane"
>
<div className="flex flex-col h-full overflow-y-auto">
{/* Accordion 1: Views */}
{/* Accordion 1: Custom Views */}
<div className="border-b border-secondary-200">
<button
onClick={() => setViewsOpen(!viewsOpen)}
@@ -365,23 +401,67 @@ export function ContactsListPage() {
</button>
{viewsOpen && (
<div className="px-3 pb-2 space-y-0.5">
{[
{ value: 'list', label: t('contacts.viewList'), icon: <List className="w-3.5 h-3.5" strokeWidth={2} /> },
{ value: 'table', label: t('contacts.viewTable'), icon: <Table2 className="w-3.5 h-3.5" strokeWidth={2} /> },
{ value: 'cards', label: t('contacts.viewCards'), icon: <LayoutGrid className="w-3.5 h-3.5" strokeWidth={2} /> },
].map((opt) => (
<button
key={opt.value}
onClick={() => setViewMode(opt.value as ContactViewMode)}
className={`
w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left transition-colors
${viewMode === opt.value ? 'bg-primary-100 text-primary-700 font-medium' : 'text-secondary-600 hover:bg-secondary-100'}
`}
>
{opt.icon}
{opt.label}
</button>
))}
{/* Default view */}
<button
onClick={() => {
setFilterState(emptyFilterState);
setSortState(emptySortState);
setGroupState(emptyGroupState);
setSelectedFilter('all');
setViewMode('list');
}}
className={`
w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left transition-colors
${!filterState.conditions.length && !sortState.conditions.length && !groupState.conditions.length && selectedFilter === 'all'
? 'bg-primary-100 text-primary-700 font-medium'
: 'text-secondary-600 hover:bg-secondary-100'}
`}
>
<List className="w-3.5 h-3.5" strokeWidth={2} />
Standardansicht
</button>
{/* Placeholder for saved custom views */}
{savedViews.length === 0 ? (
<div className="px-2 py-3 text-center">
<p className="text-[11px] text-secondary-400 mb-2">Keine benutzerdefinierten Ansichten</p>
<button
onClick={handleSaveView}
className="inline-flex items-center gap-1 px-2 py-1 text-[11px] text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3 h-3" />
Aktuelle Ansicht speichern
</button>
</div>
) : (
<>
{savedViews.map((view) => (
<button
key={view.id}
onClick={() => applySavedView(view)}
className={`
w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-left transition-colors group
${activeViewId === view.id ? 'bg-primary-100 text-primary-700 font-medium' : 'text-secondary-600 hover:bg-secondary-100'}
`}
>
<Bookmark className="w-3.5 h-3.5 flex-shrink-0" strokeWidth={2} />
<span className="flex-1 truncate">{view.name}</span>
<button
onClick={(e) => { e.stopPropagation(); deleteSavedView(view.id); }}
className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-red-600 transition-opacity"
>
<X className="w-3 h-3" />
</button>
</button>
))}
<button
onClick={handleSaveView}
className="w-full flex items-center gap-1 px-2 py-1.5 rounded text-xs text-primary-600 hover:bg-primary-50 transition-colors"
>
<Plus className="w-3 h-3" />
Aktuelle Ansicht speichern
</button>
</>
)}
</div>
)}
</div>