typescript

This commit is contained in:
2025-11-13 01:07:55 +01:00
parent b812d748ea
commit ada089c05b
147 changed files with 7427 additions and 2534 deletions
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useId } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { createPortal } from 'react-dom';
import {
ChevronIcon,
@@ -17,6 +18,7 @@ import {
MoonIcon,
DesktopIcon,
UploadIcon,
LogoIcon,
} from '../ui/icons';
import PanelHeader from '../ui/PanelHeader';
import useFloatingMenu from '../ui/useFloatingMenu';
@@ -24,8 +26,100 @@ import useFloatingMenu from '../ui/useFloatingMenu';
import { getTagColorStyle } from '../utils/colors';
import { useSidebarContext } from './SidebarContext';
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
const FolderNode = ({
type Identifier = string | number;
type FolderIdentifier = Identifier | 'root';
interface FolderTreeNode {
id: FolderIdentifier;
name?: string;
children: FolderIdentifier[];
hasChildren?: boolean;
loaded?: boolean;
expanded?: boolean;
}
interface FolderNodeProps {
node: FolderTreeNode;
depth: number;
isSelected: boolean;
onToggle: (folderId: FolderIdentifier) => void;
onSelect: (folderId: FolderIdentifier) => void;
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onDragOver: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onDragLeave: (event: React.DragEvent<HTMLDivElement>) => void;
onDelete: (folderId: FolderIdentifier) => void;
onRename?: (folderId: FolderIdentifier, name: string) => void;
renderChildren: (ids: FolderIdentifier[], depth: number) => ReactNode;
onFolderDragStart?: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onFolderDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void;
draggingFolderId?: FolderIdentifier | null;
}
interface TagEntry {
id: Identifier;
label: string;
color?: string | null;
}
interface CorrespondentEntry {
id: Identifier;
name: string;
}
interface TenantOption {
id?: Identifier | null;
name?: string | null;
}
type UploadHandler = (
files: FileList | File[] | Iterable<File>,
targetFolderId?: FolderIdentifier | null,
) => void;
interface SidebarProps {
folderNodes: Map<FolderIdentifier, FolderTreeNode>;
onToggle: (folderId: FolderIdentifier) => void;
onSelect: (folderId: FolderIdentifier) => void;
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onDragOver: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onDragLeave: (event: React.DragEvent<HTMLDivElement>) => void;
onDeleteFolder: (folderId: FolderIdentifier) => void;
onRenameFolder?: (folderId: FolderIdentifier, name: string) => void;
selectedFolder?: FolderIdentifier | null;
onFolderDragStart?: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
onFolderDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void;
draggedFolderId?: FolderIdentifier | null;
onCreateFolder?: () => void;
creatingFolder?: boolean;
tags?: TagEntry[];
untaggedFilterId?: Identifier | null;
activeTagIds?: Array<Identifier | null | undefined>;
onToggleTagFilter?: (tagId: Identifier | null | undefined) => void;
correspondents?: CorrespondentEntry[];
activeCorrespondentIds?: Array<Identifier | null | undefined>;
onToggleCorrespondentFilter?: (correspondentId: Identifier | null | undefined) => void;
onManageTags?: () => void;
onManageCorrespondents?: () => void;
onCreateTag?: (label: string) => Promise<void> | void;
onCreateCorrespondent?: (name: string) => Promise<void> | void;
searchQuery?: string;
onSearchChange?: (value: string) => void;
onSearchSubmit?: () => void;
onSearchClear?: () => void;
isFilterActive?: boolean;
onLogout?: () => void;
status?: StatusMessage | null;
tenantName?: string | null;
tenants?: TenantOption[];
activeTenantId?: Identifier | null;
onSelectTenant?: (tenant: TenantOption | null, options?: { refreshOnly?: boolean }) => void;
onOpenSettings?: () => void;
onUploadFiles?: UploadHandler;
}
const FolderNode: React.FC<FolderNodeProps> = ({
node,
depth,
isSelected,
@@ -42,7 +136,8 @@ const FolderNode = ({
draggingFolderId,
}) => {
const isRoot = node.id === 'root';
const hasChildren = Boolean(node.hasChildren);
const childIds = Array.isArray(node.children) ? node.children : [];
const hasChildren = Boolean(node.hasChildren || childIds.length);
const canToggle = hasChildren || !node.loaded;
const showChevron = canToggle;
const icon = showChevron ? <ChevronIcon className="toggle-icon" /> : null;
@@ -54,7 +149,7 @@ const FolderNode = ({
rowClasses.push('active');
}
const handleToggleClick = (event) => {
const handleToggleClick = (event: React.MouseEvent<HTMLSpanElement>) => {
event.stopPropagation();
if (canToggle) {
onToggle(node.id);
@@ -80,9 +175,7 @@ const FolderNode = ({
onFolderDragStart(event, node.id);
}}
onDragEnd={(event) => {
if (onFolderDragEnd) {
onFolderDragEnd(event);
}
onFolderDragEnd?.(event);
}}
>
<span
@@ -135,9 +228,9 @@ const FolderNode = ({
</div>
)}
</div>
{isExpanded && node.children.length > 0 && (
{isExpanded && childIds.length > 0 && (
<ul className={`folder-children${depth === 0 ? ' folder-children--level1' : ''}`}>
{renderChildren(node.children, depth + 1)}
{renderChildren(childIds, depth + 1)}
</ul>
)}
</li>
@@ -151,7 +244,16 @@ const THEME_MODE_LABELS = {
dark: 'Dark',
};
const Sidebar = ({
interface FloatingMenuControls {
isOpen: boolean;
toggle: () => void;
close: () => void;
menuRef: React.RefObject<HTMLDivElement>;
menuStyle: CSSProperties | null;
updatePosition: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({
folderNodes,
onToggle,
onSelect,
@@ -160,10 +262,10 @@ const Sidebar = ({
onDragLeave,
onDeleteFolder,
onRenameFolder,
selectedFolder,
selectedFolder = null,
onFolderDragStart,
onFolderDragEnd,
draggedFolderId,
draggedFolderId = null,
onCreateFolder,
creatingFolder = false,
tags = [],
@@ -181,9 +283,9 @@ const Sidebar = ({
onSearchChange,
onSearchSubmit,
onSearchClear,
isFilterActive,
isFilterActive = false,
onLogout,
status,
status = null,
tenantName,
tenants = [],
activeTenantId = null,
@@ -209,8 +311,8 @@ const Sidebar = ({
sidebarSuppressed,
collapseSidebar,
} = usePanelManager();
const uploadInputRef = useRef(null);
const sidebarRef = useRef(null);
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const sidebarRef = useRef<HTMLDivElement | null>(null);
const {
panelStyle: sidebarStyle,
handleProps: sidebarHandleProps,
@@ -223,24 +325,36 @@ const Sidebar = ({
const neutralHueInputId = useId();
const neutralChromaInputId = useId();
const neutralContrastInputId = useId();
const sortedCorrespondents = useMemo(() => {
const sortedCorrespondents = useMemo<CorrespondentEntry[]>(() => {
if (!Array.isArray(correspondents)) {
return [];
}
return correspondents
.filter((entry) => entry?.name)
.filter((entry): entry is CorrespondentEntry & { name: string } => Boolean(entry?.name))
.slice()
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
.sort((a, b) => a.name!.localeCompare(b.name!, undefined, { sensitivity: 'base' }));
}, [correspondents]);
const activeCorrespondentSet = useMemo(
() => new Set(activeCorrespondentIds || []),
() => new Set<Identifier | null | undefined>(activeCorrespondentIds || []),
[activeCorrespondentIds],
);
const handleToggleTag = onToggleTagFilter || (() => {});
const activeTagSet = new Set(activeTagIds);
const handleToggleTag = useCallback(
(tagId: Identifier | null | undefined) => {
onToggleTagFilter?.(tagId);
},
[onToggleTagFilter],
);
const activeTagSet = useMemo(
() => new Set<Identifier | null | undefined>(activeTagIds || []),
[activeTagIds],
);
const untaggedActive = untaggedFilterId ? activeTagSet.has(untaggedFilterId) : false;
const handleManageTags = onManageTags || (() => {});
const handleManageCorrespondents = onManageCorrespondents || (() => {});
const handleManageTags = useCallback(() => {
onManageTags?.();
}, [onManageTags]);
const handleManageCorrespondents = useCallback(() => {
onManageCorrespondents?.();
}, [onManageCorrespondents]);
const handleCreateTag = useCallback(async () => {
const input = window.prompt('New tag name');
if (!input) {
@@ -252,7 +366,7 @@ const Sidebar = ({
}
try {
await onCreateTag?.(trimmed);
} catch (error) {
} catch (error: unknown) {
console.error('[sidebar] failed to create tag', error);
}
}, [onCreateTag]);
@@ -268,7 +382,7 @@ const Sidebar = ({
}
try {
await onCreateCorrespondent?.(trimmed);
} catch (error) {
} catch (error: unknown) {
console.error('[sidebar] failed to create correspondent', error);
}
}, [onCreateCorrespondent]);
@@ -288,10 +402,10 @@ const Sidebar = ({
}, [onUploadFiles]);
const handleUploadInputChange = useCallback(
(event) => {
(event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target?.files;
if (files && files.length && onUploadFiles) {
onUploadFiles(files, selectedFolder);
onUploadFiles(files, selectedFolder ?? 'root');
}
if (event.target) {
event.target.value = '';
@@ -307,7 +421,7 @@ const Sidebar = ({
}, [resetNeutralHue, resetNeutralChroma, resetNeutralContrast]);
const handleNeutralHueChange = useCallback(
(value) => {
(value: string) => {
if (value === '') {
resetNeutralHue();
return;
@@ -322,7 +436,7 @@ const Sidebar = ({
);
const handleNeutralChromaChange = useCallback(
(value) => {
(value: string) => {
if (value === '') {
resetNeutralChroma();
return;
@@ -337,7 +451,7 @@ const Sidebar = ({
);
const handleNeutralContrastChange = useCallback(
(value) => {
(value: string) => {
if (value === '') {
resetNeutralContrast();
return;
@@ -351,16 +465,16 @@ const Sidebar = ({
[resetNeutralContrast, setNeutralContrast],
);
const formatSliderValue = useCallback((value) => {
const formatSliderValue = useCallback((value: number | string) => {
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return value;
return String(value);
}
return parsed.toFixed(2);
}, []);
const themeModeList = themeModes && themeModes.length ? themeModes : THEME_MODES;
const themeModeIndex = themeModeList.indexOf(themeMode);
const themeModeList = Array.isArray(themeModes) && themeModes.length ? themeModes : THEME_MODES;
const themeModeIndex = themeModeList.indexOf(themeMode as string);
const safeThemeModeIndex = themeModeIndex === -1 ? 0 : themeModeIndex;
const resolvedThemeMode = themeModeList[safeThemeModeIndex] || THEME_MODES[0];
const nextThemeMode = themeModeList[(safeThemeModeIndex + 1) % themeModeList.length];
@@ -450,13 +564,13 @@ const Sidebar = ({
: null;
const handleSearchInputChange = useCallback(
(event) => {
(event: React.ChangeEvent<HTMLInputElement>) => {
onSearchChange?.(event.target.value);
},
[onSearchChange],
);
const handleSearchFormSubmit = useCallback(
(event) => {
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSearchSubmit?.();
},
@@ -465,7 +579,7 @@ const Sidebar = ({
const handleSearchClear = useCallback(() => {
onSearchClear?.();
}, [onSearchClear]);
const tenantButtonRef = useRef(null);
const tenantButtonRef = useRef<HTMLButtonElement | null>(null);
const {
isOpen: tenantMenuOpen,
toggle: toggleTenantMenuFloating,
@@ -477,7 +591,7 @@ const Sidebar = ({
anchorRef: tenantButtonRef,
minWidth: 220,
offset: 6,
});
}) as FloatingMenuControls;
const showTenantList = tenants.length > 1;
const menuClassName = `menu${!showTenantList && !themeMenuSection ? ' menu--simple' : ''}`;
@@ -495,7 +609,7 @@ const Sidebar = ({
}, [tenantMenuOpen, refreshTenantMenuPosition, tenants.length]);
const handleTenantSelect = useCallback(
(tenant) => {
(tenant: TenantOption | null) => {
const targetId = tenant?.id || null;
if (!targetId) {
return;
@@ -517,7 +631,7 @@ const Sidebar = ({
}, [closeTenantMenu, onOpenSettings]);
const renderNodes = useCallback(
(ids, depth) =>
(ids: FolderIdentifier[], depth: number): ReactNode =>
ids.map((id) => {
const node = folderNodes.get(id);
if (!node) return null;
@@ -527,7 +641,7 @@ const Sidebar = ({
node={node}
depth={depth}
isSelected={selectedFolder === id}
onToggle={() => onToggle(id)}
onToggle={onToggle}
onSelect={onSelect}
onDrop={onDrop}
onDragOver={onDragOver}
@@ -566,6 +680,65 @@ const Sidebar = ({
sidebarClassNames.push('sidebar--suppressed');
}
const tenantMenuContent = tenantMenuOpen
&& tenantMenuStyle
&& typeof document !== 'undefined'
? createPortal(
<div
className={menuClassName}
ref={tenantMenuRef}
role="menu"
aria-label="Account menu"
style={tenantMenuStyle}
>
{showTenantList ? (
<div className="menu__section">
<div className="menu__heading">Switch tenant</div>
<div className="menu__wrapper">
{tenants.map((tenant) => {
const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId;
const tenantLabel = tenant?.name || tenantId || 'Tenant';
return (
<button
key={tenantId ?? tenantLabel}
type="button"
className={`menu__button${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>
</div>
) : null}
<div className="menu__section">
<div className="menu__wrapper">
<button type="button" className="menu__button" onClick={handleSettingsFromMenu}>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__button menu__button--danger"
onClick={handleLogoutFromMenu}
>
<LogoutIcon size={16} />
Log out
</button>
</div>
</div>
{themeMenuSection}
</div>,
document.body,
)
: null;
return (
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef} style={sidebarStyle}>
<button
@@ -596,7 +769,8 @@ const Sidebar = ({
ref={tenantButtonRef}
>
<span className="sidebar__title">
Papercrate
<LogoIcon className="sidebar__logo" />
<span className="sidebar__title-text">Papercrate</span>
{tenantName ? <span className="sidebar__tenant"> / {tenantName}</span> : null}
</span>
<ChevronDownIcon
@@ -604,66 +778,7 @@ const Sidebar = ({
size={16}
/>
</button>
{tenantMenuOpen && tenantMenuStyle
? createPortal(
<div
className={menuClassName}
ref={tenantMenuRef}
role="menu"
aria-label="Account menu"
style={tenantMenuStyle}
>
{showTenantList ? (
<div className="menu__section">
<div className="menu__heading">Switch tenant</div>
<div className="menu__wrapper">
{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__button${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>
</div>
) : null}
<div className="menu__section">
<div className="menu__wrapper">
<button
type="button"
className="menu__button"
onClick={handleSettingsFromMenu}
>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__button menu__button--danger"
onClick={handleLogoutFromMenu}
>
<LogoutIcon size={16} />
Log out
</button>
</div>
</div>
{themeMenuSection}
</div>,
document.body,
)
: null}
{tenantMenuContent}
</>
)}
actions={(
@@ -7,9 +7,29 @@ import React, {
useEffect,
} from 'react';
interface SidebarContextValue {
collapsed: boolean;
setCollapsed: (value: boolean) => void;
neutralHue: number;
setNeutralHue: (value: number | string) => void;
resetNeutralHue: () => void;
neutralChroma: number;
setNeutralChroma: (value: number | string) => void;
resetNeutralChroma: () => void;
neutralContrast: number;
setNeutralContrast: (value: number | string) => void;
resetNeutralContrast: () => void;
themeMode: ThemeMode;
setThemeMode: (mode: ThemeMode) => void;
cycleThemeMode: () => void;
themeModes: ThemeMode[];
}
type ThemeMode = 'system' | 'light' | 'dark';
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
const SidebarContext = createContext(null);
const SidebarContext = createContext<SidebarContextValue | null>(null);
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
const DEFAULT_NEUTRAL_HUE = 145;
-132
View File
@@ -1,132 +0,0 @@
import { useMemo } from 'react';
import { TAG_FILTER_UNTAGGED } from '../app/appLayoutUtils';
const useSidebarProps = ({
folderNodes,
folderClickHandlers,
handleFolderDelete,
handleFolderRename,
selectedFolder,
handleFolderDragStart,
handleFolderDragEnd,
draggedFolderId,
handlePromptCreateFolder,
creatingFolder,
tags,
activeTagFilters,
toggleTagFilter,
handleTagCreate,
correspondents,
activeCorrespondentFilters,
toggleCorrespondentFilter,
handleCorrespondentCreate,
appStatus,
loading,
previewActive,
searchQuery,
handleSearchChange,
handleSearchSubmit,
clearFilters,
isFilterActive,
handleLogout,
status,
tenantName,
tenantOptions,
currentTenantId,
handleTenantSelect,
openSettings,
handleFileSelection,
uploadQueue,
}) =>
useMemo(
() => ({
folderNodes,
onToggle: folderClickHandlers.onToggle,
onSelect: folderClickHandlers.onSelect,
onDrop: folderClickHandlers.onDrop,
onDragOver: folderClickHandlers.onDragOver,
onDragLeave: folderClickHandlers.onDragLeave,
onDeleteFolder: handleFolderDelete,
onRenameFolder: handleFolderRename,
selectedFolder,
onFolderDragStart: handleFolderDragStart,
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
onCreateFolder: handlePromptCreateFolder,
creatingFolder,
tags,
untaggedFilterId: TAG_FILTER_UNTAGGED,
activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter,
onCreateTag: (label) => handleTagCreate({ label }),
correspondents,
activeCorrespondentIds: activeCorrespondentFilters,
onToggleCorrespondentFilter: toggleCorrespondentFilter,
onCreateCorrespondent: (name) => handleCorrespondentCreate({ name }),
appStatus,
loading,
previewActive,
searchQuery,
onSearchChange: handleSearchChange,
onSearchSubmit: handleSearchSubmit,
onSearchClear: clearFilters,
isFilterActive,
onLogout: handleLogout,
status,
tenantName,
tenants: tenantOptions,
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
onOpenSettings: openSettings,
onUploadFiles: (files, targetFolderId) => {
if (!handleFileSelection) {
return;
}
if (!files || files.length === 0) {
return;
}
const folderId = targetFolderId ?? selectedFolder ?? 'root';
handleFileSelection(files, folderId);
},
uploadQueue,
}),
[
handleFileSelection,
uploadQueue,
activeCorrespondentFilters,
activeTagFilters,
appStatus,
clearFilters,
correspondents,
creatingFolder,
currentTenantId,
folderClickHandlers,
folderNodes,
handleCorrespondentCreate,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderDelete,
handleFolderRename,
handleLogout,
handlePromptCreateFolder,
handleSearchChange,
handleSearchSubmit,
handleTagCreate,
handleTenantSelect,
isFilterActive,
loading,
openSettings,
previewActive,
searchQuery,
draggedFolderId,
selectedFolder,
status,
tags,
tenantName,
tenantOptions,
toggleCorrespondentFilter,
toggleTagFilter,
],
);
export default useSidebarProps;
+288
View File
@@ -0,0 +1,288 @@
import { useMemo } from 'react';
import type { DragEvent } from 'react';
import { TAG_FILTER_UNTAGGED } from '../app/appLayoutUtils';
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
type Identifier = string | number;
interface FolderTreeNode {
id: Identifier;
name?: string;
hasChildren?: boolean;
loaded?: boolean;
expanded?: boolean;
children: Identifier[];
[key: string]: unknown;
}
type FolderNodeMap = Map<Identifier, FolderTreeNode>;
type FolderToggleHandler = (folderId: Identifier) => void | Promise<void>;
type FolderDropHandler = (
event: DragEvent<HTMLElement>,
folderId: Identifier,
) => void | Promise<void>;
interface FolderClickHandlers {
onToggle: FolderToggleHandler;
onSelect: (folderId: Identifier | null) => void;
onDrop: FolderDropHandler;
onDragOver: (event: DragEvent<HTMLElement>, folderId: Identifier) => void;
onDragLeave: (event: DragEvent<HTMLElement>) => void;
}
interface TagOption {
id?: Identifier;
label?: string;
color?: string | null;
[key: string]: unknown;
}
interface CorrespondentOption {
id?: Identifier;
name?: string;
[key: string]: unknown;
}
interface TenantOption {
id?: Identifier;
name?: string;
[key: string]: unknown;
}
interface UploadQueueItem {
id: string;
name?: string;
size?: number | null;
folderId?: Identifier | 'root';
status?: string;
error?: unknown;
code?: number | null;
document?: unknown;
conflictDocumentId?: Identifier | null;
[key: string]: unknown;
}
type UploadFilesHandler = (
files: FileList | File[] | Iterable<File>,
targetFolderId: Identifier | 'root',
) => void | Promise<void>;
interface UseSidebarPropsArgs {
folderNodes: FolderNodeMap;
folderClickHandlers: FolderClickHandlers;
handleFolderDelete: (folderId: Identifier) => void | Promise<void>;
handleFolderRename: (folderId: Identifier, name: string) => void | Promise<void>;
selectedFolder: Identifier | null;
handleFolderDragStart: (event: DragEvent<HTMLElement>, folderId: Identifier) => void;
handleFolderDragEnd: (event: DragEvent<HTMLElement>) => void;
draggedFolderId: Identifier | null;
handlePromptCreateFolder?: () => void;
creatingFolder: boolean;
tags: TagOption[];
activeTagFilters: Identifier[];
toggleTagFilter: (tagId: Identifier) => void;
handleTagCreate: (payload: { label?: string }) => void | Promise<void>;
correspondents: CorrespondentOption[];
activeCorrespondentFilters: Identifier[];
toggleCorrespondentFilter: (correspondentId: Identifier) => void;
handleCorrespondentCreate: (payload: { name?: string }) =>
| Promise<CorrespondentOption | null | void>
| CorrespondentOption
| null
| void;
appStatus: string;
loading: boolean;
previewActive: boolean;
searchQuery: string;
handleSearchChange: (value: string) => void;
handleSearchSubmit: () => void;
clearFilters: () => void;
isFilterActive: boolean;
handleLogout: () => void | Promise<void>;
status: StatusMessage | null;
tenantName: string | null;
tenantOptions: TenantOption[];
currentTenantId: Identifier | null;
handleTenantSelect: (tenant: TenantOption | null, options?: { refreshOnly?: boolean }) => void | Promise<void>;
openSettings?: () => void;
handleFileSelection?: UploadFilesHandler;
uploadQueue?: UploadQueueItem[];
}
interface SidebarHookResult {
folderNodes: FolderNodeMap;
onToggle: FolderClickHandlers['onToggle'];
onSelect: FolderClickHandlers['onSelect'];
onDrop: FolderClickHandlers['onDrop'];
onDragOver: FolderClickHandlers['onDragOver'];
onDragLeave: FolderClickHandlers['onDragLeave'];
onDeleteFolder: UseSidebarPropsArgs['handleFolderDelete'];
onRenameFolder: UseSidebarPropsArgs['handleFolderRename'];
selectedFolder: Identifier | null;
onFolderDragStart: UseSidebarPropsArgs['handleFolderDragStart'];
onFolderDragEnd: UseSidebarPropsArgs['handleFolderDragEnd'];
draggedFolderId: Identifier | null;
onCreateFolder: UseSidebarPropsArgs['handlePromptCreateFolder'];
creatingFolder: boolean;
tags: TagOption[];
untaggedFilterId: typeof TAG_FILTER_UNTAGGED;
activeTagIds: Identifier[];
onToggleTagFilter: UseSidebarPropsArgs['toggleTagFilter'];
onCreateTag: (label: string) => void;
correspondents: CorrespondentOption[];
activeCorrespondentIds: Identifier[];
onToggleCorrespondentFilter: UseSidebarPropsArgs['toggleCorrespondentFilter'];
onCreateCorrespondent: (name: string) => void;
appStatus: string;
loading: boolean;
previewActive: boolean;
searchQuery: string;
onSearchChange: UseSidebarPropsArgs['handleSearchChange'];
onSearchSubmit: UseSidebarPropsArgs['handleSearchSubmit'];
onSearchClear: UseSidebarPropsArgs['clearFilters'];
isFilterActive: boolean;
onLogout: UseSidebarPropsArgs['handleLogout'];
status: StatusMessage | null;
tenantName: string | null;
tenants: TenantOption[];
activeTenantId: Identifier | null;
onSelectTenant: UseSidebarPropsArgs['handleTenantSelect'];
onOpenSettings: UseSidebarPropsArgs['openSettings'];
onUploadFiles: (files: FileList | File[] | Iterable<File>, targetFolderId?: Identifier | 'root') => void;
uploadQueue?: UploadQueueItem[];
}
const useSidebarProps = ({
folderNodes,
folderClickHandlers,
handleFolderDelete,
handleFolderRename,
selectedFolder,
handleFolderDragStart,
handleFolderDragEnd,
draggedFolderId,
handlePromptCreateFolder,
creatingFolder,
tags,
activeTagFilters,
toggleTagFilter,
handleTagCreate,
correspondents,
activeCorrespondentFilters,
toggleCorrespondentFilter,
handleCorrespondentCreate,
appStatus,
loading,
previewActive,
searchQuery,
handleSearchChange,
handleSearchSubmit,
clearFilters,
isFilterActive,
handleLogout,
status,
tenantName,
tenantOptions,
currentTenantId,
handleTenantSelect,
openSettings,
handleFileSelection,
uploadQueue,
}: UseSidebarPropsArgs): SidebarHookResult =>
useMemo(
() => ({
folderNodes,
onToggle: folderClickHandlers.onToggle,
onSelect: folderClickHandlers.onSelect,
onDrop: folderClickHandlers.onDrop,
onDragOver: folderClickHandlers.onDragOver,
onDragLeave: folderClickHandlers.onDragLeave,
onDeleteFolder: handleFolderDelete,
onRenameFolder: handleFolderRename,
selectedFolder,
onFolderDragStart: handleFolderDragStart,
onFolderDragEnd: handleFolderDragEnd,
draggedFolderId,
onCreateFolder: handlePromptCreateFolder,
creatingFolder,
tags,
untaggedFilterId: TAG_FILTER_UNTAGGED,
activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter,
onCreateTag: (label) => handleTagCreate({ label }),
correspondents,
activeCorrespondentIds: activeCorrespondentFilters,
onToggleCorrespondentFilter: toggleCorrespondentFilter,
onCreateCorrespondent: (name) => handleCorrespondentCreate({ name }),
appStatus,
loading,
previewActive,
searchQuery,
onSearchChange: handleSearchChange,
onSearchSubmit: handleSearchSubmit,
onSearchClear: clearFilters,
isFilterActive,
onLogout: handleLogout,
status,
tenantName,
tenants: tenantOptions,
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
onOpenSettings: openSettings,
onUploadFiles: (files, targetFolderId) => {
if (!handleFileSelection) {
return;
}
if (!files) {
return;
}
const normalizedFiles = Array.isArray(files) ? files : Array.from(files);
if (normalizedFiles.length === 0) {
return;
}
const folderId = targetFolderId ?? selectedFolder ?? 'root';
handleFileSelection(normalizedFiles, folderId);
},
uploadQueue,
}),
[
handleFileSelection,
uploadQueue,
activeCorrespondentFilters,
activeTagFilters,
appStatus,
clearFilters,
correspondents,
creatingFolder,
currentTenantId,
folderClickHandlers,
folderNodes,
handleCorrespondentCreate,
handleFolderDragEnd,
handleFolderDragStart,
handleFolderDelete,
handleFolderRename,
handleLogout,
handlePromptCreateFolder,
handleSearchChange,
handleSearchSubmit,
handleTagCreate,
handleTenantSelect,
isFilterActive,
loading,
openSettings,
previewActive,
searchQuery,
draggedFolderId,
selectedFolder,
status,
tags,
tenantName,
tenantOptions,
toggleCorrespondentFilter,
toggleTagFilter,
],
);
export default useSidebarProps;