2026-02-21 06:09:55 +07:00
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
|
2026-04-26 14:03:39 +03:00
|
|
|
|
import { useI18n } from '@/lib/i18n';
|
2026-05-13 13:26:15 +03:00
|
|
|
|
import { Icon } from "@/components/icon/Icon";
|
2026-02-21 06:09:55 +07:00
|
|
|
|
|
|
|
|
|
|
interface SessionFolderItemProps<TSessionNode> {
|
|
|
|
|
|
folder: SessionFolder;
|
|
|
|
|
|
sessions: TSessionNode[];
|
2026-02-23 03:56:38 +07:00
|
|
|
|
/** Sub-folders that belong directly to this folder */
|
|
|
|
|
|
subFolderItems?: React.ReactNode;
|
2026-02-21 06:09:55 +07:00
|
|
|
|
isCollapsed: boolean;
|
|
|
|
|
|
onToggle: () => void;
|
|
|
|
|
|
onRename: (name: string) => void;
|
|
|
|
|
|
onDelete: () => void;
|
|
|
|
|
|
renderSessionNode: (
|
|
|
|
|
|
node: TSessionNode,
|
|
|
|
|
|
depth?: number,
|
|
|
|
|
|
groupDir?: string | null,
|
|
|
|
|
|
projectId?: string | null,
|
2026-03-06 17:22:59 +02:00
|
|
|
|
archivedBucket?: boolean,
|
2026-02-21 06:09:55 +07:00
|
|
|
|
) => React.ReactNode;
|
|
|
|
|
|
groupDirectory?: string | null;
|
|
|
|
|
|
projectId?: string | null;
|
|
|
|
|
|
mobileVariant?: boolean;
|
2026-05-03 17:19:55 +03:00
|
|
|
|
alwaysShowActions?: boolean;
|
2026-02-21 06:09:55 +07:00
|
|
|
|
isRenaming?: boolean;
|
|
|
|
|
|
renameDraft?: string;
|
|
|
|
|
|
onRenameDraftChange?: (value: string) => void;
|
|
|
|
|
|
onRenameSave?: () => void;
|
|
|
|
|
|
onRenameCancel?: () => void;
|
2026-02-23 03:56:38 +07:00
|
|
|
|
/** 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;
|
2026-03-06 17:22:59 +02:00
|
|
|
|
/** Hide folder action buttons (rename/delete/new) */
|
|
|
|
|
|
hideActions?: boolean;
|
|
|
|
|
|
/** Whether folder belongs to archived section */
|
|
|
|
|
|
archivedBucket?: boolean;
|
2026-02-21 06:09:55 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SessionFolderItemBase = <TSessionNode,>({
|
|
|
|
|
|
folder,
|
|
|
|
|
|
sessions,
|
2026-02-23 03:56:38 +07:00
|
|
|
|
subFolderItems,
|
2026-02-21 06:09:55 +07:00
|
|
|
|
isCollapsed,
|
|
|
|
|
|
onToggle,
|
|
|
|
|
|
onRename,
|
|
|
|
|
|
onDelete,
|
|
|
|
|
|
renderSessionNode,
|
|
|
|
|
|
groupDirectory,
|
|
|
|
|
|
projectId,
|
|
|
|
|
|
mobileVariant = false,
|
2026-05-03 17:19:55 +03:00
|
|
|
|
alwaysShowActions = mobileVariant,
|
2026-02-21 06:09:55 +07:00
|
|
|
|
isRenaming = false,
|
|
|
|
|
|
renameDraft = '',
|
|
|
|
|
|
onRenameDraftChange,
|
|
|
|
|
|
onRenameSave,
|
|
|
|
|
|
onRenameCancel,
|
2026-02-23 03:56:38 +07:00
|
|
|
|
droppableRef,
|
|
|
|
|
|
isDropTarget = false,
|
|
|
|
|
|
onNewSession,
|
|
|
|
|
|
onNewSubFolder,
|
|
|
|
|
|
depth = 0,
|
2026-03-06 17:22:59 +02:00
|
|
|
|
hideActions = false,
|
|
|
|
|
|
archivedBucket = false,
|
2026-02-21 06:09:55 +07:00
|
|
|
|
}: SessionFolderItemProps<TSessionNode>) => {
|
2026-04-26 14:03:39 +03:00
|
|
|
|
const { t } = useI18n();
|
2026-02-21 06:09:55 +07:00
|
|
|
|
const [localRenaming, setLocalRenaming] = React.useState(false);
|
|
|
|
|
|
const [localDraft, setLocalDraft] = React.useState('');
|
|
|
|
|
|
const inputRef = React.useRef<HTMLInputElement | null>(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],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-23 03:56:38 +07:00
|
|
|
|
// Auto-focus rename when externally triggered
|
2026-02-21 06:09:55 +07:00
|
|
|
|
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]);
|
|
|
|
|
|
|
2026-05-13 13:26:15 +03:00
|
|
|
|
const folderIconName = isCollapsed ? 'folder' : 'folder-open';
|
2026-02-23 03:56:38 +07:00
|
|
|
|
const isSubFolder = depth > 0;
|
2026-02-21 06:09:55 +07:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-23 03:56:38 +07:00
|
|
|
|
<div className={cn('oc-folder', isSubFolder && 'ml-3')}>
|
|
|
|
|
|
{/* Folder header – also acts as a drop zone when droppableRef is provided */}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
<div
|
2026-02-23 03:56:38 +07:00
|
|
|
|
ref={droppableRef}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
className={cn(
|
2026-03-20 01:01:03 +02:00
|
|
|
|
'group/folder relative flex items-center justify-between gap-1.5 py-1 min-w-0 rounded-md',
|
|
|
|
|
|
'cursor-pointer',
|
2026-02-23 03:56:38 +07:00
|
|
|
|
isDropTarget && 'bg-primary/10 ring-1 ring-inset ring-primary/30',
|
2026-02-21 06:09:55 +07:00
|
|
|
|
)}
|
|
|
|
|
|
onClick={renaming ? undefined : onToggle}
|
|
|
|
|
|
role={renaming ? undefined : 'button'}
|
|
|
|
|
|
tabIndex={renaming ? undefined : 0}
|
|
|
|
|
|
onKeyDown={
|
|
|
|
|
|
renaming
|
|
|
|
|
|
? undefined
|
|
|
|
|
|
: (event) => {
|
|
|
|
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
onToggle();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-26 14:03:39 +03:00
|
|
|
|
aria-label={isCollapsed
|
|
|
|
|
|
? t('sessions.sidebar.folderItem.expandAria', { folderName: folder.name })
|
|
|
|
|
|
: t('sessions.sidebar.folderItem.collapseAria', { folderName: folder.name })}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
>
|
2026-03-20 01:01:03 +02:00
|
|
|
|
<div className={cn(
|
|
|
|
|
|
'min-w-0 flex items-center gap-1.5 pl-1.5 flex-1 transition-[padding]',
|
|
|
|
|
|
archivedBucket
|
2026-05-03 17:19:55 +03:00
|
|
|
|
? (alwaysShowActions ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7')
|
2026-03-20 01:01:03 +02:00
|
|
|
|
: '',
|
|
|
|
|
|
)}>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name={folderIconName} className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
|
2026-02-21 06:09:55 +07:00
|
|
|
|
|
|
|
|
|
|
{renaming ? (
|
|
|
|
|
|
<form
|
|
|
|
|
|
className="flex min-w-0 flex-1 items-center gap-1"
|
2026-04-24 09:33:02 +03:00
|
|
|
|
|
2026-03-17 02:46:26 +02:00
|
|
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
|
|
|
|
onMouseDown={(event) => event.stopPropagation()}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
onSubmit={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
handleSaveRename();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
|
value={draft}
|
|
|
|
|
|
onChange={(event) => handleDraftChange(event.target.value)}
|
|
|
|
|
|
className="flex-1 min-w-0 bg-transparent typography-ui-label outline-none placeholder:text-muted-foreground"
|
|
|
|
|
|
autoFocus
|
2026-04-26 14:03:39 +03:00
|
|
|
|
placeholder={t('sessions.sidebar.folderItem.namePlaceholder')}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
onClick={(event) => event.stopPropagation()}
|
2026-03-17 02:46:26 +02:00
|
|
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
|
|
|
|
onMouseDown={(event) => event.stopPropagation()}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
handleCancelRename();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (event.key === ' ' || event.key === 'Enter') {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
onClick={(event) => event.stopPropagation()}
|
2026-03-17 02:46:26 +02:00
|
|
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
|
|
|
|
onMouseDown={(event) => event.stopPropagation()}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="check" className="size-4" />
|
2026-02-21 06:09:55 +07:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
handleCancelRename();
|
|
|
|
|
|
}}
|
2026-03-17 02:46:26 +02:00
|
|
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
|
|
|
|
onMouseDown={(event) => event.stopPropagation()}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
|
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="close" className="size-4" />
|
2026-02-21 06:09:55 +07:00
|
|
|
|
</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="min-w-0 flex items-center gap-1.5 flex-1">
|
2026-02-23 03:56:38 +07:00
|
|
|
|
<span className={cn('typography-ui-label font-semibold truncate', isDropTarget ? 'text-primary' : 'text-muted-foreground')}>
|
2026-02-21 06:09:55 +07:00
|
|
|
|
{folder.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="typography-micro text-muted-foreground/70 flex-shrink-0">
|
|
|
|
|
|
• {sessions.length}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{isCollapsed ? (
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="arrow-right-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
2026-02-21 06:09:55 +07:00
|
|
|
|
) : (
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
2026-02-21 06:09:55 +07:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Action buttons */}
|
2026-03-20 01:01:03 +02:00
|
|
|
|
{!renaming && (!hideActions || archivedBucket) ? (
|
2026-02-21 06:09:55 +07:00
|
|
|
|
<div className="flex items-center gap-0.5 px-0.5">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
'flex items-center gap-0.5 transition-opacity',
|
2026-05-03 17:19:55 +03:00
|
|
|
|
alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100',
|
2026-03-20 01:01:03 +02:00
|
|
|
|
archivedBucket && 'absolute right-0.5 top-1/2 z-10 -translate-y-1/2 px-0',
|
2026-02-21 06:09:55 +07:00
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-03-20 01:01:03 +02:00
|
|
|
|
{!archivedBucket && onNewSession ? (
|
2026-02-23 03:56:38 +07:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
onNewSession();
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
2026-04-26 14:03:39 +03:00
|
|
|
|
aria-label={t('sessions.sidebar.folderItem.newSessionAria', { folderName: folder.name })}
|
|
|
|
|
|
title={t('sessions.sidebar.project.actions.newSession')}
|
2026-02-23 03:56:38 +07:00
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="add" className="h-3.5 w-3.5" />
|
2026-02-23 03:56:38 +07:00
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{/* Only allow sub-folders at depth 0 (one level deep max) */}
|
2026-03-20 01:01:03 +02:00
|
|
|
|
{!archivedBucket && onNewSubFolder && depth === 0 ? (
|
2026-02-23 03:56:38 +07:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
onNewSubFolder();
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
2026-04-26 14:03:39 +03:00
|
|
|
|
aria-label={t('sessions.sidebar.folderItem.newSubfolderAria', { folderName: folder.name })}
|
|
|
|
|
|
title={t('sessions.sidebar.folderItem.newSubfolder')}
|
2026-02-23 03:56:38 +07:00
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="folder-add" className="h-3.5 w-3.5" />
|
2026-02-23 03:56:38 +07:00
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
2026-03-20 01:01:03 +02:00
|
|
|
|
{!archivedBucket ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
handleStartRename();
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
2026-04-26 14:03:39 +03:00
|
|
|
|
aria-label={t('sessions.sidebar.folderItem.renameAria', { folderName: folder.name })}
|
2026-03-20 01:01:03 +02:00
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="pencil-ai" className="h-3.5 w-3.5" />
|
2026-03-20 01:01:03 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
) : null}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={(event) => {
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
onDelete();
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
2026-04-26 14:03:39 +03:00
|
|
|
|
aria-label={archivedBucket
|
|
|
|
|
|
? t('sessions.sidebar.folderItem.deleteArchivedInFolderAria', { folderName: folder.name })
|
|
|
|
|
|
: t('sessions.sidebar.folderItem.deleteFolderAria', { folderName: folder.name })}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
>
|
2026-05-13 13:26:15 +03:00
|
|
|
|
<Icon name="delete-bin" className="h-3.5 w-3.5" />
|
2026-02-21 06:09:55 +07:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Folder body */}
|
|
|
|
|
|
{!isCollapsed ? (
|
|
|
|
|
|
<div className="pb-1 pl-2">
|
2026-02-23 03:56:38 +07:00
|
|
|
|
{/* Sub-folders first */}
|
|
|
|
|
|
{subFolderItems}
|
|
|
|
|
|
{/* Then sessions */}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
{sessions.length > 0 ? (
|
|
|
|
|
|
sessions.map((node) =>
|
2026-03-06 17:22:59 +02:00
|
|
|
|
renderSessionNode(node, 0, groupDirectory ?? null, projectId ?? null, archivedBucket),
|
2026-02-21 06:09:55 +07:00
|
|
|
|
)
|
2026-02-23 03:56:38 +07:00
|
|
|
|
) : !subFolderItems ? (
|
2026-02-21 06:09:55 +07:00
|
|
|
|
<div className="py-1 pl-1.5 text-left typography-micro text-muted-foreground/70">
|
2026-04-26 14:03:39 +03:00
|
|
|
|
{t('sessions.sidebar.folderItem.emptyFolder')}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
</div>
|
2026-02-23 03:56:38 +07:00
|
|
|
|
) : null}
|
2026-02-21 06:09:55 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const SessionFolderItem = React.memo(SessionFolderItemBase) as <TSessionNode>(
|
|
|
|
|
|
props: SessionFolderItemProps<TSessionNode>,
|
|
|
|
|
|
) => React.ReactElement;
|