|
|
|
@@ -24,7 +24,7 @@ import AssetManager, { getAssetFromVersion, resolveDocumentAssetUrl } from './as
|
|
|
|
|
import useApiError from './hooks/useApiError';
|
|
|
|
|
import SkeuomorphicWorkspace from './skeuomorphic_ws';
|
|
|
|
|
import { DownloadIcon, EditIcon } from './ui/icons';
|
|
|
|
|
import { getTagColorStyle, HEX_COLOR_PATTERN } from './utils/colors';
|
|
|
|
|
import { generateRandomTagColor, getTagColorStyle, HEX_COLOR_PATTERN } from './utils/colors';
|
|
|
|
|
import Sidebar from './sidebar/Sidebar';
|
|
|
|
|
import DocumentsTable, { FilterBar } from './documents/DocumentsTable';
|
|
|
|
|
|
|
|
|
@@ -285,7 +285,6 @@ const PreviewStack = ({
|
|
|
|
|
|
|
|
|
|
const DetailPanel = ({
|
|
|
|
|
selectedDocuments = [],
|
|
|
|
|
detailMap = new Map(),
|
|
|
|
|
tags = [],
|
|
|
|
|
tagLookupById = new Map(),
|
|
|
|
|
tagLookupByLabel = new Map(),
|
|
|
|
@@ -306,10 +305,8 @@ const DetailPanel = ({
|
|
|
|
|
ensureAssetUrl = null,
|
|
|
|
|
getDocumentAsset = () => null,
|
|
|
|
|
}) => {
|
|
|
|
|
const lookup = detailMap && typeof detailMap.get === 'function' ? detailMap : new Map();
|
|
|
|
|
const selectedCount = selectedDocuments.length;
|
|
|
|
|
const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
|
|
|
|
|
const detail = singleDoc ? lookup.get(singleDoc.id) || null : null;
|
|
|
|
|
|
|
|
|
|
const [titleEditDocId, setTitleEditDocId] = useState(null);
|
|
|
|
|
const [titleDraft, setTitleDraft] = useState('');
|
|
|
|
@@ -465,26 +462,31 @@ const DetailPanel = ({
|
|
|
|
|
|
|
|
|
|
const stackTotalSizeBytes = useMemo(() => {
|
|
|
|
|
if (!stackPreviews.length) return 0;
|
|
|
|
|
const byId = new Map(selectedDocuments.map((doc) => [doc.id, doc]));
|
|
|
|
|
return stackPreviews.reduce((sum, item) => {
|
|
|
|
|
const detailEntry = lookup.get(item.id);
|
|
|
|
|
const bytes = detailEntry?.current_version?.size_bytes || 0;
|
|
|
|
|
const source = byId.get(item.id);
|
|
|
|
|
const bytes = source?.current_version?.size_bytes;
|
|
|
|
|
return sum + (typeof bytes === 'number' ? bytes : 0);
|
|
|
|
|
}, 0);
|
|
|
|
|
}, [stackPreviews, lookup]);
|
|
|
|
|
}, [stackPreviews, selectedDocuments]);
|
|
|
|
|
|
|
|
|
|
const renderSingle = () => {
|
|
|
|
|
if (!singleDoc) {
|
|
|
|
|
return <p className="meta">Select a document to view metadata, tags and actions.</p>;
|
|
|
|
|
}
|
|
|
|
|
if (!detail) {
|
|
|
|
|
return <p className="meta">Loading details…</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const displayName = singleDoc.title || singleDoc.original_name;
|
|
|
|
|
const downloadHref = singleDoc.current_version?.download_path
|
|
|
|
|
? resolveApiPath(singleDoc.current_version.download_path)
|
|
|
|
|
: null;
|
|
|
|
|
const isEditingTitle = titleEditDocId === singleDoc.id;
|
|
|
|
|
const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
|
|
|
|
|
const issuedAt = singleDoc.issued_at
|
|
|
|
|
? new Date(singleDoc.issued_at).toLocaleString()
|
|
|
|
|
: '—';
|
|
|
|
|
const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : [];
|
|
|
|
|
const metadata =
|
|
|
|
|
singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
@@ -555,18 +557,13 @@ const DetailPanel = ({
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<strong>Size:</strong>{' '}
|
|
|
|
|
{detail.current_version
|
|
|
|
|
? `${(detail.current_version.size_bytes / 1024).toFixed(1)} KB`
|
|
|
|
|
: '—'}
|
|
|
|
|
{sizeBytes ? `${(sizeBytes / 1024).toFixed(1)} KB` : '—'}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<strong>Type:</strong> {singleDoc.content_type || 'Unknown'}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<strong>Issued:</strong>{' '}
|
|
|
|
|
{detail.document.issued_at
|
|
|
|
|
? new Date(detail.document.issued_at).toLocaleString()
|
|
|
|
|
: '—'}
|
|
|
|
|
<strong>Issued:</strong> {issuedAt}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<strong>Original filename:</strong>{' '}
|
|
|
|
@@ -607,8 +604,8 @@ const DetailPanel = ({
|
|
|
|
|
<div>
|
|
|
|
|
<dt>Tags</dt>
|
|
|
|
|
<div className="tag-list">
|
|
|
|
|
{detail.document.tags?.length ? (
|
|
|
|
|
detail.document.tags.map((tag) => {
|
|
|
|
|
{tagsForDoc.length ? (
|
|
|
|
|
tagsForDoc.map((tag) => {
|
|
|
|
|
const colorSource = tag?.color || tagLookupById.get(tag.id)?.color;
|
|
|
|
|
const style = getTagColorStyle(colorSource);
|
|
|
|
|
return (
|
|
|
|
@@ -643,13 +640,12 @@ const DetailPanel = ({
|
|
|
|
|
</datalist>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
{detail.document.metadata &&
|
|
|
|
|
Object.keys(detail.document.metadata).length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<dt>Metadata</dt>
|
|
|
|
|
<pre>{JSON.stringify(detail.document.metadata, null, 2)}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{metadata && (
|
|
|
|
|
<div>
|
|
|
|
|
<dt>Metadata</dt>
|
|
|
|
|
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
@@ -685,48 +681,37 @@ const DetailPanel = ({
|
|
|
|
|
</div>
|
|
|
|
|
{commonTags.length > 0 && (
|
|
|
|
|
<div className="bulk-tags">
|
|
|
|
|
{commonTags.map((label) => {
|
|
|
|
|
const key = typeof label === 'string' ? label.toLowerCase() : '';
|
|
|
|
|
const tagInfo = key ? tagLookupByLabel.get(key) : null;
|
|
|
|
|
const style = getTagColorStyle(tagInfo?.color);
|
|
|
|
|
return (
|
|
|
|
|
<span key={label} className="tag-pill" style={style || undefined}>
|
|
|
|
|
{label}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
<strong>Bulk tag operations</strong>
|
|
|
|
|
<form
|
|
|
|
|
className="inline"
|
|
|
|
|
onSubmit={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const input = event.currentTarget.elements.tag;
|
|
|
|
|
const value = input.value.trim();
|
|
|
|
|
if (!value) return;
|
|
|
|
|
onBulkTagAdd?.({ label: value, input });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input name="tag" placeholder="Add tag to selection" list="tag-catalog" />
|
|
|
|
|
<button type="submit">Add tag</button>
|
|
|
|
|
</form>
|
|
|
|
|
<form
|
|
|
|
|
className="inline"
|
|
|
|
|
onSubmit={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const input = event.currentTarget.elements.tag;
|
|
|
|
|
const value = input.value.trim();
|
|
|
|
|
if (!value) return;
|
|
|
|
|
onBulkTagRemove?.({ label: value, input });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input name="tag" placeholder="Remove tag from selection" list="tag-catalog" />
|
|
|
|
|
<button type="submit" className="secondary">
|
|
|
|
|
Remove tag
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="bulk-detail-actions">
|
|
|
|
|
<form
|
|
|
|
|
className="inline"
|
|
|
|
|
onSubmit={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const input = event.currentTarget.elements.tag;
|
|
|
|
|
const value = input.value.trim();
|
|
|
|
|
if (!value) return;
|
|
|
|
|
onBulkTagAdd?.({ label: value, input });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input name="tag" placeholder="Add tag to selection" list="tag-catalog" />
|
|
|
|
|
<button type="submit">Add tag</button>
|
|
|
|
|
</form>
|
|
|
|
|
<form
|
|
|
|
|
className="inline"
|
|
|
|
|
onSubmit={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const input = event.currentTarget.elements.tag;
|
|
|
|
|
const value = input.value.trim();
|
|
|
|
|
if (!value) return;
|
|
|
|
|
onBulkTagRemove?.({ label: value, input });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input name="tag" placeholder="Remove tag from selection" list="tag-catalog" />
|
|
|
|
|
<button type="submit" className="secondary">
|
|
|
|
|
Remove tag
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
<datalist id="tag-catalog">
|
|
|
|
|
{tags.map((tag) => (
|
|
|
|
|
<option key={tag.id} value={tag.label} />
|
|
|
|
@@ -774,7 +759,6 @@ const DetailPanel = ({
|
|
|
|
|
|
|
|
|
|
const PreviewWorkspace = ({
|
|
|
|
|
document,
|
|
|
|
|
detail,
|
|
|
|
|
previewEntry,
|
|
|
|
|
onClose,
|
|
|
|
|
onRegenerateThumbnails,
|
|
|
|
@@ -783,15 +767,14 @@ const PreviewWorkspace = ({
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const title =
|
|
|
|
|
detail?.document?.title ||
|
|
|
|
|
document.title ||
|
|
|
|
|
detail?.document?.original_name ||
|
|
|
|
|
document.original_name;
|
|
|
|
|
const title = document.title || document.original_name || 'Document';
|
|
|
|
|
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 metadata =
|
|
|
|
|
document.metadata && Object.keys(document.metadata).length > 0 ? document.metadata : null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="preview-workspace">
|
|
|
|
@@ -800,17 +783,15 @@ const PreviewWorkspace = ({
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="secondary"
|
|
|
|
|
onClick={() => onClose(detail?.document?.folder_id ?? document.folder_id ?? 'root')}
|
|
|
|
|
onClick={() => onClose(document.folder_id ?? 'root')}
|
|
|
|
|
>
|
|
|
|
|
← Back
|
|
|
|
|
</button>
|
|
|
|
|
<div>
|
|
|
|
|
<h2>{title}</h2>
|
|
|
|
|
<span className="meta">
|
|
|
|
|
{document.content_type || 'Document'}
|
|
|
|
|
{detail?.current_version?.size_bytes
|
|
|
|
|
? ` · ${(detail.current_version.size_bytes / 1024 / 1024).toFixed(2)} MB`
|
|
|
|
|
: ''}
|
|
|
|
|
{document.content_type || mime}
|
|
|
|
|
{sizeBytes ? ` · ${(sizeBytes / 1024 / 1024).toFixed(2)} MB` : ''}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
@@ -850,10 +831,10 @@ const PreviewWorkspace = ({
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{detail?.document?.metadata && Object.keys(detail.document.metadata).length > 0 && (
|
|
|
|
|
{metadata && (
|
|
|
|
|
<section className="preview-workspace__metadata">
|
|
|
|
|
<h3>Metadata</h3>
|
|
|
|
|
<pre>{JSON.stringify(detail.document.metadata, null, 2)}</pre>
|
|
|
|
|
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
@@ -1133,8 +1114,6 @@ const AppLayout = () => {
|
|
|
|
|
const [focusedRowKey, setFocusedRowKey] = useState(() =>
|
|
|
|
|
routeDocumentId ? `document:${routeDocumentId}` : null,
|
|
|
|
|
);
|
|
|
|
|
const [documentDetails, setDocumentDetails] = useState(() => new Map());
|
|
|
|
|
const documentDetailsRef = useRef(documentDetails);
|
|
|
|
|
const tokenRef = useRef(token);
|
|
|
|
|
const refreshPromiseRef = useRef(null);
|
|
|
|
|
const breadcrumbFetchRef = useRef(new Set());
|
|
|
|
@@ -1240,46 +1219,6 @@ const AppLayout = () => {
|
|
|
|
|
return { ...doc, current_version: updatedCurrentVersion };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mergeAssetIntoDetail = (detail, assetData) => {
|
|
|
|
|
if (!detail) return detail;
|
|
|
|
|
|
|
|
|
|
const nextDocument = detail.document
|
|
|
|
|
? mergeAssetIntoDocument(detail.document, assetData)
|
|
|
|
|
: detail.document;
|
|
|
|
|
|
|
|
|
|
let nextAssets = detail.assets;
|
|
|
|
|
let assetsChanged = false;
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(detail.assets)) {
|
|
|
|
|
const index = detail.assets.findIndex((item) => item?.id === assetData.id);
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
const existing = detail.assets[index];
|
|
|
|
|
if (!isAssetEquivalent(existing, assetData)) {
|
|
|
|
|
const copy = detail.assets.slice();
|
|
|
|
|
copy[index] = { ...existing, ...assetData };
|
|
|
|
|
nextAssets = copy;
|
|
|
|
|
assetsChanged = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
nextAssets = detail.assets.concat({ ...assetData });
|
|
|
|
|
assetsChanged = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
nextAssets = [{ ...assetData }];
|
|
|
|
|
assetsChanged = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!assetsChanged && nextDocument === detail.document) {
|
|
|
|
|
return detail;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...detail,
|
|
|
|
|
document: nextDocument,
|
|
|
|
|
assets: assetsChanged ? nextAssets : detail.assets,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const bootstrapInitializedRef = useRef(false);
|
|
|
|
|
const selectionInitializedRef = useRef(false);
|
|
|
|
|
const dragCounterRef = useRef(0);
|
|
|
|
@@ -1302,11 +1241,6 @@ const AppLayout = () => {
|
|
|
|
|
selectionAnchorRef.current = null;
|
|
|
|
|
setDraggedDocumentIds([]);
|
|
|
|
|
setDraggedFolderId(null);
|
|
|
|
|
setDocumentDetails(() => {
|
|
|
|
|
const next = new Map();
|
|
|
|
|
documentDetailsRef.current = next;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
setSearchResults(null);
|
|
|
|
|
setTags([]);
|
|
|
|
|
setSearchQuery('');
|
|
|
|
@@ -1382,10 +1316,6 @@ const AppLayout = () => {
|
|
|
|
|
}
|
|
|
|
|
}, [appStatus, resetWorkspaceState]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
documentDetailsRef.current = documentDetails;
|
|
|
|
|
}, [documentDetails]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
tokenRef.current = token;
|
|
|
|
|
}, [token]);
|
|
|
|
@@ -2032,38 +1962,6 @@ const AppLayout = () => {
|
|
|
|
|
setDraggedFolderId(null);
|
|
|
|
|
}, [setDraggedFolderId]);
|
|
|
|
|
|
|
|
|
|
const ensureDocumentDetail = useCallback(
|
|
|
|
|
async (documentId, { force = false } = {}) => {
|
|
|
|
|
if (!documentId) return null;
|
|
|
|
|
const cached = documentDetailsRef.current.get(documentId);
|
|
|
|
|
if (!force && cached) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { data } = await api.get(`/documents/${documentId}`);
|
|
|
|
|
const hydratedDetail = assetManager.hydrateDetail(data);
|
|
|
|
|
const hydratedDocument = hydratedDetail?.document || data.document;
|
|
|
|
|
|
|
|
|
|
setDocumentDetails((prev) => {
|
|
|
|
|
const next = new Map(prev);
|
|
|
|
|
next.set(documentId, hydratedDetail);
|
|
|
|
|
documentDetailsRef.current = next;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setDocuments((prev) =>
|
|
|
|
|
prev.map((doc) => (doc.id === documentId ? hydratedDocument : doc)),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setSearchResults((prev) =>
|
|
|
|
|
prev ? prev.map((doc) => (doc.id === documentId ? hydratedDocument : doc)) : null,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return hydratedDetail;
|
|
|
|
|
},
|
|
|
|
|
[assetManager, setDocumentDetails, setDocuments, setSearchResults],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const refreshTags = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await api.get('/tags');
|
|
|
|
@@ -2111,7 +2009,7 @@ const AppLayout = () => {
|
|
|
|
|
if (!trimmed) {
|
|
|
|
|
throw new Error('Tag label is required.');
|
|
|
|
|
}
|
|
|
|
|
const payload = { label: trimmed, color: color || null };
|
|
|
|
|
const payload = { label: trimmed, color: color || generateRandomTagColor() };
|
|
|
|
|
try {
|
|
|
|
|
await api.post('/tags', payload);
|
|
|
|
|
await refreshTags();
|
|
|
|
@@ -2139,18 +2037,15 @@ const AppLayout = () => {
|
|
|
|
|
console.warn('Failed to refresh root folder tree', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const nextSelectedId = applySelectedFolder(targetId, contents);
|
|
|
|
|
applySelectedFolder(targetId, contents);
|
|
|
|
|
setSearchResults(null);
|
|
|
|
|
if (nextSelectedId) {
|
|
|
|
|
await ensureDocumentDetail(nextSelectedId, { force: true });
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notifyApiError(error, 'Failed to load folder contents.');
|
|
|
|
|
} finally {
|
|
|
|
|
if (showLoading) setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[ensureFolderData, applySelectedFolder, ensureDocumentDetail, notifyApiError],
|
|
|
|
|
[ensureFolderData, applySelectedFolder, notifyApiError],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -2236,16 +2131,13 @@ const AppLayout = () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const contents = await ensureFolderData(selectedFolder, { force: true });
|
|
|
|
|
const nextSelectedId = applySelectedFolder(selectedFolder, contents);
|
|
|
|
|
if (nextSelectedId) {
|
|
|
|
|
await ensureDocumentDetail(nextSelectedId, { force: true });
|
|
|
|
|
}
|
|
|
|
|
applySelectedFolder(selectedFolder, contents);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notifyApiError(error, 'Failed to refresh folder.');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedFolder, ensureFolderData, applySelectedFolder, ensureDocumentDetail, notifyApiError]);
|
|
|
|
|
}, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (appStatus !== 'authenticated') {
|
|
|
|
@@ -2401,9 +2293,7 @@ const AppLayout = () => {
|
|
|
|
|
action,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
selectedDocumentIds.map((id) => ensureDocumentDetail(id, { force: true }).catch(() => null)),
|
|
|
|
|
);
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
@@ -2425,7 +2315,7 @@ const AppLayout = () => {
|
|
|
|
|
tags,
|
|
|
|
|
api,
|
|
|
|
|
refreshTags,
|
|
|
|
|
ensureDocumentDetail,
|
|
|
|
|
refreshCurrentFolder,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
setLoading,
|
|
|
|
|
],
|
|
|
|
@@ -2673,21 +2563,6 @@ const AppLayout = () => {
|
|
|
|
|
: prev,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setDocumentDetails((prev) => {
|
|
|
|
|
if (!prev.has(documentId)) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
const current = prev.get(documentId);
|
|
|
|
|
const updated = mergeAssetIntoDetail(current, entry);
|
|
|
|
|
if (updated === current) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
const next = new Map(prev);
|
|
|
|
|
next.set(documentId, updated);
|
|
|
|
|
documentDetailsRef.current = next;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return entry;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notifyApiError(error, 'Unable to refresh document asset.');
|
|
|
|
@@ -2698,7 +2573,6 @@ const AppLayout = () => {
|
|
|
|
|
assetManager,
|
|
|
|
|
setDocuments,
|
|
|
|
|
setSearchResults,
|
|
|
|
|
setDocumentDetails,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
@@ -2968,12 +2842,6 @@ const AppLayout = () => {
|
|
|
|
|
'success',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setDocumentDetails((prev) => {
|
|
|
|
|
const next = new Map(prev);
|
|
|
|
|
unique.forEach((id) => next.delete(id));
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
if (targetFolderId && targetFolderId !== selectedFolder) {
|
|
|
|
|
await ensureFolderData(targetFolderId, { force: true });
|
|
|
|
@@ -3020,7 +2888,7 @@ const AppLayout = () => {
|
|
|
|
|
params: { force: true },
|
|
|
|
|
});
|
|
|
|
|
setStatusMessage('Document re-analysis queued.', 'info');
|
|
|
|
|
await ensureDocumentDetail(documentId, { force: true });
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message =
|
|
|
|
|
error.response?.data?.error || 'Failed to request thumbnail generation.';
|
|
|
|
@@ -3029,7 +2897,7 @@ const AppLayout = () => {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[token, ensureDocumentDetail, notifyApiError, setStatusMessage],
|
|
|
|
|
[token, refreshCurrentFolder, notifyApiError, setStatusMessage],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const openDocumentPreview = useCallback(
|
|
|
|
@@ -3038,46 +2906,34 @@ const AppLayout = () => {
|
|
|
|
|
setPreviewDocumentId(documentId);
|
|
|
|
|
setPreviewDocumentLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const detailData = await ensureDocumentDetail(documentId, { force: false });
|
|
|
|
|
const pool = searchResults ?? documents;
|
|
|
|
|
const doc = pool.find((item) => item.id === documentId);
|
|
|
|
|
if (!doc) {
|
|
|
|
|
throw new Error('Document metadata unavailable.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previewAsset = getAssetFromVersion(
|
|
|
|
|
detailData?.document?.current_version || null,
|
|
|
|
|
'preview',
|
|
|
|
|
);
|
|
|
|
|
const currentVersion = doc.current_version || null;
|
|
|
|
|
const previewAsset = getAssetFromVersion(currentVersion, 'preview');
|
|
|
|
|
const thumbnailAsset = getAssetFromVersion(currentVersion, 'thumbnail');
|
|
|
|
|
|
|
|
|
|
if (previewAsset) {
|
|
|
|
|
const previewExpiresAt =
|
|
|
|
|
typeof previewAsset.expiresAt === 'number' ? previewAsset.expiresAt : null;
|
|
|
|
|
const previewNeedsRefresh =
|
|
|
|
|
!previewAsset.url || (previewExpiresAt && previewExpiresAt <= Date.now());
|
|
|
|
|
if (previewNeedsRefresh) {
|
|
|
|
|
const refreshAssetIfNeeded = async (asset) => {
|
|
|
|
|
if (!asset?.id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const expiresAt = typeof asset.expiresAt === 'number' ? asset.expiresAt : null;
|
|
|
|
|
const shouldForce = Boolean(asset.url && expiresAt && expiresAt <= Date.now());
|
|
|
|
|
if (!asset.url || shouldForce) {
|
|
|
|
|
try {
|
|
|
|
|
await ensureAssetUrl(documentId, previewAsset, {
|
|
|
|
|
force: Boolean(previewExpiresAt && previewExpiresAt <= Date.now()),
|
|
|
|
|
});
|
|
|
|
|
await ensureAssetUrl(documentId, asset, { force: shouldForce || !asset.url });
|
|
|
|
|
} catch (
|
|
|
|
|
// eslint-disable-next-line no-empty
|
|
|
|
|
error
|
|
|
|
|
) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const thumbnailAsset = getAssetFromVersion(
|
|
|
|
|
detailData?.document?.current_version || null,
|
|
|
|
|
'thumbnail',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (thumbnailAsset) {
|
|
|
|
|
const thumbExpiresAt =
|
|
|
|
|
typeof thumbnailAsset.expiresAt === 'number' ? thumbnailAsset.expiresAt : null;
|
|
|
|
|
const thumbNeedsRefresh =
|
|
|
|
|
!thumbnailAsset.url || (thumbExpiresAt && thumbExpiresAt <= Date.now());
|
|
|
|
|
if (thumbNeedsRefresh) {
|
|
|
|
|
ensureAssetUrl(documentId, thumbnailAsset, {
|
|
|
|
|
force: Boolean(thumbExpiresAt && thumbExpiresAt <= Date.now()),
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await refreshAssetIfNeeded(previewAsset);
|
|
|
|
|
refreshAssetIfNeeded(thumbnailAsset);
|
|
|
|
|
|
|
|
|
|
await ensurePreviewUrl(documentId, { force: false });
|
|
|
|
|
setActivePreviewId(documentId);
|
|
|
|
@@ -3091,7 +2947,14 @@ const AppLayout = () => {
|
|
|
|
|
setPreviewDocumentLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[ensureDocumentDetail, ensurePreviewUrl, ensureAssetUrl, navigate, notifyApiError],
|
|
|
|
|
[
|
|
|
|
|
documents,
|
|
|
|
|
searchResults,
|
|
|
|
|
ensurePreviewUrl,
|
|
|
|
|
ensureAssetUrl,
|
|
|
|
|
navigate,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleDocumentListFocus = useCallback(() => {
|
|
|
|
@@ -3301,8 +3164,16 @@ const AppLayout = () => {
|
|
|
|
|
|
|
|
|
|
const hydratePreview = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const detail = await ensureDocumentDetail(routeDocumentId, { force: true });
|
|
|
|
|
const doc = detail?.document || documentDetailsRef.current.get(routeDocumentId)?.document;
|
|
|
|
|
const pool = searchResults ?? documents;
|
|
|
|
|
let doc = pool.find((item) => item.id === routeDocumentId) || null;
|
|
|
|
|
|
|
|
|
|
if (!doc) {
|
|
|
|
|
const { data } = await api.get(`/documents/${routeDocumentId}`);
|
|
|
|
|
const hydratedDetail = assetManager.hydrateDetail(data);
|
|
|
|
|
const fetched = hydratedDetail?.document || data.document || data;
|
|
|
|
|
doc = fetched ? assetManager.hydrateDocument(fetched) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetFolder = doc?.folder_id || routeFolderId || 'root';
|
|
|
|
|
if (targetFolder && targetFolder !== selectedFolder) {
|
|
|
|
|
await loadFolder(targetFolder, { showLoading: false });
|
|
|
|
@@ -3325,12 +3196,15 @@ const AppLayout = () => {
|
|
|
|
|
}, [
|
|
|
|
|
routeDocumentId,
|
|
|
|
|
routeFolderId,
|
|
|
|
|
ensureDocumentDetail,
|
|
|
|
|
api,
|
|
|
|
|
assetManager,
|
|
|
|
|
selectedFolder,
|
|
|
|
|
loadFolder,
|
|
|
|
|
openDocumentPreview,
|
|
|
|
|
previewDocumentId,
|
|
|
|
|
closeDocumentPreview,
|
|
|
|
|
documents,
|
|
|
|
|
searchResults,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
@@ -3346,14 +3220,7 @@ const AppLayout = () => {
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await api.patch(`/documents/${documentId}`, { title: trimmed });
|
|
|
|
|
const hydratedDetail = assetManager.hydrateDetail(data);
|
|
|
|
|
const hydratedDocument = hydratedDetail?.document || data.document;
|
|
|
|
|
|
|
|
|
|
setDocumentDetails((prev) => {
|
|
|
|
|
const next = new Map(prev);
|
|
|
|
|
next.set(documentId, hydratedDetail);
|
|
|
|
|
documentDetailsRef.current = next;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
const hydratedDocument = hydratedDetail?.document || data.document || data;
|
|
|
|
|
|
|
|
|
|
setDocuments((prev) =>
|
|
|
|
|
prev.map((doc) => {
|
|
|
|
@@ -3390,7 +3257,11 @@ const AppLayout = () => {
|
|
|
|
|
next.set(key, {
|
|
|
|
|
...contents,
|
|
|
|
|
documents: contents.documents.map((doc) =>
|
|
|
|
|
doc.id === documentId ? { ...doc, title: trimmed } : doc,
|
|
|
|
|
doc.id === documentId
|
|
|
|
|
? hydratedDocument
|
|
|
|
|
? { ...doc, ...hydratedDocument }
|
|
|
|
|
: { ...doc, title: trimmed }
|
|
|
|
|
: doc,
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
@@ -3421,16 +3292,12 @@ const AppLayout = () => {
|
|
|
|
|
try {
|
|
|
|
|
await api.delete(`/documents/${documentId}/tags/${tagId}`);
|
|
|
|
|
setStatusMessage('Tag removed.', 'success');
|
|
|
|
|
await Promise.all([
|
|
|
|
|
refreshCurrentFolder(),
|
|
|
|
|
refreshTags(),
|
|
|
|
|
ensureDocumentDetail(documentId, { force: true }),
|
|
|
|
|
]);
|
|
|
|
|
await Promise.all([refreshCurrentFolder(), refreshTags()]);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notifyApiError(error, 'Failed to remove tag.');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[ensureDocumentDetail, refreshCurrentFolder, refreshTags, notifyApiError, setStatusMessage],
|
|
|
|
|
[refreshCurrentFolder, refreshTags, notifyApiError, setStatusMessage],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleTagAdd = useCallback(
|
|
|
|
@@ -3438,17 +3305,17 @@ const AppLayout = () => {
|
|
|
|
|
let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
|
|
|
|
|
try {
|
|
|
|
|
if (!tag) {
|
|
|
|
|
const { data } = await api.post('/tags', { label, color: null });
|
|
|
|
|
const { data } = await api.post('/tags', {
|
|
|
|
|
label,
|
|
|
|
|
color: generateRandomTagColor(),
|
|
|
|
|
});
|
|
|
|
|
tag = data;
|
|
|
|
|
await refreshTags();
|
|
|
|
|
}
|
|
|
|
|
await api.post(`/documents/${document.id}/tags`, { tag_ids: [tag.id] });
|
|
|
|
|
setStatusMessage('Tag assigned.', 'success');
|
|
|
|
|
input.value = '';
|
|
|
|
|
await Promise.all([
|
|
|
|
|
refreshCurrentFolder(),
|
|
|
|
|
ensureDocumentDetail(document.id, { force: true }),
|
|
|
|
|
]);
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
notifyApiError(error, 'Failed to assign tag.');
|
|
|
|
|
}
|
|
|
|
@@ -3457,7 +3324,6 @@ const AppLayout = () => {
|
|
|
|
|
tags,
|
|
|
|
|
refreshTags,
|
|
|
|
|
refreshCurrentFolder,
|
|
|
|
|
ensureDocumentDetail,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
setStatusMessage,
|
|
|
|
|
],
|
|
|
|
@@ -3472,10 +3338,7 @@ const AppLayout = () => {
|
|
|
|
|
try {
|
|
|
|
|
await api.post(`/documents/${documentId}/tags`, { tag_ids: [tagId] });
|
|
|
|
|
setStatusMessage('Tag assigned.', 'success');
|
|
|
|
|
await Promise.all([
|
|
|
|
|
refreshCurrentFolder(),
|
|
|
|
|
ensureDocumentDetail(documentId, { force: true }),
|
|
|
|
|
]);
|
|
|
|
|
await refreshCurrentFolder();
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error.response?.data?.error || 'Failed to assign tag.';
|
|
|
|
@@ -3483,7 +3346,7 @@ const AppLayout = () => {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[api, refreshCurrentFolder, ensureDocumentDetail, notifyApiError, setStatusMessage],
|
|
|
|
|
[api, refreshCurrentFolder, notifyApiError, setStatusMessage],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleFolderDelete = useCallback(
|
|
|
|
@@ -3532,10 +3395,7 @@ const AppLayout = () => {
|
|
|
|
|
const parentId = node?.parentId || 'root';
|
|
|
|
|
setSelectedFolder(parentId);
|
|
|
|
|
const parentContents = await ensureFolderData(parentId, { force: true });
|
|
|
|
|
const nextSelectedId = applySelectedFolder(parentId, parentContents);
|
|
|
|
|
if (nextSelectedId) {
|
|
|
|
|
await ensureDocumentDetail(nextSelectedId, { force: true });
|
|
|
|
|
}
|
|
|
|
|
applySelectedFolder(parentId, parentContents);
|
|
|
|
|
} else if (selectedFolder !== 'root') {
|
|
|
|
|
await ensureFolderData(selectedFolder, { force: true });
|
|
|
|
|
}
|
|
|
|
@@ -3553,7 +3413,6 @@ const AppLayout = () => {
|
|
|
|
|
selectedFolder,
|
|
|
|
|
folderNodes,
|
|
|
|
|
applySelectedFolder,
|
|
|
|
|
ensureDocumentDetail,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
setStatusMessage,
|
|
|
|
|
],
|
|
|
|
@@ -3750,10 +3609,7 @@ const AppLayout = () => {
|
|
|
|
|
|
|
|
|
|
selectionAnchorRef.current = targetId;
|
|
|
|
|
|
|
|
|
|
if (!cancelled && targetId) {
|
|
|
|
|
const cached = documentDetailsRef.current.get(targetId);
|
|
|
|
|
await ensureDocumentDetail(targetId, { force: !cached });
|
|
|
|
|
}
|
|
|
|
|
// rely on hydrated search results; assets refresh on demand
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
notifyApiError(error, 'Search failed. Please try again.');
|
|
|
|
@@ -3778,7 +3634,6 @@ const AppLayout = () => {
|
|
|
|
|
searchQuery,
|
|
|
|
|
activeTagFilters,
|
|
|
|
|
selectedFolder,
|
|
|
|
|
ensureDocumentDetail,
|
|
|
|
|
notifyApiError,
|
|
|
|
|
assetManager,
|
|
|
|
|
]);
|
|
|
|
@@ -4048,11 +3903,6 @@ const AppLayout = () => {
|
|
|
|
|
: 'root';
|
|
|
|
|
}, [folderOptions, selectedFolder]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!focusedDocumentId) return;
|
|
|
|
|
ensureDocumentDetail(focusedDocumentId, { force: false }).catch(() => {});
|
|
|
|
|
}, [focusedDocumentId, ensureDocumentDetail]);
|
|
|
|
|
|
|
|
|
|
const selectedDocument = useMemo(() => {
|
|
|
|
|
if (!focusedDocumentId) {
|
|
|
|
|
return null;
|
|
|
|
@@ -4061,10 +3911,6 @@ const AppLayout = () => {
|
|
|
|
|
return list.find((doc) => doc.id === focusedDocumentId) || null;
|
|
|
|
|
}, [searchResults, documents, focusedDocumentId]);
|
|
|
|
|
|
|
|
|
|
const selectedDetail = selectedDocument
|
|
|
|
|
? documentDetails.get(selectedDocument.id)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
const documentLookup = useMemo(() => {
|
|
|
|
|
const map = new Map();
|
|
|
|
|
documents.forEach((doc) => {
|
|
|
|
@@ -4090,7 +3936,7 @@ const AppLayout = () => {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const doc = documentLookup.get(documentId) || documentDetails.get(documentId)?.document || null;
|
|
|
|
|
const doc = documentLookup.get(documentId) || null;
|
|
|
|
|
const label = doc?.title || doc?.original_name || 'this document';
|
|
|
|
|
|
|
|
|
|
const confirmed = window.confirm(`Delete "${label}"? This action cannot be undone.`);
|
|
|
|
@@ -4106,16 +3952,6 @@ const AppLayout = () => {
|
|
|
|
|
|
|
|
|
|
setSearchResults((prev) => (prev ? prev.filter((item) => item.id !== documentId) : null));
|
|
|
|
|
|
|
|
|
|
setDocumentDetails((prev) => {
|
|
|
|
|
if (!prev.has(documentId)) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
const next = new Map(prev);
|
|
|
|
|
next.delete(documentId);
|
|
|
|
|
documentDetailsRef.current = next;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setFolderContents((prev) => {
|
|
|
|
|
let changed = false;
|
|
|
|
|
const next = new Map();
|
|
|
|
@@ -4166,11 +4002,9 @@ const AppLayout = () => {
|
|
|
|
|
api,
|
|
|
|
|
token,
|
|
|
|
|
documentLookup,
|
|
|
|
|
documentDetails,
|
|
|
|
|
setStatusMessage,
|
|
|
|
|
setDocuments,
|
|
|
|
|
setSearchResults,
|
|
|
|
|
setDocumentDetails,
|
|
|
|
|
setFolderContents,
|
|
|
|
|
setPreviewEntries,
|
|
|
|
|
previewInflightRef,
|
|
|
|
@@ -4193,50 +4027,18 @@ const AppLayout = () => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
selectionOrder.forEach((id) => {
|
|
|
|
|
const doc = documentLookup.get(id) || documentDetails.get(id)?.document || null;
|
|
|
|
|
const doc = documentLookup.get(id) || null;
|
|
|
|
|
pushDoc(doc);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
selectedDocumentIds.forEach((id) => {
|
|
|
|
|
if (seen.has(id)) return;
|
|
|
|
|
const doc = documentLookup.get(id) || documentDetails.get(id)?.document || null;
|
|
|
|
|
const doc = documentLookup.get(id) || null;
|
|
|
|
|
pushDoc(doc);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return ordered;
|
|
|
|
|
}, [selectionOrder, documentLookup, selectedDocumentIds, documentDetails]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!selectedDocumentIds.length) return;
|
|
|
|
|
const selectedSet = new Set(selectedDocumentIds);
|
|
|
|
|
const targetIds = [];
|
|
|
|
|
|
|
|
|
|
for (let index = selectionOrder.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const id = selectionOrder[index];
|
|
|
|
|
if (selectedSet.has(id) && !targetIds.includes(id)) {
|
|
|
|
|
targetIds.push(id);
|
|
|
|
|
}
|
|
|
|
|
if (targetIds.length >= MAX_PREVIEW_STACK_ITEMS) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!targetIds.length) {
|
|
|
|
|
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
const id = selectedDocumentIds[index];
|
|
|
|
|
if (!targetIds.includes(id)) {
|
|
|
|
|
targetIds.push(id);
|
|
|
|
|
}
|
|
|
|
|
if (targetIds.length >= MAX_PREVIEW_STACK_ITEMS) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetIds.forEach((id) => {
|
|
|
|
|
ensureDocumentDetail(id, { force: false }).catch(() => {});
|
|
|
|
|
});
|
|
|
|
|
}, [selectionOrder, selectedDocumentIds, ensureDocumentDetail]);
|
|
|
|
|
}, [selectionOrder, documentLookup, selectedDocumentIds]);
|
|
|
|
|
|
|
|
|
|
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
|
|
|
|
|
const chain = [];
|
|
|
|
@@ -4332,16 +4134,11 @@ const AppLayout = () => {
|
|
|
|
|
return previewEntries.get(previewDocumentId) || null;
|
|
|
|
|
}, [previewDocumentId, previewEntries]);
|
|
|
|
|
|
|
|
|
|
const previewWorkspaceDetail = previewDocumentId
|
|
|
|
|
? documentDetails.get(previewDocumentId)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
const previewWorkspaceDocument = useMemo(() => {
|
|
|
|
|
if (!previewDocumentId) return null;
|
|
|
|
|
if (previewWorkspaceDetail?.document) return previewWorkspaceDetail.document;
|
|
|
|
|
const pool = searchResults ?? documents;
|
|
|
|
|
return pool.find((doc) => doc.id === previewDocumentId) || null;
|
|
|
|
|
}, [previewDocumentId, previewWorkspaceDetail, searchResults, documents]);
|
|
|
|
|
}, [previewDocumentId, searchResults, documents]);
|
|
|
|
|
|
|
|
|
|
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
|
|
|
|
|
|
|
|
|
@@ -4416,7 +4213,6 @@ const AppLayout = () => {
|
|
|
|
|
|
|
|
|
|
const detailPanelProps = {
|
|
|
|
|
selectedDocuments: orderedSelectedDocuments,
|
|
|
|
|
detailMap: documentDetails,
|
|
|
|
|
tags,
|
|
|
|
|
tagLookupById,
|
|
|
|
|
tagLookupByLabel,
|
|
|
|
@@ -4489,7 +4285,6 @@ const AppLayout = () => {
|
|
|
|
|
handleDocumentTagAttach,
|
|
|
|
|
previewActive,
|
|
|
|
|
previewWorkspaceDocument,
|
|
|
|
|
previewWorkspaceDetail,
|
|
|
|
|
previewWorkspaceEntry,
|
|
|
|
|
closeDocumentPreview,
|
|
|
|
|
handleThumbnailRegeneration,
|
|
|
|
@@ -4515,7 +4310,6 @@ const AppLayout = () => {
|
|
|
|
|
handleDocumentTagAttach,
|
|
|
|
|
previewActive,
|
|
|
|
|
previewWorkspaceDocument,
|
|
|
|
|
previewWorkspaceDetail,
|
|
|
|
|
previewWorkspaceEntry,
|
|
|
|
|
closeDocumentPreview,
|
|
|
|
|
handleThumbnailRegeneration,
|
|
|
|
@@ -4643,7 +4437,6 @@ const DocumentsRoute = () => {
|
|
|
|
|
sidebarProps,
|
|
|
|
|
previewActive,
|
|
|
|
|
previewWorkspaceDocument,
|
|
|
|
|
previewWorkspaceDetail,
|
|
|
|
|
previewWorkspaceEntry,
|
|
|
|
|
closeDocumentPreview,
|
|
|
|
|
handleThumbnailRegeneration,
|
|
|
|
@@ -4658,7 +4451,6 @@ const DocumentsRoute = () => {
|
|
|
|
|
<main className="preview-main">
|
|
|
|
|
<PreviewWorkspace
|
|
|
|
|
document={previewWorkspaceDocument}
|
|
|
|
|
detail={previewWorkspaceDetail}
|
|
|
|
|
previewEntry={previewWorkspaceEntry}
|
|
|
|
|
onClose={closeDocumentPreview}
|
|
|
|
|
onRegenerateThumbnails={handleThumbnailRegeneration}
|
|
|
|
|