feat: custom views accordion - save/apply/delete named views with filter+sort+group config
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user