feat: column visibility, bulk actions, custom sort drag-drop, custom fields in filter/sort/group, mobile optimization

This commit is contained in:
Agent Zero
2026-07-28 23:14:15 +02:00
parent 9681827395
commit 784a771039
5 changed files with 601 additions and 64 deletions
+67 -3
View File
@@ -29,8 +29,12 @@ import { useSavedFilters as useSavedFiltersApi, useCreateSavedFilter, useDeleteS
import {
useUnifiedContacts,
useUnifiedContact,
useDeleteUnifiedContact,
useUpdateUnifiedContact,
type UnifiedContact,
} from '@/api/hooks';
import { useContactFolders } from '@/api/contacts';
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
const PAGE_SIZE = 25;
@@ -56,8 +60,31 @@ export function ContactsListPage() {
const [multiSelectFolders, setMultiSelectFolders] = useState<string[]>([]);
const [activeViewId, setActiveViewId] = useState<string | null>(null);
const [saveViewDialogOpen, setSaveViewDialogOpen] = useState(false);
const [selectedContactIds, setSelectedContactIds] = useState<Set<string>>(new Set());
const openWindow = useWindowStore((s) => s.openWindow);
// Bulk action mutations
const deleteContactMut = useDeleteUnifiedContact();
const updateContactMut = useUpdateUnifiedContact();
const { data: folders } = useContactFolders();
const { data: customFieldDefs } = useCustomFieldDefinitions('contact');
// Build custom field defs for filter/sort/group functions
const customDefsForFunctions = useMemo(() => {
if (!customFieldDefs || customFieldDefs.items.length === 0) return undefined;
return customFieldDefs.items
.filter((def) => def.is_active)
.map((def) => ({
key: `custom.${def.name}`,
label: def.label || def.name,
type: (def.field_type === 'number' ? 'number' : def.field_type === 'date' ? 'date' : (def.field_type === 'select' || def.field_type === 'multiselect') ? 'select' : 'text') as any,
group: 'custom' as any,
options: (def.field_type === 'select' || def.field_type === 'multiselect') && def.options
? def.options.map((opt: string) => ({ value: opt, label: opt }))
: undefined,
}));
}, [customFieldDefs]);
// Saved views from API (durable, per-user)
const { data: apiSavedViews } = useSavedViews('contacts');
const createViewMut = useCreateSavedView();
@@ -144,14 +171,14 @@ export function ContactsListPage() {
});
}
// Apply FilterPanel conditions
result = applyFilters(result, filterState);
result = applyFilters(result, filterState, customDefsForFunctions);
// Apply SortPanel sorting
result = applySorting(result, sortState);
result = applySorting(result, sortState, customDefsForFunctions);
return result;
}, [contacts, tagFilter, filterState, sortState, multiSelectFolders]);
// Apply GroupPanel grouping
const groupedContacts = useMemo(() => applyGrouping(filteredContacts, groupState), [filteredContacts, groupState]);
const groupedContacts = useMemo(() => applyGrouping(filteredContacts, groupState, customDefsForFunctions), [filteredContacts, groupState, customDefsForFunctions]);
// Handle folder selection
const handleSelectFilter = useCallback((filter: ContactFilter) => {
@@ -221,6 +248,33 @@ export function ContactsListPage() {
setActiveView('list');
}, []);
// Bulk delete handler (Feature 2)
const handleBulkDelete = useCallback((ids: string[]) => {
ids.forEach((id) => {
deleteContactMut.mutate({ id });
});
setSelectedContactIds(new Set());
}, [deleteContactMut]);
// Bulk assign folder handler (Feature 2)
const handleBulkAssignFolder = useCallback((ids: string[], folderId: string) => {
ids.forEach((id) => {
updateContactMut.mutate({ id, data: { folder_id: folderId } });
});
setSelectedContactIds(new Set());
}, [updateContactMut]);
// Bulk add tags handler (Feature 2)
const handleBulkAddTags = useCallback((ids: string[], tags: string[]) => {
ids.forEach((id) => {
const contact = contacts.find((c) => c.id === id);
const existingTags = contact?.tags ? contact.tags.split(',').map((t) => t.trim()).filter(Boolean) : [];
const newTags = [...new Set([...existingTags, ...tags])];
updateContactMut.mutate({ id, data: { tags: newTags.join(', ') } });
});
setSelectedContactIds(new Set());
}, [updateContactMut, contacts]);
// Save current view configuration as a custom view — opens dialog
const handleSaveView = useCallback(() => {
setSaveViewDialogOpen(true);
@@ -585,6 +639,11 @@ export function ContactsListPage() {
onSortStateChange={setSortState}
groupState={groupState}
groupedContacts={groupedContacts}
selectedContactIds={selectedContactIds}
onSelectionChange={setSelectedContactIds}
onBulkDelete={handleBulkDelete}
onBulkAssignFolder={handleBulkAssignFolder}
onBulkAddTags={handleBulkAddTags}
/>
</div>
</ResizablePanel>
@@ -662,6 +721,11 @@ export function ContactsListPage() {
onSortStateChange={setSortState}
groupState={groupState}
groupedContacts={groupedContacts}
selectedContactIds={selectedContactIds}
onSelectionChange={setSelectedContactIds}
onBulkDelete={handleBulkDelete}
onBulkAssignFolder={handleBulkAssignFolder}
onBulkAddTags={handleBulkAddTags}
/>
</div>
)}