732 lines
22 KiB
React
732 lines
22 KiB
React
import React, { useCallback, useEffect, useMemo, useRef, useId } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import {
|
|
ChevronIcon,
|
|
TrashIcon,
|
|
EditIcon,
|
|
FolderIcon,
|
|
SidebarCollapseIcon,
|
|
LogoutIcon,
|
|
ChevronDownIcon,
|
|
SettingsIcon,
|
|
CheckIcon,
|
|
PlusIcon,
|
|
FolderPlusIcon,
|
|
RestoreIcon,
|
|
SunIcon,
|
|
MoonIcon,
|
|
DesktopIcon,
|
|
} from '../ui/icons';
|
|
import PanelHeader from '../ui/PanelHeader';
|
|
import useFloatingMenu from '../ui/useFloatingMenu';
|
|
|
|
import { getTagColorStyle } from '../utils/colors';
|
|
import { useSidebarContext } from './SidebarContext';
|
|
|
|
const FolderNode = ({
|
|
node,
|
|
depth,
|
|
isSelected,
|
|
onToggle,
|
|
onSelect,
|
|
onDrop,
|
|
onDragOver,
|
|
onDragLeave,
|
|
onDelete,
|
|
onRename,
|
|
renderChildren,
|
|
onFolderDragStart,
|
|
onFolderDragEnd,
|
|
draggingFolderId,
|
|
}) => {
|
|
const isRoot = node.id === 'root';
|
|
const hasChildren = Boolean(node.hasChildren);
|
|
const canToggle = hasChildren || !node.loaded;
|
|
const showChevron = canToggle;
|
|
const icon = showChevron ? <ChevronIcon className="toggle-icon" /> : null;
|
|
const canDrag = !isRoot;
|
|
const isDragging = draggingFolderId === node.id;
|
|
const isExpanded = Boolean(node.expanded);
|
|
const rowClasses = ['folder-row'];
|
|
if (isSelected) {
|
|
rowClasses.push('active');
|
|
}
|
|
|
|
const handleToggleClick = (event) => {
|
|
event.stopPropagation();
|
|
if (canToggle) {
|
|
onToggle(node.id);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<li className={`folder-node${isDragging ? ' is-dragging' : ''}`}>
|
|
<div
|
|
className={rowClasses.join(' ')}
|
|
draggable={canDrag}
|
|
onClick={() => onSelect(node.id)}
|
|
onDoubleClick={() => {
|
|
if (canToggle) {
|
|
onToggle(node.id);
|
|
}
|
|
}}
|
|
onDragOver={(event) => onDragOver(event, node.id)}
|
|
onDragLeave={onDragLeave}
|
|
onDrop={(event) => onDrop(event, node.id)}
|
|
onDragStart={(event) => {
|
|
if (!canDrag || !onFolderDragStart) return;
|
|
onFolderDragStart(event, node.id);
|
|
}}
|
|
onDragEnd={(event) => {
|
|
if (onFolderDragEnd) {
|
|
onFolderDragEnd(event);
|
|
}
|
|
}}
|
|
>
|
|
<span
|
|
className={`toggle${showChevron ? '' : ' invisible'}${isExpanded ? ' expanded' : ''}`}
|
|
onClick={handleToggleClick}
|
|
>
|
|
{icon}
|
|
</span>
|
|
<span className="name-wrap">
|
|
<FolderIcon className="folder-icon-image" size={16} />
|
|
<span className="name__label" title={node.name}>
|
|
{node.name}
|
|
</span>
|
|
</span>
|
|
{node.id !== 'root' && (
|
|
<div className="folder-row__actions">
|
|
<button
|
|
type="button"
|
|
className="icon-button ghost"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
if (!onRename) return;
|
|
const nextName = window.prompt('Rename folder', node.name);
|
|
if (!nextName) {
|
|
return;
|
|
}
|
|
const trimmed = nextName.trim();
|
|
if (!trimmed || trimmed === node.name) {
|
|
return;
|
|
}
|
|
onRename(node.id, trimmed);
|
|
}}
|
|
title="Rename folder"
|
|
aria-label={`Rename folder ${node.name}`}
|
|
>
|
|
<EditIcon className="icon-edit" size={18} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="icon-button ghost"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onDelete(node.id);
|
|
}}
|
|
title="Delete folder"
|
|
aria-label={`Delete folder ${node.name}`}
|
|
>
|
|
<TrashIcon className="icon-trash" size={18} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isExpanded && node.children.length > 0 && (
|
|
<ul className={`folder-children${depth === 0 ? ' folder-children--level1' : ''}`}>
|
|
{renderChildren(node.children, depth + 1)}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const THEME_MODES = ['system', 'light', 'dark'];
|
|
const THEME_MODE_LABELS = {
|
|
system: 'System',
|
|
light: 'Light',
|
|
dark: 'Dark',
|
|
};
|
|
|
|
const Sidebar = ({
|
|
folderNodes,
|
|
onToggle,
|
|
onSelect,
|
|
onDrop,
|
|
onDragOver,
|
|
onDragLeave,
|
|
onDeleteFolder,
|
|
onRenameFolder,
|
|
selectedFolder,
|
|
onFolderDragStart,
|
|
onFolderDragEnd,
|
|
draggedFolderId,
|
|
onCreateFolder,
|
|
creatingFolder = false,
|
|
tags = [],
|
|
untaggedFilterId = null,
|
|
activeTagIds = [],
|
|
onToggleTagFilter,
|
|
correspondents = [],
|
|
activeCorrespondentIds = [],
|
|
onToggleCorrespondentFilter,
|
|
onManageTags,
|
|
onManageCorrespondents,
|
|
onCreateTag,
|
|
onCreateCorrespondent,
|
|
searchQuery = '',
|
|
onSearchChange,
|
|
onSearchSubmit,
|
|
onSearchClear,
|
|
isFilterActive,
|
|
onLogout,
|
|
status,
|
|
tenantName,
|
|
tenants = [],
|
|
activeTenantId = null,
|
|
onSelectTenant,
|
|
onOpenSettings,
|
|
}) => {
|
|
const {
|
|
setCollapsed,
|
|
neutralHue,
|
|
setNeutralHue,
|
|
resetNeutralHue,
|
|
themeMode,
|
|
cycleThemeMode,
|
|
themeModes,
|
|
} = useSidebarContext();
|
|
const handleCollapse = useCallback(() => {
|
|
setCollapsed(true);
|
|
}, [setCollapsed]);
|
|
const neutralHueInputId = useId();
|
|
const sortedCorrespondents = useMemo(() => {
|
|
if (!Array.isArray(correspondents)) {
|
|
return [];
|
|
}
|
|
return correspondents
|
|
.filter((entry) => entry?.name)
|
|
.slice()
|
|
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
|
}, [correspondents]);
|
|
const activeCorrespondentSet = useMemo(
|
|
() => new Set(activeCorrespondentIds || []),
|
|
[activeCorrespondentIds],
|
|
);
|
|
const handleToggleTag = onToggleTagFilter || (() => {});
|
|
const activeTagSet = new Set(activeTagIds);
|
|
const untaggedActive = untaggedFilterId ? activeTagSet.has(untaggedFilterId) : false;
|
|
const handleManageTags = onManageTags || (() => {});
|
|
const handleManageCorrespondents = onManageCorrespondents || (() => {});
|
|
const handleCreateTag = useCallback(async () => {
|
|
const input = window.prompt('New tag name');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
const trimmed = input.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
try {
|
|
await onCreateTag?.(trimmed);
|
|
} catch (error) {
|
|
console.error('[sidebar] failed to create tag', error);
|
|
}
|
|
}, [onCreateTag]);
|
|
|
|
const handleCreateCorrespondent = useCallback(async () => {
|
|
const input = window.prompt('New correspondent name');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
const trimmed = input.trim();
|
|
if (!trimmed) {
|
|
return;
|
|
}
|
|
try {
|
|
await onCreateCorrespondent?.(trimmed);
|
|
} catch (error) {
|
|
console.error('[sidebar] failed to create correspondent', error);
|
|
}
|
|
}, [onCreateCorrespondent]);
|
|
|
|
const handleCreateFolder = useCallback(() => {
|
|
if (creatingFolder) {
|
|
return;
|
|
}
|
|
onCreateFolder?.();
|
|
}, [creatingFolder, onCreateFolder]);
|
|
|
|
const handleNeutralHueReset = useCallback(() => {
|
|
resetNeutralHue();
|
|
}, [resetNeutralHue]);
|
|
|
|
const handleNeutralHueChange = useCallback(
|
|
(value) => {
|
|
if (value === '') {
|
|
resetNeutralHue();
|
|
return;
|
|
}
|
|
const parsed = Number(value);
|
|
if (Number.isNaN(parsed)) {
|
|
return;
|
|
}
|
|
setNeutralHue(parsed);
|
|
},
|
|
[resetNeutralHue, setNeutralHue],
|
|
);
|
|
|
|
const themeModeList = themeModes && themeModes.length ? themeModes : THEME_MODES;
|
|
const themeModeIndex = themeModeList.indexOf(themeMode);
|
|
const safeThemeModeIndex = themeModeIndex === -1 ? 0 : themeModeIndex;
|
|
const resolvedThemeMode = themeModeList[safeThemeModeIndex] || THEME_MODES[0];
|
|
const nextThemeMode = themeModeList[(safeThemeModeIndex + 1) % themeModeList.length];
|
|
const themeModeLabel = THEME_MODE_LABELS[resolvedThemeMode] || THEME_MODE_LABELS.system;
|
|
const nextThemeLabel = THEME_MODE_LABELS[nextThemeMode] || THEME_MODE_LABELS.system;
|
|
const themeModeIcon = resolvedThemeMode === 'dark'
|
|
? <MoonIcon size={16} />
|
|
: resolvedThemeMode === 'light'
|
|
? <SunIcon size={16} />
|
|
: <DesktopIcon size={16} />;
|
|
|
|
const handleThemeModeToggle = useCallback(() => {
|
|
cycleThemeMode();
|
|
}, [cycleThemeMode]);
|
|
|
|
const handleSearchInputChange = useCallback(
|
|
(event) => {
|
|
onSearchChange?.(event.target.value);
|
|
},
|
|
[onSearchChange],
|
|
);
|
|
const handleSearchFormSubmit = useCallback(
|
|
(event) => {
|
|
event.preventDefault();
|
|
onSearchSubmit?.();
|
|
},
|
|
[onSearchSubmit],
|
|
);
|
|
const handleSearchClear = useCallback(() => {
|
|
onSearchClear?.();
|
|
}, [onSearchClear]);
|
|
const tenantButtonRef = useRef(null);
|
|
const {
|
|
isOpen: tenantMenuOpen,
|
|
toggle: toggleTenantMenuFloating,
|
|
close: closeTenantMenu,
|
|
menuRef: tenantMenuRef,
|
|
menuStyle: tenantMenuStyle,
|
|
updatePosition: refreshTenantMenuPosition,
|
|
} = useFloatingMenu({
|
|
anchorRef: tenantButtonRef,
|
|
minWidth: 220,
|
|
offset: 6,
|
|
});
|
|
const showTenantList = tenants.length > 1;
|
|
|
|
const toggleTenantMenu = useCallback(() => {
|
|
if (!tenantMenuOpen && tenants.length === 0 && onSelectTenant) {
|
|
onSelectTenant(null, { refreshOnly: true });
|
|
}
|
|
toggleTenantMenuFloating();
|
|
}, [tenantMenuOpen, tenants.length, onSelectTenant, toggleTenantMenuFloating]);
|
|
|
|
useEffect(() => {
|
|
if (tenantMenuOpen) {
|
|
refreshTenantMenuPosition();
|
|
}
|
|
}, [tenantMenuOpen, refreshTenantMenuPosition, tenants.length]);
|
|
|
|
const handleTenantSelect = useCallback(
|
|
(tenant) => {
|
|
const targetId = tenant?.id || null;
|
|
if (!targetId) {
|
|
return;
|
|
}
|
|
closeTenantMenu();
|
|
onSelectTenant?.(tenant);
|
|
},
|
|
[closeTenantMenu, onSelectTenant],
|
|
);
|
|
|
|
const handleLogoutFromMenu = useCallback(() => {
|
|
closeTenantMenu();
|
|
onLogout?.();
|
|
}, [closeTenantMenu, onLogout]);
|
|
|
|
const handleSettingsFromMenu = useCallback(() => {
|
|
closeTenantMenu();
|
|
onOpenSettings?.();
|
|
}, [closeTenantMenu, onOpenSettings]);
|
|
|
|
const renderNodes = useCallback(
|
|
(ids, depth) =>
|
|
ids.map((id) => {
|
|
const node = folderNodes.get(id);
|
|
if (!node) return null;
|
|
return (
|
|
<FolderNode
|
|
key={id}
|
|
node={node}
|
|
depth={depth}
|
|
isSelected={selectedFolder === id}
|
|
onToggle={() => onToggle(id)}
|
|
onSelect={onSelect}
|
|
onDrop={onDrop}
|
|
onDragOver={onDragOver}
|
|
onDragLeave={onDragLeave}
|
|
onDelete={onDeleteFolder}
|
|
onRename={onRenameFolder}
|
|
renderChildren={renderNodes}
|
|
onFolderDragStart={onFolderDragStart}
|
|
onFolderDragEnd={onFolderDragEnd}
|
|
draggingFolderId={draggedFolderId}
|
|
/>
|
|
);
|
|
}),
|
|
[
|
|
folderNodes,
|
|
selectedFolder,
|
|
onToggle,
|
|
onSelect,
|
|
onDrop,
|
|
onDragOver,
|
|
onDragLeave,
|
|
onDeleteFolder,
|
|
onRenameFolder,
|
|
onFolderDragStart,
|
|
onFolderDragEnd,
|
|
draggedFolderId,
|
|
],
|
|
);
|
|
|
|
const rootNode = folderNodes.get('root');
|
|
const themeControls =
|
|
typeof neutralHue === 'number' || typeof neutralHue === 'string'
|
|
? (
|
|
<div className="sidebar-section sidebar-theme">
|
|
<div className="sidebar-section__header">
|
|
<h3>Theme</h3>
|
|
<div className="sidebar-section__actions">
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleThemeModeToggle}
|
|
aria-label={`Switch theme (next: ${nextThemeLabel})`}
|
|
title={`Theme: ${themeModeLabel} (next: ${nextThemeLabel})`}
|
|
>
|
|
{themeModeIcon}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleNeutralHueReset}
|
|
aria-label="Reset neutral hue"
|
|
title="Reset neutral hue"
|
|
>
|
|
<RestoreIcon size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<label className="sidebar-slider" htmlFor={neutralHueInputId}>
|
|
<span className="sidebar-slider__label">Hue</span>
|
|
<input
|
|
id={neutralHueInputId}
|
|
type="range"
|
|
min="0"
|
|
max="360"
|
|
step="1"
|
|
value={neutralHue}
|
|
onChange={(event) => handleNeutralHueChange(event.target.value)}
|
|
/>
|
|
<span className="sidebar-slider__value">{neutralHue}°</span>
|
|
</label>
|
|
</div>
|
|
)
|
|
: null;
|
|
|
|
return (
|
|
<aside className="sidebar">
|
|
<PanelHeader
|
|
className="sidebar__header"
|
|
leading={(
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={`sidebar__title-button${tenantMenuOpen ? ' is-open' : ''}`}
|
|
onClick={toggleTenantMenu}
|
|
aria-haspopup="true"
|
|
aria-expanded={tenantMenuOpen}
|
|
ref={tenantButtonRef}
|
|
>
|
|
<span className="sidebar__title">
|
|
Papercrate
|
|
{tenantName ? <span className="sidebar__tenant"> / {tenantName}</span> : null}
|
|
</span>
|
|
<ChevronDownIcon
|
|
className={`sidebar__title-chevron${tenantMenuOpen ? ' is-open' : ''}`}
|
|
size={16}
|
|
/>
|
|
</button>
|
|
{tenantMenuOpen
|
|
&& tenantMenuStyle
|
|
&& typeof document !== 'undefined'
|
|
? createPortal(
|
|
<div
|
|
className={`menu${showTenantList ? '' : ' menu--simple'}`}
|
|
ref={tenantMenuRef}
|
|
role="menu"
|
|
aria-label="Account menu"
|
|
style={tenantMenuStyle}
|
|
>
|
|
{showTenantList ? (
|
|
<>
|
|
<div className="menu__heading">Switch tenant</div>
|
|
<div className="menu__list">
|
|
{tenants.map((tenant) => {
|
|
const tenantId = tenant?.id || null;
|
|
const isActive = tenantId === activeTenantId;
|
|
const tenantLabel = tenant?.name;
|
|
return (
|
|
<button
|
|
key={tenantId || tenantLabel}
|
|
type="button"
|
|
className={`menu__item${isActive ? ' active' : ''}`}
|
|
onClick={() => handleTenantSelect(tenant)}
|
|
role="menuitem"
|
|
>
|
|
<span className="menu__check-slot">
|
|
{isActive ? <CheckIcon size={16} /> : null}
|
|
</span>
|
|
<span className="menu__label">{tenantLabel}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
) : null}
|
|
<div className="menu__footer">
|
|
<button
|
|
type="button"
|
|
className="menu__settings"
|
|
onClick={handleSettingsFromMenu}
|
|
>
|
|
<SettingsIcon size={16} />
|
|
Settings
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="menu__logout"
|
|
onClick={handleLogoutFromMenu}
|
|
>
|
|
<LogoutIcon size={16} />
|
|
Log out
|
|
</button>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
: null}
|
|
</>
|
|
)}
|
|
actions={
|
|
handleCollapse
|
|
? [
|
|
(
|
|
<button
|
|
key="collapse"
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleCollapse}
|
|
aria-label="Collapse sidebar"
|
|
title="Collapse sidebar"
|
|
>
|
|
<SidebarCollapseIcon />
|
|
</button>
|
|
),
|
|
]
|
|
: null
|
|
}
|
|
/>
|
|
<div className="panel-body sidebar__body">
|
|
{status && (
|
|
<div className="sidebar__status">
|
|
<div className={`status-banner ${status.variant}`}>{status.message}</div>
|
|
</div>
|
|
)}
|
|
{onSearchChange && (
|
|
<form className="sidebar__search" onSubmit={handleSearchFormSubmit}>
|
|
<input
|
|
type="search"
|
|
value={searchQuery}
|
|
onChange={handleSearchInputChange}
|
|
placeholder="Search documents"
|
|
aria-label="Search documents"
|
|
/>
|
|
{isFilterActive && (
|
|
<button type="button" onClick={handleSearchClear}>
|
|
Clear
|
|
</button>
|
|
)}
|
|
</form>
|
|
)}
|
|
<div className="sidebar-section sidebar-section--folders">
|
|
<div className="sidebar-section__header">
|
|
<h3>Folders</h3>
|
|
{onCreateFolder ? (
|
|
<div className="sidebar-section__actions">
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleCreateFolder}
|
|
aria-label="Create folder"
|
|
disabled={creatingFolder}
|
|
>
|
|
<FolderPlusIcon size={16} />
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<ul className="folder-tree">
|
|
{rootNode && renderNodes([rootNode.id], 0)}
|
|
</ul>
|
|
</div>
|
|
<div className="sidebar-section">
|
|
<div className="sidebar-section__header">
|
|
<h3>Tags</h3>
|
|
<div className="sidebar-section__actions">
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleCreateTag}
|
|
aria-label="Create tag"
|
|
>
|
|
<PlusIcon size={16} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleManageTags}
|
|
aria-label="Manage tags"
|
|
>
|
|
<SettingsIcon size={16} />
|
|
</button>
|
|
<span className="meta">{tags.length}</span>
|
|
</div>
|
|
</div>
|
|
<div
|
|
className={`sidebar-tag-cloud${
|
|
activeTagSet.size ? ' sidebar-tag-cloud--has-active' : ''
|
|
}`}
|
|
role="list"
|
|
>
|
|
{untaggedFilterId ? (
|
|
<button
|
|
type="button"
|
|
role="listitem"
|
|
className={`sidebar-tag-pill sidebar-tag-pill--untagged${
|
|
untaggedActive ? ' active' : ''
|
|
}`}
|
|
onClick={() => handleToggleTag(untaggedFilterId)}
|
|
aria-pressed={untaggedActive}
|
|
draggable={false}
|
|
>
|
|
No tag assigned
|
|
</button>
|
|
) : null}
|
|
{tags.map((tag) => {
|
|
const isActive = activeTagSet.has(tag.id);
|
|
const style = getTagColorStyle(tag.color);
|
|
const className = `sidebar-tag-pill${isActive ? ' active' : ''}`;
|
|
return (
|
|
<button
|
|
key={tag.id}
|
|
type="button"
|
|
role="listitem"
|
|
className={className}
|
|
style={style || undefined}
|
|
onClick={() => handleToggleTag(tag.id)}
|
|
aria-pressed={isActive}
|
|
draggable
|
|
onDragStart={(event) => {
|
|
try {
|
|
const payload = JSON.stringify({
|
|
id: tag.id,
|
|
label: tag.label,
|
|
color: tag.color || null,
|
|
});
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
event.dataTransfer.setData('application/x-papercrate-tag', payload);
|
|
event.dataTransfer.setData('text/papercrate-tag', payload);
|
|
} catch (error) {
|
|
console.warn('[sidebar] Failed to set tag drag payload', error);
|
|
}
|
|
}}
|
|
>
|
|
{tag.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="sidebar-section">
|
|
<div className="sidebar-section__header">
|
|
<h3>Correspondents</h3>
|
|
<div className="sidebar-section__actions">
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleCreateCorrespondent}
|
|
aria-label="Create correspondent"
|
|
>
|
|
<PlusIcon size={16} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={handleManageCorrespondents}
|
|
aria-label="Manage correspondents"
|
|
>
|
|
<SettingsIcon size={16} />
|
|
</button>
|
|
<span className="meta">{correspondents.length}</span>
|
|
</div>
|
|
</div>
|
|
<ul className="sidebar-correspondent-list">
|
|
{sortedCorrespondents.map((correspondent) => {
|
|
const isActive = activeCorrespondentSet.has(correspondent.id);
|
|
const className = `sidebar-correspondent-item${isActive ? ' active' : ''}`;
|
|
const handleSelect = () => {
|
|
const nextId = isActive ? null : correspondent.id;
|
|
onToggleCorrespondentFilter?.(nextId);
|
|
};
|
|
return (
|
|
<li key={correspondent.id}>
|
|
<span
|
|
className={className}
|
|
role="button"
|
|
onClick={handleSelect}
|
|
onKeyDown={(event) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
handleSelect();
|
|
}
|
|
}}
|
|
>
|
|
{correspondent.name}
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{themeControls ? <div className="sidebar__footer">{themeControls}</div> : null}
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
export default Sidebar;
|
|
export { FolderNode };
|