From 78738f5aa946a9496dd0a8c7cc7d58f7216a0bd8 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Tue, 28 Jul 2026 12:22:30 +0200 Subject: [PATCH] feat: folder multi-select mode with checkboxes, shows contacts from multiple folders --- .../components/contacts/ContactFolderTree.tsx | 133 +++++++++++++++--- frontend/src/pages/ContactsList.tsx | 12 +- 2 files changed, 121 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/contacts/ContactFolderTree.tsx b/frontend/src/components/contacts/ContactFolderTree.tsx index 76b0d2d..6104e92 100644 --- a/frontend/src/components/contacts/ContactFolderTree.tsx +++ b/frontend/src/components/contacts/ContactFolderTree.tsx @@ -34,6 +34,8 @@ export interface ContactFolderTreeProps { tags: string[]; loading?: boolean; contacts?: { id: string; displayname: string; type: string; folder_id?: string | null }[]; + multiSelectFolders?: string[]; + onMultiSelectChange?: (folderIds: string[]) => void; } // Icons @@ -137,6 +139,9 @@ function FolderTreeItem({ onDragOver, onDragLeave, onDrop, + multiSelectMode, + multiSelectedFolders, + onToggleMultiSelect, }: { node: ContactFolderTreeNode; depth: number; @@ -147,10 +152,14 @@ function FolderTreeItem({ onDragOver: (e: React.DragEvent, folderId: string) => void; onDragLeave: (folderId: string) => void; onDrop: (e: React.DragEvent, folderId: string) => void; + multiSelectMode?: boolean; + multiSelectedFolders?: string[]; + onToggleMultiSelect?: (folderId: string) => void; }) { const [expanded, setExpanded] = useState(true); const folderKey = `folder:${node.id}` as ContactFilter; const isActive = selectedFilter === folderKey; + const isChecked = multiSelectedFolders?.includes(node.id) ?? false; return (
@@ -174,29 +183,45 @@ function FolderTreeItem({ : 'text-secondary-700 hover:bg-secondary-100', )} onClick={() => { - setExpanded(!expanded); - onSelectFolder(node.id); + if (multiSelectMode && onToggleMultiSelect) { + onToggleMultiSelect(node.id); + } else { + setExpanded(!expanded); + onSelectFolder(node.id); + } }} > - {chevron(expanded)} + {multiSelectMode ? ( + onToggleMultiSelect?.(node.id)} + onClick={(e) => e.stopPropagation()} + className="w-3.5 h-3.5 rounded border-secondary-300 text-primary-600 focus:ring-primary-500 flex-shrink-0 cursor-pointer" + /> + ) : ( + chevron(expanded) + )} {icon(ICONS.folder, 'w-4 h-4 text-secondary-400')} {node.name} {node.contact_count > 0 && ( {node.contact_count} )}
- + {!multiSelectMode && ( + + )} - {expanded && node.children.map((child) => ( + {expanded && !multiSelectMode && node.children.map((child) => ( + ))} + {multiSelectMode && node.children.map((child) => ( + ))} @@ -222,12 +267,15 @@ export function ContactFolderTree({ tags, loading, contacts = [], + multiSelectFolders = [], + onMultiSelectChange, }: ContactFolderTreeProps) { const { t } = useTranslation(); const [tagsOpen, setTagsOpen] = useState(true); const [dropdown, setDropdown] = useState(null); const [colorPicker, setColorPicker] = useState<{ folderId: string; color: string } | null>(null); const [dragOverFolderId, setDragOverFolderId] = useState(null); + const [multiSelectMode, setMultiSelectMode] = useState(false); const { data: folders, isLoading: foldersLoading } = useContactFolders(); const createFolderMut = useCreateContactFolder(); @@ -344,6 +392,22 @@ export function ContactFolderTree({ onSelect(`folder:${folderId}` as ContactFilter); }, [onSelect]); + const handleToggleMultiSelect = useCallback((folderId: string) => { + if (!onMultiSelectChange) return; + if (multiSelectFolders.includes(folderId)) { + onMultiSelectChange(multiSelectFolders.filter((id) => id !== folderId)); + } else { + onMultiSelectChange([...multiSelectFolders, folderId]); + } + }, [multiSelectFolders, onMultiSelectChange]); + + const handleToggleMultiSelectMode = () => { + setMultiSelectMode(!multiSelectMode); + if (multiSelectMode && onMultiSelectChange) { + onMultiSelectChange([]); + } + }; + return (