import React, { useCallback, useMemo, useState } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; import { Combobox } from '@renderer/components/ui/combobox'; import { Input } from '@renderer/components/ui/input'; import { CUSTOM_ROLE, FORBIDDEN_ROLES, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles'; import { Blocks, BookOpen, Bug, Check, Code2, FileText, Pencil, Shield, Zap } from 'lucide-react'; import type { ComboboxOption } from '@renderer/components/ui/combobox'; import type { LucideIcon } from 'lucide-react'; /** Icon mapping for preset roles. */ const ROLE_ICONS: Record = { architect: Blocks, reviewer: BookOpen, developer: Code2, qa: Bug, researcher: BookOpen, docs: FileText, auditor: Shield, optimizer: Zap, }; const CUSTOM_ICON = Pencil; interface RoleSelectProps { /** Current role selection value (preset role name, CUSTOM_ROLE, or NO_ROLE). */ value: string; /** Called when the user picks a preset role, NO_ROLE, or CUSTOM_ROLE. */ onValueChange: (value: string) => void; /** Current custom role text (only relevant when value === CUSTOM_ROLE). */ customRole?: string; /** Called when the user types a custom role. */ onCustomRoleChange?: (customRole: string) => void; /** Trigger height class, e.g. "h-7" or "h-8". */ triggerClassName?: string; /** Custom input height class. */ inputClassName?: string; /** Show validation error for custom role. */ customRoleError?: string | null; /** Validate custom role on change and return error or null. */ onCustomRoleValidate?: (role: string) => string | null; disabled?: boolean; } // eslint-disable-next-line sonarjs/function-return-type -- option renderer returns mixed node structure const renderRoleOption = (option: ComboboxOption, isSelected: boolean): React.ReactNode => { const Icon = option.value === CUSTOM_ROLE ? CUSTOM_ICON : option.value === NO_ROLE ? null : (ROLE_ICONS[option.value] ?? null); return ( <> {isSelected ? ( ) : Icon ? ( ) : null} {option.label} ); }; export const RoleSelect = ({ value, onValueChange, customRole = '', onCustomRoleChange, triggerClassName, inputClassName, customRoleError: externalError, onCustomRoleValidate, disabled, }: RoleSelectProps): React.JSX.Element => { const { t } = useAppTranslation('team'); const roleOptions = useMemo( () => [ { value: NO_ROLE, label: t('roleSelect.noRole') }, ...PRESET_ROLES.map((role) => ({ value: role, label: role, })), { value: CUSTOM_ROLE, label: t('roleSelect.customRole') }, ], [t] ); const [internalError, setInternalError] = useState(null); const error = externalError ?? internalError; const handleValueChange = useCallback( (newValue: string) => { onValueChange(newValue); if (newValue !== CUSTOM_ROLE) { setInternalError(null); } }, [onValueChange] ); const handleCustomChange = useCallback( (e: React.ChangeEvent) => { const val = e.target.value; onCustomRoleChange?.(val); if (onCustomRoleValidate) { setInternalError(onCustomRoleValidate(val)); } else if (FORBIDDEN_ROLES.has(val.trim().toLowerCase())) { setInternalError(t('roleSelect.reservedRole')); } else { setInternalError(null); } }, [onCustomRoleChange, onCustomRoleValidate, t] ); const selectedLabel = useMemo(() => { const opt = roleOptions.find((o) => o.value === value); return opt?.label; }, [value]); const renderTriggerLabel = useCallback((option: ComboboxOption) => { const Icon = option.value === CUSTOM_ROLE ? CUSTOM_ICON : option.value === NO_ROLE ? null : (ROLE_ICONS[option.value] ?? null); return ( {Icon ? : null} {option.label} ); }, []); return (
{value === CUSTOM_ROLE && onCustomRoleChange ? (
{error ? {error} : null}
) : null}
); };