feat: SmartSuite-style SortPanel with multi-field priority sorting, all contact fields
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* SmartSuite-style Sort Panel for contacts.
|
||||
* Multi-field sorting with priority ordering.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { ArrowDownAZ, ArrowUpZA, Plus, X } from 'lucide-react';
|
||||
import type { UnifiedContact } from '@/api/unifiedContacts';
|
||||
|
||||
// ─── Field definitions (reuse from FilterPanel) ────────────────────────────────
|
||||
|
||||
type FieldType = 'text' | 'number' | 'select' | 'date';
|
||||
|
||||
interface SortFieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
group?: 'general' | 'company' | 'person' | 'address' | 'notes' | 'meta';
|
||||
}
|
||||
|
||||
const SORT_FIELDS: SortFieldDef[] = [
|
||||
// General
|
||||
{ key: 'displayname', label: 'Anzeigename', type: 'text', group: 'general' },
|
||||
{ key: 'type', label: 'Typ', type: 'select', group: 'general' },
|
||||
{ key: 'email_1', label: 'E-Mail 1', type: 'text', group: 'general' },
|
||||
{ key: 'email_2', label: 'E-Mail 2', type: 'text', group: 'general' },
|
||||
{ key: 'phone_1', label: 'Telefon 1', type: 'text', group: 'general' },
|
||||
{ key: 'phone_2', label: 'Telefon 2', type: 'text', group: 'general' },
|
||||
{ key: 'website', label: 'Website', type: 'text', group: 'general' },
|
||||
{ key: 'tags', label: 'Tags', type: 'text', group: 'general' },
|
||||
|
||||
// Company
|
||||
{ key: 'name', label: 'Firmenname', type: 'text', group: 'company' },
|
||||
{ key: 'code', label: 'Kunden-Nr.', type: 'text', group: 'company' },
|
||||
{ key: 'accounting_code', label: 'Buchhaltungs-Code', type: 'text', group: 'company' },
|
||||
{ key: 'vat_code', label: 'USt-IdNr.', type: 'text', group: 'company' },
|
||||
{ key: 'fiscal_code', label: 'Steuernummer', type: 'text', group: 'company' },
|
||||
{ key: 'commerce_code', label: 'Handelsregister-Nr.', type: 'text', group: 'company' },
|
||||
{ key: 'bic', label: 'BIC', type: 'text', group: 'company' },
|
||||
{ key: 'bank_account', label: 'Bankkonto', type: 'text', group: 'company' },
|
||||
|
||||
// Person
|
||||
{ key: 'firstname', label: 'Vorname', type: 'text', group: 'person' },
|
||||
{ key: 'surname', label: 'Nachname', type: 'text', group: 'person' },
|
||||
{ key: 'suffix', label: 'Suffix', type: 'text', group: 'person' },
|
||||
{ key: 'gender', label: 'Geschlecht', type: 'select', group: 'person' },
|
||||
{ key: 'ext_name_line', label: 'Zusatzname', type: 'text', group: 'person' },
|
||||
|
||||
// Address
|
||||
{ key: 'mailing_street', label: 'Straße (Post)', type: 'text', group: 'address' },
|
||||
{ key: 'mailing_postalcode', label: 'PLZ (Post)', type: 'text', group: 'address' },
|
||||
{ key: 'mailing_city', label: 'Stadt (Post)', type: 'text', group: 'address' },
|
||||
{ key: 'mailing_state', label: 'Bundesland (Post)', type: 'text', group: 'address' },
|
||||
{ key: 'mailing_country', label: 'Land (Post)', type: 'text', group: 'address' },
|
||||
{ key: 'visit_city', label: 'Stadt (Besuch)', type: 'text', group: 'address' },
|
||||
{ key: 'visit_postalcode', label: 'PLZ (Besuch)', type: 'text', group: 'address' },
|
||||
{ key: 'invoice_city', label: 'Stadt (Rechnung)', type: 'text', group: 'address' },
|
||||
{ key: 'invoice_postalcode', label: 'PLZ (Rechnung)', type: 'text', group: 'address' },
|
||||
|
||||
// Notes
|
||||
{ key: 'projectnote', label: 'Projektnotiz', type: 'text', group: 'notes' },
|
||||
{ key: 'projectnote_title', label: 'Notiztitel', type: 'text', group: 'notes' },
|
||||
{ key: 'contact_warning', label: 'Warnung', type: 'text', group: 'notes' },
|
||||
|
||||
// Meta
|
||||
{ key: 'created_at', label: 'Erstellt am', type: 'date', group: 'meta' },
|
||||
{ key: 'updated_at', label: 'Geändert am', type: 'date', group: 'meta' },
|
||||
];
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SortCondition {
|
||||
id: string;
|
||||
field: string;
|
||||
order: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface SortState {
|
||||
conditions: SortCondition[];
|
||||
}
|
||||
|
||||
export const emptySortState: SortState = {
|
||||
conditions: [],
|
||||
};
|
||||
|
||||
// ─── Sort logic ───────────────────────────────────────────────────────────────
|
||||
|
||||
function getFieldValue(contact: UnifiedContact, field: string): any {
|
||||
return (contact as any)[field];
|
||||
}
|
||||
|
||||
function compareValues(a: any, b: any, fieldType: FieldType): number {
|
||||
// Handle nulls/undefined — sort them last regardless of order
|
||||
if (a == null && b == null) return 0;
|
||||
if (a == null) return 1;
|
||||
if (b == null) return -1;
|
||||
|
||||
if (fieldType === 'date') {
|
||||
const dateA = new Date(a).getTime();
|
||||
const dateB = new Date(b).getTime();
|
||||
return dateA - dateB;
|
||||
}
|
||||
|
||||
// Text and select — string comparison
|
||||
const strA = String(a).toLowerCase();
|
||||
const strB = String(b).toLowerCase();
|
||||
if (strA < strB) return -1;
|
||||
if (strA > strB) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function applySorting(contacts: UnifiedContact[], sortState: SortState): UnifiedContact[] {
|
||||
if (!sortState.conditions.length) return contacts;
|
||||
|
||||
const sorted = [...contacts];
|
||||
sorted.sort((a, b) => {
|
||||
for (const cond of sortState.conditions) {
|
||||
const def = SORT_FIELDS.find((f) => f.key === cond.field);
|
||||
if (!def) continue;
|
||||
const cmp = compareValues(getFieldValue(a, cond.field), getFieldValue(b, cond.field), def.type);
|
||||
if (cmp !== 0) {
|
||||
return cond.order === 'desc' ? -cmp : cmp;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// ─── Context-sensitive field ordering ──────────────────────────────────────────
|
||||
|
||||
function getOrderedFields(contactType?: 'company' | 'person' | undefined): SortFieldDef[] {
|
||||
if (contactType === 'company') {
|
||||
const order = ['general', 'company', 'address', 'notes', 'meta', 'person'];
|
||||
return [...SORT_FIELDS].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
}
|
||||
if (contactType === 'person') {
|
||||
const order = ['general', 'person', 'address', 'notes', 'meta', 'company'];
|
||||
return [...SORT_FIELDS].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
}
|
||||
const order = ['general', 'company', 'person', 'address', 'notes', 'meta'];
|
||||
return [...SORT_FIELDS].sort((a, b) => order.indexOf(a.group || 'general') - order.indexOf(b.group || 'general'));
|
||||
}
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface SortPanelProps {
|
||||
sortState: SortState;
|
||||
onSortChange: (sortState: SortState) => void;
|
||||
contactType?: 'company' | 'person' | undefined;
|
||||
}
|
||||
|
||||
let sortIdCounter = 0;
|
||||
function newSortId() {
|
||||
return `sort-${Date.now()}-${++sortIdCounter}`;
|
||||
}
|
||||
|
||||
export function SortPanel({ sortState, onSortChange, contactType }: SortPanelProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const btnRef = useRef<HTMLDivElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [panelPos, setPanelPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
if (btnRef.current?.contains(target) || panelRef.current?.contains(target)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!open && btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setPanelPos({ top: rect.bottom + 4, left: rect.left });
|
||||
}
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const orderedFields = useMemo(() => getOrderedFields(contactType), [contactType]);
|
||||
const activeCount = sortState.conditions.length;
|
||||
|
||||
const addCondition = () => {
|
||||
onSortChange({
|
||||
conditions: [...sortState.conditions, {
|
||||
id: newSortId(),
|
||||
field: orderedFields[0]?.key || 'displayname',
|
||||
order: 'asc' as const,
|
||||
}],
|
||||
});
|
||||
};
|
||||
|
||||
const removeCondition = (id: string) => {
|
||||
onSortChange({
|
||||
conditions: sortState.conditions.filter((c) => c.id !== id),
|
||||
});
|
||||
};
|
||||
|
||||
const updateCondition = (id: string, updates: Partial<SortCondition>) => {
|
||||
onSortChange({
|
||||
conditions: sortState.conditions.map((c) =>
|
||||
c.id === id ? { ...c, ...updates } : c
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
onSortChange({ ...emptySortState });
|
||||
};
|
||||
|
||||
const moveCondition = (id: string, direction: 'up' | 'down') => {
|
||||
const conds = [...sortState.conditions];
|
||||
const idx = conds.findIndex((c) => c.id === id);
|
||||
if (idx < 0) return;
|
||||
const newIdx = direction === 'up' ? idx - 1 : idx + 1;
|
||||
if (newIdx < 0 || newIdx >= conds.length) return;
|
||||
[conds[idx], conds[newIdx]] = [conds[newIdx], conds[idx]];
|
||||
onSortChange({ conditions: conds });
|
||||
};
|
||||
|
||||
// Group fields for dropdown
|
||||
const groupedFields = useMemo(() => {
|
||||
const groups: Record<string, SortFieldDef[]> = {};
|
||||
for (const f of orderedFields) {
|
||||
const g = f.group || 'general';
|
||||
if (!groups[g]) groups[g] = [];
|
||||
groups[g].push(f);
|
||||
}
|
||||
return groups;
|
||||
}, [orderedFields]);
|
||||
|
||||
const groupLabels: Record<string, string> = {
|
||||
general: 'Allgemein',
|
||||
company: 'Firma',
|
||||
person: 'Person',
|
||||
address: 'Adresse',
|
||||
notes: 'Notizen',
|
||||
meta: 'Metadaten',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={btnRef} className="relative flex-shrink-0">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
title="Sortieren"
|
||||
aria-label="Sortieren"
|
||||
className={`
|
||||
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
|
||||
transition-colors duration-100 cursor-pointer relative
|
||||
${open || activeCount > 0 ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'}
|
||||
`}
|
||||
>
|
||||
<ArrowDownAZ className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
{activeCount > 0 && (
|
||||
<span className="inline-flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-bold rounded-full bg-primary-600 text-white">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
<svg className={`w-3 h-3 transition-transform ${open ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="fixed bg-white border border-secondary-200 rounded-lg shadow-lg"
|
||||
style={{
|
||||
top: panelPos.top,
|
||||
left: panelPos.left,
|
||||
width: '420px',
|
||||
maxHeight: '70vh',
|
||||
overflowY: 'auto',
|
||||
zIndex: 3000,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-secondary-100">
|
||||
<span className="text-sm font-semibold text-secondary-800">Sortieren</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{activeCount > 0 && (
|
||||
<button
|
||||
onClick={clearAll}
|
||||
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
|
||||
>
|
||||
Alle löschen
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="p-1 rounded hover:bg-secondary-100 text-secondary-400 hover:text-secondary-600"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info text */}
|
||||
{activeCount === 0 && (
|
||||
<div className="px-4 py-3 border-b border-secondary-100">
|
||||
<div className="text-center py-4 text-xs text-secondary-400">
|
||||
Keine Sortierung aktiv. Datensätze können per Drag-and-Drop umsortiert werden.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active sort badges */}
|
||||
{activeCount > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 px-4 py-2 border-b border-secondary-100">
|
||||
{sortState.conditions.map((cond, idx) => {
|
||||
const def = SORT_FIELDS.find((f) => f.key === cond.field);
|
||||
return (
|
||||
<span
|
||||
key={cond.id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 text-[11px] rounded-full bg-primary-50 text-primary-700 border border-primary-200"
|
||||
>
|
||||
<span className="font-bold text-primary-500">{idx + 1}.</span>
|
||||
{def?.label}
|
||||
{cond.order === 'asc' ? <ArrowDownAZ className="w-3 h-3" /> : <ArrowUpZA className="w-3 h-3" />}
|
||||
<button onClick={() => removeCondition(cond.id)} className="hover:text-red-600">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sort rows */}
|
||||
<div className="px-4 py-3 space-y-2">
|
||||
{sortState.conditions.map((cond, idx) => {
|
||||
const def = SORT_FIELDS.find((f) => f.key === cond.field);
|
||||
return (
|
||||
<div key={cond.id} className="flex items-center gap-1.5">
|
||||
{/* Priority number + reorder */}
|
||||
<div className="flex flex-col items-center w-6 flex-shrink-0">
|
||||
<span className="text-[10px] font-bold text-primary-600">{idx + 1}</span>
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
onClick={() => moveCondition(cond.id, 'up')}
|
||||
disabled={idx === 0}
|
||||
className="text-secondary-300 hover:text-secondary-600 disabled:opacity-30 leading-none"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
|
||||
<polyline points="18 15 12 9 6 15" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => moveCondition(cond.id, 'down')}
|
||||
disabled={idx === sortState.conditions.length - 1}
|
||||
className="text-secondary-300 hover:text-secondary-600 disabled:opacity-30 leading-none"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Field dropdown */}
|
||||
<select
|
||||
value={cond.field}
|
||||
onChange={(e) => updateCondition(cond.id, { field: e.target.value })}
|
||||
className="flex-1 min-w-0 px-2 py-1.5 text-xs border border-secondary-200 rounded bg-white cursor-pointer focus:outline-none focus:border-primary-400"
|
||||
>
|
||||
{Object.entries(groupedFields).map(([group, fields]) => (
|
||||
<optgroup key={group} label={groupLabels[group] || group}>
|
||||
{fields.map((f) => (
|
||||
<option key={f.key} value={f.key}>{f.label}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Order toggle */}
|
||||
<button
|
||||
onClick={() => updateCondition(cond.id, { order: cond.order === 'asc' ? 'desc' : 'asc' })}
|
||||
className="
|
||||
inline-flex items-center gap-1 px-2 py-1.5 text-xs border border-secondary-200 rounded
|
||||
bg-white cursor-pointer hover:bg-secondary-50 transition-colors flex-shrink-0
|
||||
"
|
||||
title={cond.order === 'asc' ? 'Aufsteigend' : 'Absteigend'}
|
||||
>
|
||||
{cond.order === 'asc'
|
||||
? <ArrowDownAZ className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
: <ArrowUpZA className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
}
|
||||
<span className="text-[10px]">{cond.order === 'asc' ? 'A→Z' : 'Z→A'}</span>
|
||||
</button>
|
||||
|
||||
{/* Remove button */}
|
||||
<button
|
||||
onClick={() => removeCondition(cond.id)}
|
||||
className="p-1 rounded hover:bg-red-50 text-secondary-400 hover:text-red-600 transition-colors flex-shrink-0"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
Sortierung hinzufügen
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ 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 { FilterPanel, applyFilters, emptyFilterState, type FilterState } from '@/components/contacts/FilterPanel';
|
||||
import { SortPanel, applySorting, emptySortState, type SortState } from '@/components/contacts/SortPanel';
|
||||
import {
|
||||
useUnifiedContacts,
|
||||
useUnifiedContact,
|
||||
@@ -44,6 +45,7 @@ export function ContactsListPage() {
|
||||
const [savedFiltersOpen, setSavedFiltersOpen] = useState(false);
|
||||
const [selectedTags, setSelectedTags] = useState<Tag[]>([]);
|
||||
const [filterState, setFilterState] = useState<FilterState>(emptyFilterState);
|
||||
const [sortState, setSortState] = useState<SortState>(emptySortState);
|
||||
const openWindow = useWindowStore((s) => s.openWindow);
|
||||
|
||||
// Debounce search
|
||||
@@ -113,8 +115,10 @@ export function ContactsListPage() {
|
||||
}
|
||||
// Apply FilterPanel conditions
|
||||
result = applyFilters(result, filterState);
|
||||
// Apply SortPanel sorting
|
||||
result = applySorting(result, sortState);
|
||||
return result;
|
||||
}, [contacts, tagFilter, filterState]);
|
||||
}, [contacts, tagFilter, filterState, sortState]);
|
||||
|
||||
// Handle folder selection
|
||||
const handleSelectFilter = useCallback((filter: ContactFilter) => {
|
||||
@@ -232,26 +236,20 @@ export function ContactsListPage() {
|
||||
),
|
||||
onClick: () => {},
|
||||
},
|
||||
// Sort dropdown
|
||||
// SortPanel — SmartSuite-style multi-field sort
|
||||
{
|
||||
id: 'sort-by',
|
||||
id: 'sort-panel',
|
||||
plugin: 'contacts',
|
||||
label: t('common.sort', 'Sortieren'),
|
||||
type: 'dropdown' as const,
|
||||
label: 'Sortieren',
|
||||
type: 'custom' as const,
|
||||
group: 'sort',
|
||||
icon: <ArrowDownAZ className="w-3.5 h-3.5" strokeWidth={2} />,
|
||||
menuWidth: '200px',
|
||||
menuOptions: [
|
||||
...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); },
|
||||
})),
|
||||
{ value: 'sep-order', label: '', separator: true, onClick: () => {} },
|
||||
{ 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') },
|
||||
],
|
||||
customComponent: (
|
||||
<SortPanel
|
||||
sortState={sortState}
|
||||
onSortChange={setSortState}
|
||||
contactType={contactType}
|
||||
/>
|
||||
),
|
||||
onClick: () => {},
|
||||
},
|
||||
// View mode dropdown (list / table / cards)
|
||||
@@ -313,7 +311,7 @@ export function ContactsListPage() {
|
||||
];
|
||||
registerItems('contacts', items);
|
||||
return () => unregisterPlugin('contacts');
|
||||
}, [handleSearch, handleCreate, handleSelectFilter, handleSortChange, t, registerItems, unregisterPlugin, sortBy, sortOrder, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType]);
|
||||
}, [handleSearch, handleCreate, handleSelectFilter, t, registerItems, unregisterPlugin, selectedFilter, viewMode, sortOptions, viewModeOptions, filterState, contactType, sortState]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full" data-testid="contacts-list-page">
|
||||
|
||||
Reference in New Issue
Block a user