typescript
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user