Eliminate folder grouping logic and related UI from all document list views. Switch to a flat document array model for rendering, selection, and filtering. Remove folder collapse/expand state and folder drag-and-drop handling. Update sidebar and toolbar components to remove folder creation and management controls. Simplify props and internal state across all views for consistency.
640 lines
20 KiB
TypeScript
640 lines
20 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
import { useDocuments } from '@/contexts/DocumentContext';
|
|
import type {
|
|
DocumentListDocument,
|
|
DocumentListState,
|
|
Folder,
|
|
IconSize,
|
|
SidebarFilter,
|
|
SortBy,
|
|
SortDirection,
|
|
ViewMode,
|
|
} from '@/types/documents';
|
|
import {
|
|
getDocumentListState,
|
|
getDocumentRecentlyOpenedMap,
|
|
saveDocumentListState,
|
|
} from '@/lib/client/dexie';
|
|
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
|
import { CreateFolderDialog } from '@/components/doclist/CreateFolderDialog';
|
|
import { DocumentListSkeleton } from '@/components/doclist/DocumentListSkeleton';
|
|
import { DocumentUploader } from '@/components/documents/DocumentUploader';
|
|
import { DocumentDndProvider } from './dnd/DocumentDndProvider';
|
|
import {
|
|
DocumentSelectionProvider,
|
|
useDocumentSelection,
|
|
} from './dnd/DocumentSelectionContext';
|
|
import type { DocumentDragItem } from './dnd/dndTypes';
|
|
import { FinderWindow, useIsNarrow } from './window/FinderWindow';
|
|
import { FinderToolbar } from './window/FinderToolbar';
|
|
import { FinderSidebar } from './window/FinderSidebar';
|
|
import { FinderStatusBar } from './window/FinderStatusBar';
|
|
import { IconsView } from './views/IconsView';
|
|
import { ListView } from './views/ListView';
|
|
import { ColumnsView } from './views/ColumnsView';
|
|
import { GalleryView } from './views/GalleryView';
|
|
|
|
type DocumentToDelete = {
|
|
id: string;
|
|
name: string;
|
|
type: DocumentListDocument['type'];
|
|
};
|
|
|
|
const DEFAULT_STATE: Required<
|
|
Pick<
|
|
DocumentListState,
|
|
'sortBy' | 'sortDirection' | 'folders' | 'collapsedFolders' | 'showHint'
|
|
>
|
|
> & {
|
|
viewMode: ViewMode;
|
|
iconSize: IconSize;
|
|
sidebarWidth: number;
|
|
sidebarFilter: SidebarFilter;
|
|
sidebarCollapsed: boolean;
|
|
} = {
|
|
sortBy: 'name',
|
|
sortDirection: 'asc',
|
|
folders: [],
|
|
collapsedFolders: [],
|
|
showHint: true,
|
|
viewMode: 'icons',
|
|
iconSize: 'md',
|
|
sidebarWidth: 220,
|
|
sidebarFilter: 'all',
|
|
sidebarCollapsed: false,
|
|
};
|
|
|
|
function normalizeViewMode(stored: DocumentListState['viewMode']): ViewMode {
|
|
if (stored === 'grid' || stored === undefined) return 'icons';
|
|
if (stored === 'list') return 'list';
|
|
return stored;
|
|
}
|
|
|
|
function generateDefaultFolderName(
|
|
doc1: DocumentListDocument,
|
|
doc2: DocumentListDocument,
|
|
): string {
|
|
const words1 = doc1.name.toLowerCase().split(/[\s\-_.]+/);
|
|
const words2 = doc2.name.toLowerCase().split(/[\s\-_.]+/);
|
|
const common = words1.filter((w) => words2.includes(w));
|
|
const significant = common.find((w) => w.length >= 3);
|
|
if (significant) {
|
|
if (significant === 'pdf') return 'PDFs';
|
|
if (significant === 'epub') return 'EPUBs';
|
|
if (significant === 'txt' || significant === 'md') return 'Documents';
|
|
return significant.charAt(0).toUpperCase() + significant.slice(1);
|
|
}
|
|
const timestamp = new Date().toISOString().slice(0, 10);
|
|
return `Folder ${timestamp}`;
|
|
}
|
|
|
|
function sortDocs(
|
|
docs: DocumentListDocument[],
|
|
sortBy: SortBy,
|
|
direction: SortDirection,
|
|
): DocumentListDocument[] {
|
|
const sorted = [...docs].sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'name':
|
|
return a.name.localeCompare(b.name);
|
|
case 'type':
|
|
return a.type.localeCompare(b.type);
|
|
case 'size':
|
|
return a.size - b.size;
|
|
default:
|
|
return a.lastModified - b.lastModified;
|
|
}
|
|
});
|
|
return direction === 'asc' ? sorted : sorted.reverse();
|
|
}
|
|
|
|
interface DocumentListInnerProps {
|
|
brand?: ReactNode;
|
|
appActions?: ReactNode;
|
|
}
|
|
|
|
function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
|
|
const [sortBy, setSortBy] = useState<SortBy>(DEFAULT_STATE.sortBy);
|
|
const [sortDirection, setSortDirection] = useState<SortDirection>(
|
|
DEFAULT_STATE.sortDirection,
|
|
);
|
|
const [viewMode, setViewMode] = useState<ViewMode>(DEFAULT_STATE.viewMode);
|
|
const [iconSize, setIconSize] = useState<IconSize>(DEFAULT_STATE.iconSize);
|
|
const [folders, setFolders] = useState<Folder[]>(DEFAULT_STATE.folders);
|
|
const [showHint, setShowHint] = useState(true);
|
|
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_STATE.sidebarWidth);
|
|
const [sidebarFilter, setSidebarFilter] = useState<SidebarFilter>('all');
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
const [query, setQuery] = useState('');
|
|
const [recentlyOpenedById, setRecentlyOpenedById] = useState<Record<string, number>>({});
|
|
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
|
|
const [documentToDelete, setDocumentToDelete] = useState<DocumentToDelete | null>(null);
|
|
const [pendingMerge, setPendingMerge] = useState<
|
|
| { sources: DocumentListDocument[]; target: DocumentListDocument }
|
|
| null
|
|
>(null);
|
|
const [newFolderName, setNewFolderName] = useState('');
|
|
const [manualFolderPrompt, setManualFolderPrompt] = useState(false);
|
|
|
|
const isNarrow = useIsNarrow();
|
|
const selection = useDocumentSelection();
|
|
|
|
const {
|
|
pdfDocs,
|
|
removePDFDocument,
|
|
isPDFLoading,
|
|
epubDocs,
|
|
removeEPUBDocument,
|
|
isEPUBLoading,
|
|
htmlDocs,
|
|
removeHTMLDocument,
|
|
isHTMLLoading,
|
|
} = useDocuments();
|
|
|
|
// Load saved state.
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
const saved = await getDocumentListState();
|
|
if (cancelled) return;
|
|
if (saved) {
|
|
setSortBy(saved.sortBy);
|
|
setSortDirection(saved.sortDirection);
|
|
setFolders(saved.folders ?? []);
|
|
setShowHint(saved.showHint ?? true);
|
|
setViewMode(normalizeViewMode(saved.viewMode));
|
|
setIconSize(saved.iconSize ?? DEFAULT_STATE.iconSize);
|
|
setSidebarWidth(saved.sidebarWidth ?? DEFAULT_STATE.sidebarWidth);
|
|
setSidebarFilter(saved.sidebarFilter ?? 'all');
|
|
setSidebarOpen(!(saved.sidebarCollapsed ?? false));
|
|
}
|
|
setIsInitialized(true);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
// Persist.
|
|
useEffect(() => {
|
|
if (!isInitialized) return;
|
|
const state: DocumentListState = {
|
|
sortBy,
|
|
sortDirection,
|
|
folders,
|
|
collapsedFolders: [],
|
|
showHint,
|
|
viewMode,
|
|
iconSize,
|
|
sidebarWidth,
|
|
sidebarFilter,
|
|
sidebarCollapsed: !sidebarOpen,
|
|
};
|
|
void saveDocumentListState(state);
|
|
}, [
|
|
sortBy,
|
|
sortDirection,
|
|
folders,
|
|
showHint,
|
|
viewMode,
|
|
iconSize,
|
|
sidebarWidth,
|
|
sidebarFilter,
|
|
sidebarOpen,
|
|
isInitialized,
|
|
]);
|
|
|
|
// Mobile drawer should never auto-open from persisted desktop state.
|
|
useEffect(() => {
|
|
if (!isNarrow) return;
|
|
setMobileSidebarOpen(false);
|
|
}, [isNarrow]);
|
|
|
|
// Build the union document list.
|
|
const rawDocuments: DocumentListDocument[] = useMemo(
|
|
() => [
|
|
...pdfDocs.map((d) => ({ ...d, type: 'pdf' as const })),
|
|
...epubDocs.map((d) => ({ ...d, type: 'epub' as const })),
|
|
...htmlDocs.map((d) => ({ ...d, type: 'html' as const })),
|
|
],
|
|
[pdfDocs, epubDocs, htmlDocs],
|
|
);
|
|
const rawDocumentIdsKey = useMemo(
|
|
() => rawDocuments.map((d) => d.id).sort().join('|'),
|
|
[rawDocuments],
|
|
);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const recentMap = await getDocumentRecentlyOpenedMap();
|
|
if (cancelled) return;
|
|
setRecentlyOpenedById(recentMap);
|
|
} catch (err) {
|
|
console.warn('Failed to load recently opened cache metadata:', err);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [rawDocumentIdsKey]);
|
|
|
|
const allDocuments: DocumentListDocument[] = useMemo(
|
|
() =>
|
|
rawDocuments.map((doc) => ({
|
|
...doc,
|
|
recentlyOpenedAt: recentlyOpenedById[doc.id] ?? 0,
|
|
})),
|
|
[rawDocuments, recentlyOpenedById],
|
|
);
|
|
|
|
const allDocumentsById = useMemo(() => {
|
|
const map = new Map<string, DocumentListDocument>();
|
|
for (const doc of allDocuments) map.set(doc.id, doc);
|
|
return map;
|
|
}, [allDocuments]);
|
|
|
|
const foldersWithLiveDocs = useMemo(
|
|
() =>
|
|
folders.map((folder) => ({
|
|
...folder,
|
|
documents: folder.documents
|
|
.map((d) => allDocumentsById.get(d.id))
|
|
.filter((d): d is DocumentListDocument => Boolean(d))
|
|
.map((d) => ({ ...d, folderId: folder.id })),
|
|
})),
|
|
[folders, allDocumentsById],
|
|
);
|
|
|
|
const folderIdByDocId = useMemo(() => {
|
|
const map = new Map<string, string>();
|
|
for (const folder of foldersWithLiveDocs) {
|
|
for (const doc of folder.documents) map.set(doc.id, folder.id);
|
|
}
|
|
return map;
|
|
}, [foldersWithLiveDocs]);
|
|
|
|
const allDocumentsWithFolder = useMemo(
|
|
() =>
|
|
allDocuments.map((doc) => ({
|
|
...doc,
|
|
folderId: folderIdByDocId.get(doc.id),
|
|
})),
|
|
[allDocuments, folderIdByDocId],
|
|
);
|
|
|
|
// Filter based on sidebar selection + search query.
|
|
const visibleDocuments = useMemo(() => {
|
|
const q = query.trim().toLowerCase();
|
|
let docs = allDocumentsWithFolder;
|
|
if (sidebarFilter === 'pdf') docs = docs.filter((d) => d.type === 'pdf');
|
|
else if (sidebarFilter === 'epub') docs = docs.filter((d) => d.type === 'epub');
|
|
else if (sidebarFilter === 'html') docs = docs.filter((d) => d.type === 'html');
|
|
else if (sidebarFilter === 'recents') {
|
|
docs = [...docs]
|
|
.filter((d) => (d.recentlyOpenedAt ?? 0) > 0)
|
|
.sort((a, b) => (b.recentlyOpenedAt ?? 0) - (a.recentlyOpenedAt ?? 0))
|
|
.slice(0, 20);
|
|
} else if (sidebarFilter.startsWith('folder:')) {
|
|
const fid = sidebarFilter.slice('folder:'.length);
|
|
const folder = foldersWithLiveDocs.find((f) => f.id === fid);
|
|
docs = folder
|
|
? folder.documents
|
|
.map((d) => allDocumentsById.get(d.id))
|
|
.filter((d): d is DocumentListDocument => Boolean(d))
|
|
.map((d) => ({ ...d, folderId: fid }))
|
|
: [];
|
|
}
|
|
if (q) docs = docs.filter((d) => d.name.toLowerCase().includes(q));
|
|
return docs;
|
|
}, [allDocumentsWithFolder, sidebarFilter, query, foldersWithLiveDocs, allDocumentsById]);
|
|
|
|
// Apply sort.
|
|
const sortedVisible = useMemo(() => {
|
|
if (sidebarFilter === 'recents') return visibleDocuments;
|
|
return sortDocs(visibleDocuments, sortBy, sortDirection);
|
|
}, [visibleDocuments, sidebarFilter, sortBy, sortDirection]);
|
|
|
|
const counts = useMemo(
|
|
() => ({
|
|
all: allDocuments.length,
|
|
pdf: pdfDocs.length,
|
|
epub: epubDocs.length,
|
|
html: htmlDocs.length,
|
|
}),
|
|
[allDocuments.length, pdfDocs.length, epubDocs.length, htmlDocs.length],
|
|
);
|
|
|
|
// --- Actions ---
|
|
|
|
const handleDelete = useCallback(async () => {
|
|
if (!documentToDelete) return;
|
|
try {
|
|
if (documentToDelete.type === 'pdf') await removePDFDocument(documentToDelete.id);
|
|
else if (documentToDelete.type === 'epub') await removeEPUBDocument(documentToDelete.id);
|
|
else if (documentToDelete.type === 'html') await removeHTMLDocument(documentToDelete.id);
|
|
setFolders((prev) =>
|
|
prev.map((f) => ({
|
|
...f,
|
|
documents: f.documents.filter(
|
|
(d) => !(d.id === documentToDelete.id && d.type === documentToDelete.type),
|
|
),
|
|
})),
|
|
);
|
|
setDocumentToDelete(null);
|
|
} catch (err) {
|
|
console.error('Failed to remove document:', err);
|
|
}
|
|
}, [documentToDelete, removePDFDocument, removeEPUBDocument, removeHTMLDocument]);
|
|
|
|
const handleDeleteDoc = useCallback((doc: DocumentListDocument) => {
|
|
setDocumentToDelete({ id: doc.id, name: doc.name, type: doc.type });
|
|
}, []);
|
|
|
|
const handleDropOnFolder = useCallback(
|
|
(folderId: string, item: DocumentDragItem) => {
|
|
setFolders((prev) =>
|
|
prev.map((f) => {
|
|
if (f.id !== folderId) {
|
|
// Remove the dropped docs from any other folder they were in.
|
|
return {
|
|
...f,
|
|
documents: f.documents.filter(
|
|
(d) => !item.ids.includes(d.id),
|
|
),
|
|
};
|
|
}
|
|
const existingIds = new Set(f.documents.map((d) => d.id));
|
|
const newDocs = item.docs
|
|
.filter((d) => !existingIds.has(d.id))
|
|
.map((d) => ({ ...d, folderId }));
|
|
return { ...f, documents: [...f.documents, ...newDocs] };
|
|
}),
|
|
);
|
|
setSidebarFilter(`folder:${folderId}`);
|
|
selection.clear();
|
|
},
|
|
[selection],
|
|
);
|
|
|
|
const handleMergeIntoFolder = useCallback(
|
|
(sources: DocumentListDocument[], target: DocumentListDocument) => {
|
|
if (target.folderId) return;
|
|
const filtered = sources.filter((s) => s.id !== target.id && !s.folderId);
|
|
if (filtered.length === 0) return;
|
|
setPendingMerge({ sources: filtered, target });
|
|
setNewFolderName('');
|
|
},
|
|
[],
|
|
);
|
|
|
|
const createFolderFromPending = useCallback(() => {
|
|
if (!pendingMerge) return;
|
|
const name =
|
|
newFolderName.trim() ||
|
|
generateDefaultFolderName(pendingMerge.sources[0], pendingMerge.target);
|
|
const folderId = `folder-${Date.now()}`;
|
|
setFolders((prev) => [
|
|
...prev,
|
|
{
|
|
id: folderId,
|
|
name,
|
|
documents: [
|
|
...pendingMerge.sources.map((d) => ({ ...d, folderId })),
|
|
{ ...pendingMerge.target, folderId },
|
|
],
|
|
},
|
|
]);
|
|
setPendingMerge(null);
|
|
setNewFolderName('');
|
|
setShowHint(false);
|
|
setSidebarFilter(`folder:${folderId}`);
|
|
selection.clear();
|
|
}, [pendingMerge, newFolderName, selection]);
|
|
|
|
const createManualFolder = useCallback(() => {
|
|
const name = newFolderName.trim() || `New Folder`;
|
|
const folderId = `folder-${Date.now()}`;
|
|
setFolders((prev) => [...prev, { id: folderId, name, documents: [] }]);
|
|
setNewFolderName('');
|
|
setManualFolderPrompt(false);
|
|
setSidebarFilter(`folder:${folderId}`);
|
|
}, [newFolderName]);
|
|
|
|
const handleDeleteFolder = useCallback((folderId: string) => {
|
|
setFolders((prev) => prev.filter((f) => f.id !== folderId));
|
|
if (sidebarFilter === `folder:${folderId}`) setSidebarFilter('all');
|
|
}, [sidebarFilter]);
|
|
|
|
// Status bar summary.
|
|
const summary = useMemo(() => {
|
|
const parts: string[] = [];
|
|
if (counts.pdf) parts.push(`${counts.pdf} PDF${counts.pdf === 1 ? '' : 's'}`);
|
|
if (counts.epub) parts.push(`${counts.epub} EPUB${counts.epub === 1 ? '' : 's'}`);
|
|
if (counts.html) parts.push(`${counts.html} HTML${counts.html === 1 ? '' : 's'}`);
|
|
return parts.join(' • ');
|
|
}, [counts]);
|
|
|
|
const totalBytes = useMemo(
|
|
() => allDocuments.reduce((acc, d) => acc + d.size, 0),
|
|
[allDocuments],
|
|
);
|
|
|
|
const isLoading = isPDFLoading || isEPUBLoading || isHTMLLoading;
|
|
|
|
const fallbackViewMode: ViewMode =
|
|
viewMode === 'columns' && isNarrow ? 'list' : viewMode;
|
|
const effectiveSidebarOpen = isNarrow ? mobileSidebarOpen : sidebarOpen;
|
|
|
|
return (
|
|
<FinderWindow
|
|
toolbar={
|
|
<FinderToolbar
|
|
viewMode={fallbackViewMode}
|
|
onViewModeChange={setViewMode}
|
|
iconSize={iconSize}
|
|
onIconSizeChange={setIconSize}
|
|
sortBy={sortBy}
|
|
sortDirection={sortDirection}
|
|
onSortByChange={setSortBy}
|
|
onSortDirectionToggle={() =>
|
|
setSortDirection((p) => (p === 'asc' ? 'desc' : 'asc'))
|
|
}
|
|
query={query}
|
|
onQueryChange={setQuery}
|
|
onToggleSidebar={() =>
|
|
isNarrow
|
|
? setMobileSidebarOpen((p) => !p)
|
|
: setSidebarOpen((p) => !p)
|
|
}
|
|
isSidebarOpen={effectiveSidebarOpen}
|
|
isNarrow={isNarrow}
|
|
leftSlot={brand}
|
|
rightSlot={appActions}
|
|
/>
|
|
}
|
|
sidebar={
|
|
<FinderSidebar
|
|
filter={sidebarFilter}
|
|
onFilterChange={setSidebarFilter}
|
|
folders={foldersWithLiveDocs}
|
|
counts={counts}
|
|
onDeleteFolder={handleDeleteFolder}
|
|
onNewFolder={() => {
|
|
setNewFolderName('');
|
|
setManualFolderPrompt(true);
|
|
}}
|
|
onDropOnFolder={handleDropOnFolder}
|
|
width={sidebarWidth}
|
|
onWidthChange={setSidebarWidth}
|
|
topSlot={<DocumentUploader variant="compact" />}
|
|
/>
|
|
}
|
|
statusBar={
|
|
<FinderStatusBar
|
|
itemCount={allDocuments.length}
|
|
selectedCount={selection.selectionSize}
|
|
totalSize={totalBytes}
|
|
summary={summary}
|
|
/>
|
|
}
|
|
sidebarOpen={effectiveSidebarOpen}
|
|
onSidebarOpenChange={(open) => {
|
|
if (isNarrow) setMobileSidebarOpen(open);
|
|
else setSidebarOpen(open);
|
|
}}
|
|
>
|
|
{!isLoading && showHint && allDocuments.length > 1 && (
|
|
<div className="px-3 pt-3 shrink-0 bg-background">
|
|
<div className="flex items-center justify-between bg-base border border-offbase rounded-md px-3 py-1 text-[12px]">
|
|
<p className="text-foreground">
|
|
Drag files onto each other to make folders. Drop into the sidebar to move.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowHint(false)}
|
|
className="h-6 w-6 inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base hover:scale-[1.01] rounded transition-all duration-200 ease-out"
|
|
aria-label="Dismiss hint"
|
|
>
|
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading ? (
|
|
<div className="flex-1 min-h-0 overflow-hidden">
|
|
<DocumentListSkeleton viewMode={fallbackViewMode} iconSize={iconSize} />
|
|
</div>
|
|
) : allDocuments.length === 0 ? (
|
|
<div className="flex-1 min-h-0 flex items-center justify-center p-6">
|
|
<DocumentUploader className="py-12 w-full max-w-2xl" />
|
|
</div>
|
|
) : (
|
|
<DocumentUploader variant="overlay" className="flex-1 min-h-0 flex flex-col">
|
|
{fallbackViewMode === 'icons' && (
|
|
<IconsView
|
|
documents={sortedVisible}
|
|
iconSize={iconSize}
|
|
onDeleteDoc={handleDeleteDoc}
|
|
onMergeIntoFolder={handleMergeIntoFolder}
|
|
/>
|
|
)}
|
|
{fallbackViewMode === 'list' && (
|
|
<ListView
|
|
documents={sortedVisible}
|
|
sortBy={sortBy}
|
|
sortDirection={sortDirection}
|
|
onSortChange={(b, d) => {
|
|
setSortBy(b);
|
|
setSortDirection(d);
|
|
}}
|
|
onDeleteDoc={handleDeleteDoc}
|
|
onMergeIntoFolder={handleMergeIntoFolder}
|
|
/>
|
|
)}
|
|
{fallbackViewMode === 'columns' && (
|
|
<ColumnsView
|
|
documents={sortedVisible}
|
|
onDeleteDoc={handleDeleteDoc}
|
|
onMergeIntoFolder={handleMergeIntoFolder}
|
|
/>
|
|
)}
|
|
{fallbackViewMode === 'gallery' && (
|
|
<GalleryView
|
|
documents={sortedVisible}
|
|
onDeleteDoc={handleDeleteDoc}
|
|
onMergeIntoFolder={handleMergeIntoFolder}
|
|
/>
|
|
)}
|
|
</DocumentUploader>
|
|
)}
|
|
|
|
<CreateFolderDialog
|
|
isOpen={pendingMerge !== null}
|
|
onClose={() => setPendingMerge(null)}
|
|
folderName={newFolderName}
|
|
onFolderNameChange={setNewFolderName}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
createFolderFromPending();
|
|
} else if (e.key === 'Escape') {
|
|
setPendingMerge(null);
|
|
setNewFolderName('');
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<CreateFolderDialog
|
|
isOpen={manualFolderPrompt}
|
|
onClose={() => setManualFolderPrompt(false)}
|
|
folderName={newFolderName}
|
|
onFolderNameChange={setNewFolderName}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
createManualFolder();
|
|
} else if (e.key === 'Escape') {
|
|
setManualFolderPrompt(false);
|
|
setNewFolderName('');
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
isOpen={documentToDelete !== null}
|
|
onClose={() => setDocumentToDelete(null)}
|
|
onConfirm={handleDelete}
|
|
title="Delete Document"
|
|
message={`Are you sure you want to delete ${documentToDelete?.name ?? 'this document'}?`}
|
|
confirmText="Delete"
|
|
isDangerous
|
|
/>
|
|
</FinderWindow>
|
|
);
|
|
}
|
|
|
|
export function DocumentList({
|
|
brand,
|
|
appActions,
|
|
}: {
|
|
brand?: ReactNode;
|
|
appActions?: ReactNode;
|
|
} = {}) {
|
|
return (
|
|
<DocumentDndProvider>
|
|
<DocumentSelectionProvider>
|
|
<DocumentListInner brand={brand} appActions={appActions} />
|
|
</DocumentSelectionProvider>
|
|
</DocumentDndProvider>
|
|
);
|
|
}
|