import React, { useState, useEffect, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { useContactFolders, useCreateContactFolder, useUpdateContactFolder, useDeleteContactFolder, useMoveContactToFolder, } from '@/api/hooks'; import { buildFolderTree, type ContactFolderTreeNode, type ContactFolder } from '@/api/contactFolders'; import { ChevronRight, Folder, MoreVertical, Palette, Pencil, Pin, Plus, Shield, Tag, Trash2, Users } from 'lucide-react'; import { FolderPermissionDialog } from './FolderPermissionDialog'; import { useToast } from '@/components/ui/Toast'; export type ContactFilter = 'all' | 'company' | 'person' | `tag:${string}` | `folder:${string}`; /** * Returns true if `descendantId` is a descendant of (or equal to) `ancestorId` * in the given flat folder list. Used to prevent circular folder moves. */ function isDescendantOrSelf(folders: ContactFolder[], descendantId: string, ancestorId: string): boolean { if (descendantId === ancestorId) return true; let current = folders.find((f) => f.id === descendantId); while (current && current.parent_id) { if (current.parent_id === ancestorId) return true; current = folders.find((f) => f.id === current!.parent_id!); } return false; } export interface ContactFolderTreeProps { selectedFilter: ContactFilter; onSelect: (filter: ContactFilter) => void; tags: string[]; loading?: boolean; contacts?: { id: string; displayname: string; type: string; folder_id?: string | null }[]; multiSelectFolders?: string[]; onMultiSelectChange?: (folderIds: string[]) => void; } // Icons const icon = (IconComp: React.ElementType, cls = 'w-4 h-4') => (