feat: column visibility, bulk actions, custom sort drag-drop, custom fields in filter/sort/group, mobile optimization
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user