import { useCallback, useMemo, useState } from 'react'; import { RoleSelect } from '@renderer/components/team/RoleSelect'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea'; import { CUSTOM_ROLE, NO_ROLE } from '@renderer/constants/teamRoles'; import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence'; import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer'; import { Loader2 } from 'lucide-react'; import type { MentionSuggestion } from '@renderer/types/mention'; import type { ResolvedTeamMember } from '@shared/types'; const NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/; interface AddMemberDialogProps { open: boolean; teamName: string; existingNames: string[]; onClose: () => void; onAdd: (name: string, role?: string, workflow?: string) => void; adding?: boolean; /** Project path for @file mentions in workflow field. */ projectPath?: string | null; /** Existing team members for @mention suggestions. */ existingMembers?: ResolvedTeamMember[]; } export const AddMemberDialog = ({ open, teamName, existingNames, onClose, onAdd, adding, projectPath, existingMembers = [], }: AddMemberDialogProps): React.JSX.Element => { const [name, setName] = useState(''); const [roleSelect, setRoleSelect] = useState(NO_ROLE); const [customRole, setCustomRole] = useState(''); const [error, setError] = useState(null); const draftKey = `addMember:${teamName}:workflow`; const workflowDraft = useDraftPersistence({ key: draftKey, enabled: open, }); // Pre-warm file list cache for @file mentions useFileListCacheWarmer(open && projectPath ? projectPath : null); const mentionSuggestions = useMemo( () => existingMembers .filter((m) => !m.removedAt) .map((m) => ({ id: m.name, name: m.name, subtitle: m.role ?? undefined, color: m.color, })), [existingMembers] ); const effectiveRole = roleSelect === CUSTOM_ROLE ? customRole.trim() : roleSelect === NO_ROLE ? undefined : roleSelect; const validate = (): string | null => { const trimmed = name.trim().toLowerCase(); if (!trimmed) return 'Name is required'; if (trimmed.length < 2) return 'Name must be at least 2 characters'; if (trimmed.length > 30) return 'Name must be at most 30 characters'; if (!NAME_REGEX.test(trimmed)) return 'Name must be lowercase alphanumeric with hyphens (e.g. alice, dev-1)'; if (trimmed === 'user') return 'Name "user" is reserved'; if (trimmed === 'team-lead') return 'Name "team-lead" is reserved'; if (existingNames.some((n) => n.toLowerCase() === trimmed)) return 'Name is already taken'; return null; }; const handleSubmit = (): void => { const err = validate(); if (err) { setError(err); return; } setError(null); const wf = workflowDraft.value.trim() || undefined; onAdd(name.trim().toLowerCase(), effectiveRole, wf); // Reset form fields after successful submission setName(''); setRoleSelect(NO_ROLE); setCustomRole(''); workflowDraft.clearDraft(); }; const handleOpenChange = (nextOpen: boolean): void => { if (!nextOpen) { setName(''); setRoleSelect(NO_ROLE); setCustomRole(''); workflowDraft.setValue(''); workflowDraft.clearDraft(); setError(null); onClose(); } }; const handleWorkflowChange = useCallback( (v: string) => { workflowDraft.setValue(v); }, [workflowDraft] ); return ( Add Member Add a new member to {teamName}
{ e.preventDefault(); handleSubmit(); }} >
{ setName(e.target.value); setError(null); }} autoFocus /> {error &&

{error}

}
Saved ) : null } />
); };