frontend tweaks, preview workspace
This commit is contained in:
+175
-49
@@ -26,6 +26,7 @@ import './styles.css';
|
|||||||
import AssetManager, { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView } from './asset_manager';
|
import AssetManager, { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView } from './asset_manager';
|
||||||
import useApiError from './hooks/useApiError';
|
import useApiError from './hooks/useApiError';
|
||||||
import SkeuomorphicWorkspace from './skeuomorphic_ws';
|
import SkeuomorphicWorkspace from './skeuomorphic_ws';
|
||||||
|
import PreviewWorkspace from './preview/PreviewWorkspace';
|
||||||
import DetailPanel from './detail/DetailPanel';
|
import DetailPanel from './detail/DetailPanel';
|
||||||
import TagsPanel from './tags/TagsPanel';
|
import TagsPanel from './tags/TagsPanel';
|
||||||
import CorrespondentsPanel from './correspondents/CorrespondentsPanel';
|
import CorrespondentsPanel from './correspondents/CorrespondentsPanel';
|
||||||
@@ -41,10 +42,13 @@ import {
|
|||||||
RefreshIcon,
|
RefreshIcon,
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
MinusVerticalIcon,
|
MinusVerticalIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
TextScanIcon,
|
||||||
|
AnalyzeIcon,
|
||||||
|
CloseIcon,
|
||||||
} from './ui/icons';
|
} from './ui/icons';
|
||||||
import DocumentsTable from './documents/DocumentsTable';
|
import DocumentsTable from './documents/DocumentsTable';
|
||||||
import { AppShellContext, useAppShell } from './appShellContext';
|
import { AppShellContext, useAppShell } from './appShellContext';
|
||||||
import DocumentViewerRoute from './routes/DocumentViewerRoute';
|
|
||||||
|
|
||||||
const runtimeApiBase =
|
const runtimeApiBase =
|
||||||
typeof window !== 'undefined' && window.__PAPERCRATE_API_BASE_URL
|
typeof window !== 'undefined' && window.__PAPERCRATE_API_BASE_URL
|
||||||
@@ -3624,6 +3628,26 @@ const AppLayout = () => {
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [previewDocumentId, closeDocumentPreview]);
|
}, [previewDocumentId, closeDocumentPreview]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previewDocumentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
ensurePreviewData(previewDocumentId).catch((error) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyApiError(error, 'Failed to open document preview.');
|
||||||
|
closeDocumentPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [previewDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]);
|
||||||
|
|
||||||
const handleDocumentTitleUpdate = useCallback(
|
const handleDocumentTitleUpdate = useCallback(
|
||||||
async (documentId, nextTitle) => {
|
async (documentId, nextTitle) => {
|
||||||
const trimmed = nextTitle.trim();
|
const trimmed = nextTitle.trim();
|
||||||
@@ -5188,6 +5212,8 @@ const AppLayout = () => {
|
|||||||
exitSkeuoWorkspace,
|
exitSkeuoWorkspace,
|
||||||
skeuoWorkspaceProps,
|
skeuoWorkspaceProps,
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
resolveApiPath,
|
resolveApiPath,
|
||||||
openTagsModal,
|
openTagsModal,
|
||||||
@@ -5226,6 +5252,8 @@ const AppLayout = () => {
|
|||||||
exitSkeuoWorkspace,
|
exitSkeuoWorkspace,
|
||||||
skeuoWorkspaceProps,
|
skeuoWorkspaceProps,
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
resolveApiPath,
|
resolveApiPath,
|
||||||
openTagsModal,
|
openTagsModal,
|
||||||
@@ -5387,6 +5415,14 @@ const DocumentsRoute = () => {
|
|||||||
openTagsModal,
|
openTagsModal,
|
||||||
openCorrespondentsModal,
|
openCorrespondentsModal,
|
||||||
exitSkeuoWorkspace,
|
exitSkeuoWorkspace,
|
||||||
|
previewWorkspaceDocument,
|
||||||
|
previewWorkspaceEntry,
|
||||||
|
closeDocumentPreview,
|
||||||
|
handleThumbnailRegeneration,
|
||||||
|
resolveApiPath,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
|
notifyApiError,
|
||||||
} = useAppShell();
|
} = useAppShell();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -5416,12 +5452,24 @@ const DocumentsRoute = () => {
|
|||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
} = documentsTableProps;
|
} = documentsTableProps;
|
||||||
|
|
||||||
|
const isWorkspace = workspaceMode === 'skeuo';
|
||||||
|
const isPreviewWorkspace = Boolean(previewWorkspaceDocument);
|
||||||
|
const showPreviewWorkspace = !isWorkspace && isPreviewWorkspace;
|
||||||
const isGridView = viewMode === 'grid';
|
const isGridView = viewMode === 'grid';
|
||||||
const showingSearchResults = Array.isArray(searchResults);
|
const showingSearchResults = Array.isArray(searchResults);
|
||||||
const headerTitle = showingSearchResults ? 'Search results' : currentFolderName;
|
let headerTitle;
|
||||||
const headerSubtitle = showingSearchResults
|
let headerSubtitle = null;
|
||||||
? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}`
|
if (showPreviewWorkspace) {
|
||||||
: null;
|
const previewTitle = previewWorkspaceDocument?.title
|
||||||
|
|| previewWorkspaceDocument?.original_name
|
||||||
|
|| 'Document preview';
|
||||||
|
headerTitle = previewTitle;
|
||||||
|
} else if (showingSearchResults) {
|
||||||
|
headerTitle = 'Search results';
|
||||||
|
headerSubtitle = `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}`;
|
||||||
|
} else {
|
||||||
|
headerTitle = currentFolderName;
|
||||||
|
}
|
||||||
const parentBreadcrumb = breadcrumbs && breadcrumbs.length > 1 ? breadcrumbs[breadcrumbs.length - 2] : null;
|
const parentBreadcrumb = breadcrumbs && breadcrumbs.length > 1 ? breadcrumbs[breadcrumbs.length - 2] : null;
|
||||||
const handleNavigateParent = parentBreadcrumb
|
const handleNavigateParent = parentBreadcrumb
|
||||||
? () => {
|
? () => {
|
||||||
@@ -5432,18 +5480,85 @@ const DocumentsRoute = () => {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const detailHasContent = detailPanelProps.selectedDocuments?.length > 0;
|
const detailHasContent = !isWorkspace && !showPreviewWorkspace
|
||||||
|
&& detailPanelProps.selectedDocuments?.length > 0;
|
||||||
const toggleSidebar = sidebarCollapsed ? expandSidebar : collapseSidebar;
|
const toggleSidebar = sidebarCollapsed ? expandSidebar : collapseSidebar;
|
||||||
const ToggleIcon = sidebarCollapsed ? ChevronsRightIcon : ChevronsLeftIcon;
|
const ToggleIcon = sidebarCollapsed ? ChevronsRightIcon : ChevronsLeftIcon;
|
||||||
const toggleLabel = sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
|
const toggleLabel = sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
|
||||||
|
const mainContentClass = (() => {
|
||||||
|
if (isWorkspace) {
|
||||||
|
return 'main-content main-content--workspace';
|
||||||
|
}
|
||||||
|
if (showPreviewWorkspace) {
|
||||||
|
return 'main-content main-content--preview';
|
||||||
|
}
|
||||||
|
return `main-content main-content--documents${
|
||||||
|
detailHasContent ? ' main-content--has-detail' : ''
|
||||||
|
}`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const bodyClass = (() => {
|
||||||
|
if (isWorkspace) {
|
||||||
|
return 'main-content__body main-content__body--workspace';
|
||||||
|
}
|
||||||
|
if (showPreviewWorkspace) {
|
||||||
|
return 'main-content__body main-content__body--preview';
|
||||||
|
}
|
||||||
|
return `main-content__body main-content__body--documents${
|
||||||
|
detailHasContent ? ' main-content__body--has-detail' : ''
|
||||||
|
}`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const previewDownloadHref = useMemo(() => {
|
||||||
|
if (!previewWorkspaceDocument?.current_version?.download_path) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resolveApiPath(previewWorkspaceDocument.current_version.download_path);
|
||||||
|
}, [previewWorkspaceDocument, resolveApiPath]);
|
||||||
|
|
||||||
|
const previewHasOcr = useMemo(
|
||||||
|
() => Boolean(
|
||||||
|
previewWorkspaceDocument && getDocumentAsset(previewWorkspaceDocument, 'ocr-text'),
|
||||||
|
),
|
||||||
|
[previewWorkspaceDocument, getDocumentAsset],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePreviewOcr = useCallback(async () => {
|
||||||
|
if (!previewWorkspaceDocument) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const asset = getDocumentAsset(previewWorkspaceDocument, 'ocr-text');
|
||||||
|
if (!asset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const ensured = await ensureAssetUrl(previewWorkspaceDocument.id, asset, { force: false });
|
||||||
|
const entry = ensured || asset;
|
||||||
|
const url = entry?.url
|
||||||
|
|| resolveDocumentAssetUrl(previewWorkspaceDocument, 'ocr-text', {
|
||||||
|
ensureAssetUrl,
|
||||||
|
getAsset: getDocumentAsset,
|
||||||
|
});
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('OCR text URL unavailable.');
|
||||||
|
}
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
} catch (error) {
|
||||||
|
notifyApiError(error, 'Unable to open OCR text.');
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
previewWorkspaceDocument,
|
||||||
|
ensureAssetUrl,
|
||||||
|
getDocumentAsset,
|
||||||
|
notifyApiError,
|
||||||
|
]);
|
||||||
|
|
||||||
if (workspaceMode === 'skeuo') {
|
|
||||||
return (
|
return (
|
||||||
<DocumentsLayout
|
<DocumentsLayout
|
||||||
sidebarProps={sidebarPropsWithActions}
|
sidebarProps={sidebarPropsWithActions}
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
sidebarCollapsed={sidebarCollapsed}
|
||||||
>
|
>
|
||||||
<div className="main-content">
|
<div className={mainContentClass}>
|
||||||
<div className="panel-header main-content__header">
|
<div className="panel-header main-content__header">
|
||||||
<div className="panel-actions main-content__actions">
|
<div className="panel-actions main-content__actions">
|
||||||
{sidebarCollapsed ? (
|
{sidebarCollapsed ? (
|
||||||
@@ -5457,7 +5572,7 @@ const DocumentsRoute = () => {
|
|||||||
<ToggleIcon />
|
<ToggleIcon />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{parentBreadcrumb ? (
|
{parentBreadcrumb && !showPreviewWorkspace ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button ghost"
|
className="icon-button ghost"
|
||||||
@@ -5475,6 +5590,8 @@ const DocumentsRoute = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="spacer" />
|
<div className="spacer" />
|
||||||
|
{isWorkspace ? (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button ghost"
|
className="icon-button ghost"
|
||||||
@@ -5487,53 +5604,53 @@ const DocumentsRoute = () => {
|
|||||||
<button type="button" className="secondary" onClick={exitSkeuoWorkspace}>
|
<button type="button" className="secondary" onClick={exitSkeuoWorkspace}>
|
||||||
Back to List
|
Back to List
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</>
|
||||||
</div>
|
) : showPreviewWorkspace ? (
|
||||||
<div className="main-content__body">
|
<>
|
||||||
<SkeuomorphicWorkspace {...skeuoWorkspaceProps} />
|
{previewDownloadHref ? (
|
||||||
</div>
|
<a
|
||||||
</div>
|
className="icon-button"
|
||||||
</DocumentsLayout>
|
href={previewDownloadHref}
|
||||||
);
|
target="_blank"
|
||||||
}
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Download document"
|
||||||
return (
|
title="Download document"
|
||||||
<DocumentsLayout
|
|
||||||
sidebarProps={sidebarPropsWithActions}
|
|
||||||
sidebarCollapsed={sidebarCollapsed}
|
|
||||||
>
|
>
|
||||||
<div className={`main-content main-content--documents${detailHasContent ? ' main-content--has-detail' : ''}`}>
|
<DownloadIcon />
|
||||||
<div className="panel-header main-content__header">
|
</a>
|
||||||
<div className="panel-actions main-content__actions">
|
) : null}
|
||||||
{sidebarCollapsed ? (
|
{previewHasOcr ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button ghost"
|
className="icon-button ghost"
|
||||||
onClick={toggleSidebar}
|
onClick={handlePreviewOcr}
|
||||||
aria-label={toggleLabel}
|
aria-label="View OCR text"
|
||||||
title={toggleLabel}
|
title="View OCR text"
|
||||||
>
|
>
|
||||||
<ToggleIcon />
|
<TextScanIcon />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{parentBreadcrumb ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-button ghost"
|
className="icon-button ghost"
|
||||||
onClick={handleNavigateParent}
|
onClick={() => handleThumbnailRegeneration(previewWorkspaceDocument.id)}
|
||||||
aria-label="Go to parent folder"
|
aria-label="Re-run analysis"
|
||||||
title="Go to parent folder"
|
title="Re-run analysis"
|
||||||
>
|
>
|
||||||
<ArrowUpIcon />
|
<AnalyzeIcon />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
<button
|
||||||
<h2 className="main-content__title">
|
type="button"
|
||||||
{headerTitle}
|
className="icon-button ghost"
|
||||||
{headerSubtitle ? (
|
onClick={() => closeDocumentPreview()}
|
||||||
<span className="main-content__subtitle">{headerSubtitle}</span>
|
aria-label="Close preview"
|
||||||
) : null}
|
title="Close preview"
|
||||||
</h2>
|
>
|
||||||
<div className="spacer" />
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="view-toggle" role="group" aria-label="Change view">
|
<div className="view-toggle" role="group" aria-label="Change view">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -5579,14 +5696,23 @@ const DocumentsRoute = () => {
|
|||||||
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
|
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
|
||||||
Desk View
|
Desk View
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={bodyClass}>
|
||||||
className={`main-content__body main-content__body--documents${detailHasContent ? ' main-content__body--has-detail' : ''}`}
|
{isWorkspace ? (
|
||||||
>
|
<SkeuomorphicWorkspace {...skeuoWorkspaceProps} />
|
||||||
|
) : showPreviewWorkspace ? (
|
||||||
|
<PreviewWorkspace
|
||||||
|
document={previewWorkspaceDocument}
|
||||||
|
previewEntry={previewWorkspaceEntry}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<DocumentsTable {...documentsTableProps} showHeader={false} />
|
<DocumentsTable {...documentsTableProps} showHeader={false} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{detailHasContent ? (
|
{!isWorkspace && !showPreviewWorkspace && detailHasContent ? (
|
||||||
<DetailPanel {...detailPanelProps} />
|
<DetailPanel {...detailPanelProps} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -5739,7 +5865,7 @@ const AppRouter = () => (
|
|||||||
<Route path="/" element={<Navigate to="/documents" replace />} />
|
<Route path="/" element={<Navigate to="/documents" replace />} />
|
||||||
<Route path="/documents" element={<DocumentsRoute />} />
|
<Route path="/documents" element={<DocumentsRoute />} />
|
||||||
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
||||||
<Route path="/documents/:documentId" element={<DocumentViewerRoute />} />
|
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
|
||||||
<Route path="*" element={<Navigate to="/documents" replace />} />
|
<Route path="*" element={<Navigate to="/documents" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DownloadIcon } from '../ui/icons';
|
|
||||||
import { formatFileSize } from '../utils/format';
|
import { formatFileSize } from '../utils/format';
|
||||||
|
|
||||||
const PreviewWorkspace = ({
|
const PreviewWorkspace = ({
|
||||||
document,
|
document,
|
||||||
previewEntry,
|
previewEntry,
|
||||||
resolveApiPath,
|
|
||||||
onClose,
|
|
||||||
onRegenerateThumbnails,
|
|
||||||
}) => {
|
}) => {
|
||||||
if (!document) {
|
if (!document) {
|
||||||
return null;
|
return null;
|
||||||
@@ -15,9 +11,6 @@ const PreviewWorkspace = ({
|
|||||||
|
|
||||||
const title = document.title || document.original_name || 'Document';
|
const title = document.title || document.original_name || 'Document';
|
||||||
const mime = previewEntry?.contentType || document.content_type || 'application/pdf';
|
const mime = previewEntry?.contentType || document.content_type || 'application/pdf';
|
||||||
const downloadHref = document.current_version?.download_path
|
|
||||||
? resolveApiPath(document.current_version.download_path)
|
|
||||||
: null;
|
|
||||||
const sizeBytes = Number(document.current_version?.size_bytes) || 0;
|
const sizeBytes = Number(document.current_version?.size_bytes) || 0;
|
||||||
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : null;
|
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : null;
|
||||||
const metadata =
|
const metadata =
|
||||||
@@ -25,48 +18,6 @@ const PreviewWorkspace = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="preview-workspace">
|
<section className="preview-workspace">
|
||||||
<header className="preview-workspace__header">
|
|
||||||
<div className="preview-workspace__meta">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary"
|
|
||||||
onClick={() => onClose(document.folder_id ?? 'root')}
|
|
||||||
>
|
|
||||||
← Back
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<h2>{title}</h2>
|
|
||||||
<span className="meta">
|
|
||||||
{document.content_type || mime}
|
|
||||||
{sizeLabel ? ` · ${sizeLabel}` : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="preview-workspace__actions">
|
|
||||||
<a
|
|
||||||
className="button-link with-icon"
|
|
||||||
href={downloadHref || '#'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-disabled={!downloadHref}
|
|
||||||
onClick={(event) => {
|
|
||||||
if (!downloadHref) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DownloadIcon className="icon-inline" />
|
|
||||||
<span>Download</span>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="secondary"
|
|
||||||
onClick={() => onRegenerateThumbnails(document.id)}
|
|
||||||
>
|
|
||||||
Re-run analysis
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="preview-workspace__body">
|
<div className="preview-workspace__body">
|
||||||
{!previewEntry?.url ? (
|
{!previewEntry?.url ? (
|
||||||
<div className="preview-workspace__message">Loading preview…</div>
|
<div className="preview-workspace__message">Loading preview…</div>
|
||||||
@@ -78,15 +29,16 @@ const PreviewWorkspace = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{metadata && (
|
{metadata ? (
|
||||||
<section className="preview-workspace__metadata">
|
<section className="preview-workspace__metadata">
|
||||||
<h3>Metadata</h3>
|
<h3>Metadata</h3>
|
||||||
|
{sizeLabel ? <p><strong>Size:</strong> {sizeLabel}</p> : null}
|
||||||
|
<p><strong>Type:</strong> {mime}</p>
|
||||||
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
||||||
</section>
|
</section>
|
||||||
)}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PreviewWorkspace;
|
export default PreviewWorkspace;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useAppShell } from '../appShellContext';
|
import { useAppShell } from '../appShellContext';
|
||||||
import PreviewWorkspace from '../preview/PreviewWorkspace';
|
|
||||||
|
|
||||||
const DocumentViewerRoute = () => {
|
const DocumentViewerRoute = () => {
|
||||||
const {
|
const {
|
||||||
previewWorkspaceDocument,
|
previewWorkspaceDocument,
|
||||||
previewWorkspaceEntry,
|
|
||||||
closeDocumentPreview,
|
|
||||||
handleThumbnailRegeneration,
|
|
||||||
ensurePreviewData,
|
ensurePreviewData,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
resolveApiPath,
|
|
||||||
} = useAppShell();
|
} = useAppShell();
|
||||||
const { documentId } = useParams();
|
const { documentId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -44,29 +39,15 @@ const DocumentViewerRoute = () => {
|
|||||||
};
|
};
|
||||||
}, [documentId, ensurePreviewData, notifyApiError, navigate]);
|
}, [documentId, ensurePreviewData, notifyApiError, navigate]);
|
||||||
|
|
||||||
const isReady =
|
if (!documentId) {
|
||||||
documentId && previewWorkspaceDocument && previewWorkspaceDocument.id === documentId;
|
return <Navigate to="/documents" replace />;
|
||||||
|
|
||||||
if (!isReady) {
|
|
||||||
return (
|
|
||||||
<main className="preview-main">
|
|
||||||
<div className="preview-workspace__message">Loading preview…</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (!previewWorkspaceDocument || previewWorkspaceDocument.id !== documentId) {
|
||||||
<main className="preview-main">
|
return <div className="preview-workspace__message">Loading preview…</div>;
|
||||||
<PreviewWorkspace
|
}
|
||||||
document={previewWorkspaceDocument}
|
|
||||||
previewEntry={previewWorkspaceEntry}
|
return <Navigate to="/documents" replace />;
|
||||||
resolveApiPath={resolveApiPath}
|
|
||||||
onClose={closeDocumentPreview}
|
|
||||||
onRegenerateThumbnails={handleThumbnailRegeneration}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DocumentViewerRoute;
|
export default DocumentViewerRoute;
|
||||||
|
|
||||||
|
|||||||
@@ -291,12 +291,6 @@ const Sidebar = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const rootNode = folderNodes.get('root');
|
const rootNode = folderNodes.get('root');
|
||||||
const hintText = appStatus === 'bootstrapping' && loading
|
|
||||||
? 'Loading your library…'
|
|
||||||
: previewActive
|
|
||||||
? 'Viewing document preview. Press ← Back to return to the library.'
|
|
||||||
: 'Drag files here to upload.';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<div className="panel-header sidebar__header">
|
<div className="panel-header sidebar__header">
|
||||||
@@ -371,7 +365,6 @@ const Sidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body sidebar__body">
|
<div className="panel-body sidebar__body">
|
||||||
<span className="sidebar__hint">{hintText}</span>
|
|
||||||
{status && (
|
{status && (
|
||||||
<div className="sidebar__status">
|
<div className="sidebar__status">
|
||||||
<div className={`status-banner ${status.variant}`}>{status.message}</div>
|
<div className={`status-banner ${status.variant}`}>{status.message}</div>
|
||||||
|
|||||||
@@ -1188,11 +1188,6 @@ button.danger:hover:not([disabled]) {
|
|||||||
background: rgba(209, 67, 67, 0.12);
|
background: rgba(209, 67, 67, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__hint {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__search {
|
.sidebar__search {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
IconMinusVertical,
|
IconMinusVertical,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import FolderSvg from '../assets/folder.svg';
|
import FolderSvg from '../assets/folder.svg';
|
||||||
|
|
||||||
@@ -185,6 +186,15 @@ export const MinusVerticalIcon = ({ className, size = '1em', stroke = 1.6, ...re
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||||
|
<IconX
|
||||||
|
className={composeClassName('icon', className)}
|
||||||
|
size={size}
|
||||||
|
stroke={stroke}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||||
<IconAnalyze
|
<IconAnalyze
|
||||||
className={composeClassName('icon', className)}
|
className={composeClassName('icon', className)}
|
||||||
|
|||||||
Reference in New Issue
Block a user