contacts toolbar: consolidate views and filters into dropdowns
This commit is contained in:
@@ -85,6 +85,71 @@ function ToolbarSearch({ item }: { item: ToolbarItem }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarDropdown({ item }: { item: ToolbarItem }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
const activeOption = item.menuOptions?.find((o) => o.active);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
title={item.label}
|
||||
aria-label={item.label}
|
||||
className={`
|
||||
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
|
||||
transition-colors duration-100 cursor-pointer
|
||||
${open || activeOption ? 'bg-primary-100 text-primary-700' : 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'}
|
||||
`}
|
||||
>
|
||||
{item.icon && <span className="w-4 h-4 flex-shrink-0">{item.icon}</span>}
|
||||
<span className="hidden sm:inline">{activeOption?.label || item.label}</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>
|
||||
{open && (
|
||||
<div
|
||||
className="absolute top-full left-0 mt-1 bg-white border border-secondary-200 rounded-lg shadow-lg z-50 py-1"
|
||||
style={{ minWidth: item.menuWidth || '180px' }}
|
||||
>
|
||||
{item.menuOptions?.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => {
|
||||
opt.onClick();
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`
|
||||
w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors
|
||||
${opt.active ? 'bg-primary-50 text-primary-700 font-medium' : 'text-secondary-700 hover:bg-secondary-100'}
|
||||
`}
|
||||
>
|
||||
{opt.icon && <span className="w-4 h-4 flex-shrink-0">{opt.icon}</span>}
|
||||
<span className="flex-1">{opt.label}</span>
|
||||
{opt.active && (
|
||||
<svg className="w-3.5 h-3.5 text-primary-600" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarSelect({ item }: { item: ToolbarItem }) {
|
||||
return (
|
||||
<select
|
||||
@@ -128,6 +193,7 @@ export function PluginToolbar() {
|
||||
{groups[groupName].map((item) => {
|
||||
if (item.type === 'search') return <ToolbarSearch key={item.id} item={item} />;
|
||||
if (item.type === 'select') return <ToolbarSelect key={item.id} item={item} />;
|
||||
if (item.type === 'dropdown') return <ToolbarDropdown key={item.id} item={item} />;
|
||||
return <ToolbarButton key={item.id} item={item} />;
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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 } from 'lucide-react';
|
||||
import { ArrowDownAZ, ArrowUpZA, Bookmark, ChevronLeft, ExternalLink, Filter, LayoutGrid, List, Plus, Printer, Table2 } from 'lucide-react';
|
||||
import {
|
||||
useUnifiedContacts,
|
||||
useUnifiedContact,
|
||||
@@ -233,68 +233,37 @@ export function ContactsListPage() {
|
||||
: <ArrowUpZA className="w-3.5 h-3.5" strokeWidth={2} />,
|
||||
onClick: () => handleSortChange(sortBy, sortOrder === 'asc' ? 'desc' : 'asc'),
|
||||
},
|
||||
// Type filter: All
|
||||
// Type filter dropdown (all / companies / persons)
|
||||
{
|
||||
id: 'filter-all',
|
||||
id: 'filter-type',
|
||||
plugin: 'contacts',
|
||||
label: t('common.all'),
|
||||
type: 'button' as const,
|
||||
label: t('common.filter', 'Filter'),
|
||||
type: 'dropdown' as const,
|
||||
group: 'filter',
|
||||
active: selectedFilter === 'all',
|
||||
onClick: () => handleSelectFilter('all'),
|
||||
icon: <Filter className="w-3.5 h-3.5" strokeWidth={2} />,
|
||||
menuWidth: '200px',
|
||||
menuOptions: [
|
||||
{ value: 'all', label: t('common.all'), 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') },
|
||||
],
|
||||
onClick: () => {},
|
||||
},
|
||||
// Type filter: Companies
|
||||
// View mode dropdown (list / table / cards)
|
||||
{
|
||||
id: 'filter-company',
|
||||
id: 'view-mode',
|
||||
plugin: 'contacts',
|
||||
label: t('contacts.companies'),
|
||||
type: 'button' as const,
|
||||
group: 'filter',
|
||||
active: selectedFilter === 'company',
|
||||
onClick: () => handleSelectFilter('company'),
|
||||
},
|
||||
// Type filter: Persons
|
||||
{
|
||||
id: 'filter-person',
|
||||
plugin: 'contacts',
|
||||
label: t('contacts.persons'),
|
||||
type: 'button' as const,
|
||||
group: 'filter',
|
||||
active: selectedFilter === 'person',
|
||||
onClick: () => handleSelectFilter('person'),
|
||||
},
|
||||
// View mode: List
|
||||
{
|
||||
id: 'view-list',
|
||||
plugin: 'contacts',
|
||||
label: t('contacts.viewList'),
|
||||
type: 'button' as const,
|
||||
label: t('common.view', 'Ansicht'),
|
||||
type: 'dropdown' as const,
|
||||
group: 'view',
|
||||
active: viewMode === 'list',
|
||||
icon: <List className="w-3.5 h-3.5" strokeWidth={2} />,
|
||||
onClick: () => setViewMode('list'),
|
||||
},
|
||||
// View mode: Table
|
||||
{
|
||||
id: 'view-table',
|
||||
plugin: 'contacts',
|
||||
label: t('contacts.viewTable'),
|
||||
type: 'button' as const,
|
||||
group: 'view',
|
||||
active: viewMode === 'table',
|
||||
icon: <LayoutGrid className="w-3.5 h-3.5" strokeWidth={2} />,
|
||||
onClick: () => setViewMode('table'),
|
||||
},
|
||||
// View mode: Cards
|
||||
{
|
||||
id: 'view-cards',
|
||||
plugin: 'contacts',
|
||||
label: t('contacts.viewCards'),
|
||||
type: 'button' as const,
|
||||
group: 'view',
|
||||
active: viewMode === 'cards',
|
||||
icon: <LayoutGrid className="w-3.5 h-3.5" strokeWidth={2} />,
|
||||
onClick: () => setViewMode('cards'),
|
||||
menuWidth: '180px',
|
||||
menuOptions: [
|
||||
{ value: 'list', label: t('contacts.viewList'), icon: <List className="w-3.5 h-3.5" strokeWidth={2} />, active: viewMode === 'list', onClick: () => setViewMode('list') },
|
||||
{ value: 'table', label: t('contacts.viewTable'), icon: <Table2 className="w-3.5 h-3.5" strokeWidth={2} />, active: viewMode === 'table', onClick: () => setViewMode('table') },
|
||||
{ value: 'cards', label: t('contacts.viewCards'), icon: <LayoutGrid className="w-3.5 h-3.5" strokeWidth={2} />, active: viewMode === 'cards', onClick: () => setViewMode('cards') },
|
||||
],
|
||||
onClick: () => {},
|
||||
},
|
||||
// Saved filters
|
||||
{
|
||||
|
||||
@@ -9,12 +9,14 @@ export interface ToolbarItem {
|
||||
group?: string;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
type?: 'button' | 'search' | 'select';
|
||||
type?: 'button' | 'search' | 'select' | 'dropdown';
|
||||
searchPlaceholder?: string;
|
||||
onSearch?: (query: string) => void;
|
||||
selectOptions?: { value: string; label: string }[];
|
||||
selectValue?: string;
|
||||
onSelect?: (value: string) => void;
|
||||
menuOptions?: { value: string; label: string; icon?: React.ReactNode; active?: boolean; onClick: () => void }[];
|
||||
menuWidth?: string;
|
||||
}
|
||||
|
||||
interface PluginToolbarState {
|
||||
|
||||
Reference in New Issue
Block a user