import React from 'react'; import { cn } from '@/lib/utils'; import type { SessionFolder } from '@/stores/useSessionFoldersStore'; import { useI18n } from '@/lib/i18n'; import { Icon } from "@/components/icon/Icon"; interface SessionFolderItemProps { folder: SessionFolder; sessions: TSessionNode[]; /** Sub-folders that belong directly to this folder */ subFolderItems?: React.ReactNode; isCollapsed: boolean; onToggle: () => void; onRename: (name: string) => void; onDelete: () => void; renderSessionNode: ( node: TSessionNode, depth?: number, groupDir?: string | null, projectId?: string | null, archivedBucket?: boolean, ) => React.ReactNode; groupDirectory?: string | null; projectId?: string | null; mobileVariant?: boolean; alwaysShowActions?: boolean; isRenaming?: boolean; renameDraft?: string; onRenameDraftChange?: (value: string) => void; onRenameSave?: () => void; onRenameCancel?: () => void; /** Ref callback from useDroppable – attach to folder header to make it a drop zone */ droppableRef?: (node: HTMLElement | null) => void; /** Whether a draggable session is currently hovering over this folder */ isDropTarget?: boolean; /** Create a new session scoped to this folder */ onNewSession?: () => void; /** Create a new sub-folder inside this folder */ onNewSubFolder?: () => void; /** Visual indent depth (0 = root folder, 1 = sub-folder) */ depth?: number; /** Hide folder action buttons (rename/delete/new) */ hideActions?: boolean; /** Whether folder belongs to archived section */ archivedBucket?: boolean; } const SessionFolderItemBase = ({ folder, sessions, subFolderItems, isCollapsed, onToggle, onRename, onDelete, renderSessionNode, groupDirectory, projectId, mobileVariant = false, alwaysShowActions = mobileVariant, isRenaming = false, renameDraft = '', onRenameDraftChange, onRenameSave, onRenameCancel, droppableRef, isDropTarget = false, onNewSession, onNewSubFolder, depth = 0, hideActions = false, archivedBucket = false, }: SessionFolderItemProps) => { const { t } = useI18n(); const [localRenaming, setLocalRenaming] = React.useState(false); const [localDraft, setLocalDraft] = React.useState(''); const inputRef = React.useRef(null); const renaming = isRenaming || localRenaming; const draft = isRenaming ? renameDraft : localDraft; const handleStartRename = React.useCallback(() => { setLocalDraft(folder.name); setLocalRenaming(true); }, [folder.name]); const handleSaveRename = React.useCallback(() => { const trimmed = draft.trim(); if (trimmed && trimmed !== folder.name) { onRename(trimmed); } if (isRenaming && onRenameSave) { onRenameSave(); } setLocalRenaming(false); setLocalDraft(''); }, [draft, folder.name, isRenaming, onRename, onRenameSave]); const handleCancelRename = React.useCallback(() => { if (isRenaming && onRenameCancel) { onRenameCancel(); } setLocalRenaming(false); setLocalDraft(''); }, [isRenaming, onRenameCancel]); const handleDraftChange = React.useCallback( (value: string) => { if (isRenaming && onRenameDraftChange) { onRenameDraftChange(value); } else { setLocalDraft(value); } }, [isRenaming, onRenameDraftChange], ); // Auto-focus rename when externally triggered React.useEffect(() => { if (!isRenaming) return; const focusInput = () => { const input = inputRef.current; if (!input) return; input.focus(); input.select(); }; const frameId = requestAnimationFrame(focusInput); const timeoutId = window.setTimeout(focusInput, 0); return () => { cancelAnimationFrame(frameId); window.clearTimeout(timeoutId); }; }, [isRenaming]); const folderIconName = isCollapsed ? 'folder' : 'folder-open'; const isSubFolder = depth > 0; return (
{/* Folder header – also acts as a drop zone when droppableRef is provided */}
{ if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onToggle(); } } } aria-label={isCollapsed ? t('sessions.sidebar.folderItem.expandAria', { folderName: folder.name }) : t('sessions.sidebar.folderItem.collapseAria', { folderName: folder.name })} >
{renaming ? (
event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onSubmit={(event) => { event.preventDefault(); handleSaveRename(); }} > handleDraftChange(event.target.value)} className="flex-1 min-w-0 bg-transparent typography-ui-label outline-none placeholder:text-muted-foreground" autoFocus placeholder={t('sessions.sidebar.folderItem.namePlaceholder')} onClick={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} onKeyDown={(event) => { if (event.key === 'Escape') { event.stopPropagation(); handleCancelRename(); return; } if (event.key === ' ' || event.key === 'Enter') { event.stopPropagation(); } }} />
) : (
{folder.name} • {sessions.length} {isCollapsed ? ( ) : ( )}
)}
{/* Action buttons */} {!renaming && (!hideActions || archivedBucket) ? (
{!archivedBucket && onNewSession ? ( ) : null} {/* Only allow sub-folders at depth 0 (one level deep max) */} {!archivedBucket && onNewSubFolder && depth === 0 ? ( ) : null} {!archivedBucket ? ( ) : null}
) : null}
{/* Folder body */} {!isCollapsed ? (
{/* Sub-folders first */} {subFolderItems} {/* Then sessions */} {sessions.length > 0 ? ( sessions.map((node) => renderSessionNode(node, 0, groupDirectory ?? null, projectId ?? null, archivedBucket), ) ) : !subFolderItems ? (
{t('sessions.sidebar.folderItem.emptyFolder')}
) : null}
) : null}
); }; export const SessionFolderItem = React.memo(SessionFolderItemBase) as ( props: SessionFolderItemProps, ) => React.ReactElement;