7
ci / docker (backend, backend/Dockerfile, backend) (push) Successful in 12m50s
ci / docker (frontend, frontend/Dockerfile, frontend) (push) Successful in 12m49s

This commit is contained in:
2025-10-14 02:43:38 +02:00
parent c17a9484ca
commit 9aa43d2753
5 changed files with 183 additions and 298 deletions
+1 -1
View File
@@ -53,13 +53,13 @@ pub struct Document {
pub original_name: String, pub original_name: String,
pub content_type: Option<String>, pub content_type: Option<String>,
pub folder_id: Option<Uuid>, pub folder_id: Option<Uuid>,
pub current_version_id: Uuid,
pub uploaded_at: NaiveDateTime, pub uploaded_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub deleted_at: Option<NaiveDateTime>, pub deleted_at: Option<NaiveDateTime>,
pub metadata: serde_json::Value, pub metadata: serde_json::Value,
pub issued_at: Option<NaiveDateTime>, pub issued_at: Option<NaiveDateTime>,
pub title: String, pub title: String,
pub current_version_id: Uuid,
} }
#[derive(Debug, Insertable)] #[derive(Debug, Insertable)]
+22 -53
View File
@@ -17,14 +17,34 @@ export const getAssetFromVersion = (currentVersion, assetType) => {
return getAssetFromGroup(currentVersion.assets, assetType); return getAssetFromGroup(currentVersion.assets, assetType);
}; };
export const resolveDocumentAssetUrl = (doc, type, { ensureAssetUrl, getAsset, ensureOptions } = {}) => {
if (!doc || !type) {
return null;
}
const asset = typeof getAsset === 'function' ? getAsset(doc, type) : null;
if (!asset) {
return null;
}
const now = Date.now();
const expiresAt = typeof asset.expiresAt === 'number' ? asset.expiresAt : null;
const hasFreshUrl = asset.url && (!expiresAt || expiresAt > now);
if (hasFreshUrl) {
return asset.url;
}
if (doc.id && asset.id && typeof ensureAssetUrl === 'function') {
const force = Boolean(asset.url && expiresAt && expiresAt <= now);
const options = ensureOptions ? { ...ensureOptions, force } : { force };
ensureAssetUrl(doc.id, asset, options).catch(() => {});
}
return null;
};
class AssetManager { class AssetManager {
constructor({ api, assetPresignTtlMs }) { constructor({ api, assetPresignTtlMs }) {
this.api = api; this.api = api;
this.assetPresignTtlMs = assetPresignTtlMs; this.assetPresignTtlMs = assetPresignTtlMs;
this.assetCache = new Map(); this.assetCache = new Map();
this.assetInflight = new Map(); this.assetInflight = new Map();
this.previewCache = new Map();
this.previewInflight = new Map();
} }
setApi(api) { setApi(api) {
@@ -197,60 +217,9 @@ class AssetManager {
return request; return request;
} }
ensurePreview(documentId, { force = false } = {}) {
if (!documentId) {
return Promise.resolve(null);
}
const cached = this.previewCache.get(documentId);
const now = Date.now();
if (!force && cached && (!cached.expiresAt || cached.expiresAt > now)) {
return Promise.resolve(cached);
}
if (!force && this.previewInflight.has(documentId)) {
return this.previewInflight.get(documentId);
}
if (!this.api) {
return Promise.reject(new Error('AssetManager API client is not configured.'));
}
const request = this.api
.get(`/documents/${documentId}/download`)
.then(({ data }) => {
const ttl = data.expires_in ? Math.max(data.expires_in - 60, 30) * 1000 : 5 * 60 * 1000;
const entry = {
url: data.url,
contentType: data.content_type || null,
filename: data.filename,
expiresAt: Date.now() + ttl,
};
this.previewCache.set(documentId, entry);
return entry;
})
.finally(() => {
this.previewInflight.delete(documentId);
});
this.previewInflight.set(documentId, request);
return request;
}
getPreview(documentId) {
return this.previewCache.get(documentId) || null;
}
deleteDocument(documentId) {
if (!documentId) return;
this.previewCache.delete(documentId);
this.previewInflight.delete(documentId);
}
reset() { reset() {
this.assetCache.clear(); this.assetCache.clear();
this.assetInflight.clear(); this.assetInflight.clear();
this.previewCache.clear();
this.previewInflight.clear();
} }
} }
+88 -92
View File
@@ -21,7 +21,7 @@ import {
matchPath, matchPath,
} from 'react-router-dom'; } from 'react-router-dom';
import './styles.css'; import './styles.css';
import AssetManager, { getAssetFromVersion } from './asset_manager'; import AssetManager, { getAssetFromVersion, resolveDocumentAssetUrl } from './asset_manager';
import useApiError from './hooks/useApiError'; import useApiError from './hooks/useApiError';
import SkeuomorphicWorkspace from './skeuomorphic_ws'; import SkeuomorphicWorkspace from './skeuomorphic_ws';
@@ -502,25 +502,20 @@ const FilterBar = ({
</div> </div>
); );
const DocumentThumbnailImage = ({ documentId, asset, ensureAssetUrl, alt }) => { const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt }) => {
useEffect(() => { const url = useMemo(
if (!documentId || !asset?.id || !ensureAssetUrl) { () =>
return; resolveDocumentAssetUrl(document, 'thumbnail', {
} ensureAssetUrl,
const expiresAt = typeof asset.expiresAt === 'number' ? asset.expiresAt : null; getAsset: getDocumentAsset,
const now = Date.now(); }),
const needsRefresh = !asset?.url || (expiresAt && expiresAt <= now); [document, ensureAssetUrl, getDocumentAsset],
if (!needsRefresh) { );
return;
}
const force = Boolean(asset.url && expiresAt && expiresAt <= now);
ensureAssetUrl(documentId, asset, { force }).catch(() => {});
}, [documentId, asset?.id, asset?.url, asset?.expiresAt, ensureAssetUrl]);
if (asset?.url) { if (url) {
return ( return (
<img <img
src={asset.url} src={url}
alt={alt || ''} alt={alt || ''}
className="document-thumbnail" className="document-thumbnail"
/> />
@@ -567,10 +562,6 @@ const DocumentsTable = ({
const showingSearchResults = searchResults !== null; const showingSearchResults = searchResults !== null;
const rows = showingSearchResults ? searchResults : documents; const rows = showingSearchResults ? searchResults : documents;
const resolveThumbnailAsset = useCallback((doc) => {
if (!doc) return null;
return getDocumentAsset(doc, 'thumbnail');
}, [getDocumentAsset]);
const selectedSet = useMemo( const selectedSet = useMemo(
() => new Set(selectedDocumentIds), () => new Set(selectedDocumentIds),
[selectedDocumentIds], [selectedDocumentIds],
@@ -775,8 +766,6 @@ const DocumentsTable = ({
rowClasses.push('dragging'); rowClasses.push('dragging');
} }
const thumbnailAsset = resolveThumbnailAsset(doc);
return ( return (
<tr <tr
key={doc.id} key={doc.id}
@@ -799,16 +788,12 @@ const DocumentsTable = ({
onDragEnd={onDocumentDragEnd} onDragEnd={onDocumentDragEnd}
> >
<td className="thumb-cell"> <td className="thumb-cell">
{thumbnailAsset ? (
<DocumentThumbnailImage <DocumentThumbnailImage
documentId={doc.id} document={doc}
asset={thumbnailAsset}
ensureAssetUrl={ensureAssetUrl} ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
alt={`Thumbnail for ${doc.title || doc.original_name}`} alt={`Thumbnail for ${doc.title || doc.original_name}`}
/> />
) : (
<div className="thumb-placeholder">DOC</div>
)}
</td> </td>
<td> <td>
<div className="doc-name"> <div className="doc-name">
@@ -1076,20 +1061,19 @@ const DetailPanel = ({
[onPromoteSelection], [onPromoteSelection],
); );
const resolvePreviewAsset = useCallback((doc) => { const makePreviewItem = useCallback(
(doc) => {
if (!doc) return null; if (!doc) return null;
return getDocumentAsset(doc, 'preview'); const url = resolveDocumentAssetUrl(doc, 'preview', {
}, [getDocumentAsset]); ensureAssetUrl,
getAsset: getDocumentAsset,
const makePreviewItem = useCallback((doc) => { });
if (!doc) return null; if (!url) {
const previewAsset = resolvePreviewAsset(doc);
if (!previewAsset?.url) {
return null; return null;
} }
const url = previewAsset.url; const asset = getDocumentAsset(doc, 'preview');
const width = Number(previewAsset?.metadata?.width) || 0; const width = Number(asset?.metadata?.width) || 0;
const height = Number(previewAsset?.metadata?.height) || 0; const height = Number(asset?.metadata?.height) || 0;
const orientation = width > 0 && height > 0 ? (width >= height ? 'landscape' : 'portrait') : 'landscape'; const orientation = width > 0 && height > 0 ? (width >= height ? 'landscape' : 'portrait') : 'landscape';
return { return {
id: doc.id, id: doc.id,
@@ -1097,7 +1081,9 @@ const DetailPanel = ({
orientation, orientation,
alt: doc.title || doc.original_name || 'Document preview', alt: doc.title || doc.original_name || 'Document preview',
}; };
}, [resolvePreviewAsset]); },
[ensureAssetUrl, getDocumentAsset],
);
const stackDocuments = useMemo(() => { const stackDocuments = useMemo(() => {
if (!selectedDocuments.length) return []; if (!selectedDocuments.length) return [];
@@ -1135,24 +1121,12 @@ const DetailPanel = ({
} }
stackDocuments.forEach((doc) => { stackDocuments.forEach((doc) => {
const docId = doc?.id; resolveDocumentAssetUrl(doc, 'preview', {
if (!docId) { ensureAssetUrl,
return; getAsset: getDocumentAsset,
}
const asset = resolvePreviewAsset(doc);
if (!asset?.id) {
return;
}
const expiresAt = typeof asset.expiresAt === 'number' ? asset.expiresAt : null;
const now = Date.now();
const needsRefresh = !asset.url || (expiresAt && expiresAt <= now);
if (!needsRefresh) {
return;
}
const force = Boolean(asset.url && expiresAt && expiresAt <= now);
ensureAssetUrl(docId, asset, { force }).catch(() => {});
}); });
}, [stackDocuments, resolvePreviewAsset, ensureAssetUrl]); });
}, [stackDocuments, ensureAssetUrl, getDocumentAsset]);
const commonTags = useMemo(() => { const commonTags = useMemo(() => {
if (selectedCount < 2) return []; if (selectedCount < 2) return [];
@@ -2121,7 +2095,8 @@ const AppLayout = () => {
setPreviewDocumentId(null); setPreviewDocumentId(null);
setPreviewDocumentLoading(false); setPreviewDocumentLoading(false);
assetManager.reset(); assetManager.reset();
setPreviewCacheTick(0); setPreviewEntries(() => new Map());
previewInflightRef.current = new Map();
dragCounterRef.current = 0; dragCounterRef.current = 0;
breadcrumbFetchRef.current = new Set(); breadcrumbFetchRef.current = new Set();
bootstrapInitializedRef.current = false; bootstrapInitializedRef.current = false;
@@ -3367,7 +3342,8 @@ const AppLayout = () => {
); );
const folderPathCacheRef = useRef(new Map()); const folderPathCacheRef = useRef(new Map());
const [previewCacheTick, setPreviewCacheTick] = useState(0); const [previewEntries, setPreviewEntries] = useState(() => new Map());
const previewInflightRef = useRef(new Map());
const ensureFolderPathOnServer = useCallback( const ensureFolderPathOnServer = useCallback(
async (baseFolderId, segments) => { async (baseFolderId, segments) => {
@@ -3452,18 +3428,45 @@ const AppLayout = () => {
async (documentId, { force = false } = {}) => { async (documentId, { force = false } = {}) => {
if (!documentId) return null; if (!documentId) return null;
try { const existing = previewEntries.get(documentId) || null;
const entry = await assetManager.ensurePreview(documentId, { force }); const now = Date.now();
if (entry) { const expiresAt = typeof existing?.expiresAt === 'number' ? existing.expiresAt : null;
setPreviewCacheTick((prev) => prev + 1); if (!force && existing && (!expiresAt || expiresAt > now)) {
return existing;
} }
if (!force && previewInflightRef.current.has(documentId)) {
return previewInflightRef.current.get(documentId);
}
const request = (async () => {
try {
const { data } = await api.get(`/documents/${documentId}/download`);
const ttl = data.expires_in ? Math.max(data.expires_in - 60, 30) * 1000 : 5 * 60 * 1000;
const entry = {
url: data.url,
contentType: data.content_type || null,
filename: data.filename,
expiresAt: Date.now() + ttl,
};
setPreviewEntries((prev) => {
const next = new Map(prev);
next.set(documentId, entry);
return next;
});
return entry; return entry;
} catch (error) { } catch (error) {
notifyApiError(error, 'Unable to fetch document preview.'); notifyApiError(error, 'Unable to fetch document preview.');
throw error; throw error;
} finally {
previewInflightRef.current.delete(documentId);
} }
})();
previewInflightRef.current.set(documentId, request);
return request;
}, },
[assetManager, notifyApiError], [previewEntries, notifyApiError],
); );
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => { const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
@@ -4771,8 +4774,15 @@ const AppLayout = () => {
return changed ? next : prev; return changed ? next : prev;
}); });
assetManager.deleteDocument(documentId); setPreviewEntries((prev) => {
setPreviewCacheTick((prev) => prev + 1); if (!prev.has(documentId)) {
return prev;
}
const next = new Map(prev);
next.delete(documentId);
return next;
});
previewInflightRef.current.delete(documentId);
if (selectedDocumentIds.includes(documentId)) { if (selectedDocumentIds.includes(documentId)) {
const remaining = selectedDocumentIds.filter((id) => id !== documentId); const remaining = selectedDocumentIds.filter((id) => id !== documentId);
@@ -4801,8 +4811,8 @@ const AppLayout = () => {
setSearchResults, setSearchResults,
setDocumentDetails, setDocumentDetails,
setFolderContents, setFolderContents,
assetManager, setPreviewEntries,
setPreviewCacheTick, previewInflightRef,
previewDocumentId, previewDocumentId,
closeDocumentPreview, closeDocumentPreview,
selectedDocumentIds, selectedDocumentIds,
@@ -4951,15 +4961,15 @@ const AppLayout = () => {
if (!selectedDocument) { if (!selectedDocument) {
return null; return null;
} }
return assetManager.getPreview(selectedDocument.id); return previewEntries.get(selectedDocument.id) || null;
}, [assetManager, selectedDocument, previewCacheTick]); }, [selectedDocument, previewEntries]);
const previewWorkspaceEntry = useMemo(() => { const previewWorkspaceEntry = useMemo(() => {
if (!previewDocumentId) { if (!previewDocumentId) {
return null; return null;
} }
return assetManager.getPreview(previewDocumentId); return previewEntries.get(previewDocumentId) || null;
}, [assetManager, previewDocumentId, previewCacheTick]); }, [previewDocumentId, previewEntries]);
const previewWorkspaceDetail = previewDocumentId const previewWorkspaceDetail = previewDocumentId
? documentDetails.get(previewDocumentId) ? documentDetails.get(previewDocumentId)
@@ -4992,23 +5002,11 @@ const AppLayout = () => {
}; };
const resolveThumbnailUrlForDoc = useCallback( const resolveThumbnailUrlForDoc = useCallback(
(doc) => { (doc) =>
if (!doc) { resolveDocumentAssetUrl(doc, 'thumbnail', {
return null; ensureAssetUrl,
} getAsset: getDocumentAsset,
const asset = getDocumentAsset(doc, 'thumbnail'); }),
const now = Date.now();
const assetExpiresAt = typeof asset?.expiresAt === 'number' ? asset.expiresAt : null;
if (asset?.url && (!assetExpiresAt || assetExpiresAt > now)) {
return asset.url;
}
if (doc.id && asset?.id) {
ensureAssetUrl(doc.id, asset, { force: Boolean(assetExpiresAt && assetExpiresAt <= now) }).catch(
() => {},
);
}
return null;
},
[ensureAssetUrl, getDocumentAsset], [ensureAssetUrl, getDocumentAsset],
); );
@@ -5088,7 +5086,6 @@ const AppLayout = () => {
onCreateTag: handleTagCreate, onCreateTag: handleTagCreate,
onAssignTagToDocument: handleDocumentTagAttach, onAssignTagToDocument: handleDocumentTagAttach,
onRemoveTagFromDocument: handleTagRemove, onRemoveTagFromDocument: handleTagRemove,
resolveAssetPath: resolveApiPath,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
}), }),
@@ -5105,7 +5102,6 @@ const AppLayout = () => {
handleTagCreate, handleTagCreate,
handleDocumentTagAttach, handleDocumentTagAttach,
handleTagRemove, handleTagRemove,
resolveApiPath,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
], ],
+6 -35
View File
@@ -11,24 +11,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
--desk-1: #f0e2c6;
--desk-2: #e7d2ad;
--desk-3: #d8b98a;
--desk-grain-dark: rgba(110, 86, 48, 0.08);
--desk-grain-fine: rgba(86, 66, 33, 0.04);
--desk-vignette: rgba(60, 40, 18, 0.15);
background:
radial-gradient(120% 85% at 50% -10%, var(--desk-vignette), #0000 48%),
linear-gradient(7deg, #0000 0 32%, var(--desk-grain-dark) 60%, #0000 85%),
repeating-linear-gradient(
7deg,
var(--desk-grain-fine) 0 2px,
#0000 2px 9px
),
linear-gradient(90deg, var(--desk-1), var(--desk-2) 48%, var(--desk-3) 100%);
background-size: 100% 100%, 100% 100%, 100% 100%, 100% 100%;
background-blend-mode: multiply, overlay, normal, normal;
position: relative; position: relative;
background: #7a7c7c;
} }
.skeuo-header { .skeuo-header {
@@ -36,7 +20,8 @@
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
padding: 1rem 1.5rem 0.75rem; padding: 0.75rem 1rem 0.5rem;
background-color: var(--surface-subtle, rgba(255, 255, 255, 0.08));
} }
.skeuo-header__meta { .skeuo-header__meta {
@@ -84,13 +69,7 @@
flex: 1; flex: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin: 0 1.5rem 1.5rem; margin: 0;
border-radius: 18px;
background-image: radial-gradient(rgba(255, 255, 255, 0.5) 9%, transparent 9%),
linear-gradient(180deg, rgba(0, 0, 0, 0.05), transparent 32%);
background-size: 46px 46px, 100% 100%;
box-shadow: inset 0 16px 28px rgba(0, 0, 0, 0.07), inset 0 -6px 20px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(6px);
} }
.skeuo-empty { .skeuo-empty {
@@ -144,7 +123,7 @@
} }
.skeuo-item.is-zoomed .skeuo-item__card { .skeuo-item.is-zoomed .skeuo-item__card {
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24); box-shadow: 0 18px 80px rgba(0, 0, 0, 0.8);
} }
.skeuo-item.is-tag-target .skeuo-item__card { .skeuo-item.is-tag-target .skeuo-item__card {
@@ -290,23 +269,15 @@ body.skeuo-cursor-remove * {
.skeuo-item__card { .skeuo-item__card {
position: relative; position: relative;
width: 100%;
height: 100%;
border-radius: 0; border-radius: 0;
background: var(--surface);
border: 1px solid var(--border);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.24); box-shadow: 0 12px 72px rgba(0, 0, 0, 0.25);
overflow: hidden; overflow: hidden;
} }
.skeuo-item__card img { .skeuo-item__card img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
} }
.skeuo-item__placeholder { .skeuo-item__placeholder {
+45 -96
View File
@@ -6,6 +6,7 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { resolveDocumentAssetUrl } from './asset_manager';
import './skeuomorphic_ws.css'; import './skeuomorphic_ws.css';
const ITEM_WIDTH = 220; const ITEM_WIDTH = 220;
@@ -258,31 +259,6 @@ const useDocumentDrag = ({
}; };
}; };
const resolvePreviewDimensions = (doc) => {
if (!doc) return null;
const group = doc?.current_version?.assets || doc?.assets;
let asset = null;
if (group && !Array.isArray(group) && group?.preview) {
asset = group.preview;
} else if (Array.isArray(group)) {
asset = group.find((entry) => entry?.asset_type === 'preview') || null;
}
if (!asset) {
return null;
}
const width = asset?.metadata?.width;
const height = asset?.metadata?.height;
if (Number.isFinite(width) && Number.isFinite(height)) {
return { width, height };
}
return null;
};
const seededRandom = (input) => { const seededRandom = (input) => {
const text = String(input); const text = String(input);
let hash = 2166136261; let hash = 2166136261;
@@ -336,37 +312,6 @@ const getContrastingTextColor = (hex) => {
return luminance > 0.6 ? '#1b1f24' : '#ffffff'; return luminance > 0.6 ? '#1b1f24' : '#ffffff';
}; };
const absolutizeAssetUrl = (input, resolver) => {
if (!input) return null;
if (/^(?:https?:|data:|blob:)/i.test(input)) {
return input;
}
if (typeof resolver === 'function') {
return resolver(input);
}
return input;
};
const resolvePreviewUrl = (doc, resolver) => {
if (!doc) return null;
const group = doc?.current_version?.assets || doc?.assets;
let asset = null;
if (group && !Array.isArray(group) && group?.preview) {
asset = group.preview;
} else if (Array.isArray(group)) {
asset = group.find((entry) => entry?.asset_type === 'preview') || null;
}
if (!asset?.url) {
return null;
}
const now = Date.now();
const expiresAt = typeof asset.expiresAt === 'number' ? asset.expiresAt : null;
if (expiresAt && expiresAt <= now) {
return null;
}
return absolutizeAssetUrl(asset.url, resolver);
};
const SkeuomorphicWorkspace = ({ const SkeuomorphicWorkspace = ({
documents = [], documents = [],
searchResults = null, searchResults = null,
@@ -380,13 +325,13 @@ const SkeuomorphicWorkspace = ({
onCreateTag = null, onCreateTag = null,
onAssignTagToDocument = null, onAssignTagToDocument = null,
onRemoveTagFromDocument = null, onRemoveTagFromDocument = null,
resolveAssetPath = null,
ensureAssetUrl = null, ensureAssetUrl = null,
getDocumentAsset = () => null, getDocumentAsset = () => null,
}) => { }) => {
const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]); const items = useMemo(() => (searchResults ? searchResults : documents), [documents, searchResults]);
const showingSearchResults = searchResults !== null; const showingSearchResults = searchResults !== null;
const containerRef = useRef(null); const containerRef = useRef(null);
const layoutRef = useRef(new Map()); const layoutRef = useRef(new Map());
const itemRefs = useRef(new Map()); const itemRefs = useRef(new Map());
@@ -406,47 +351,51 @@ const SkeuomorphicWorkspace = ({
const docSizeMapRef = useRef(new Map()); const docSizeMapRef = useRef(new Map());
const removalCursorActiveRef = useRef(false); const removalCursorActiveRef = useRef(false);
const resolvePreviewAsset = useCallback(
(doc) => {
if (!doc) return null;
return getDocumentAsset(doc, 'preview');
},
[getDocumentAsset],
);
const resolvePreviewDimensions = useCallback(
(doc) => {
if (!doc) return null;
const asset = resolvePreviewAsset(doc);
const width = asset?.metadata?.width;
const height = asset?.metadata?.height;
if (typeof width === 'number' && typeof height === 'number') {
return { width, height };
}
return null;
},
[resolvePreviewAsset],
);
const resolvePreviewUrl = useCallback(
(doc) => {
if (!doc) return null;
return resolveDocumentAssetUrl(doc, 'preview', {
ensureAssetUrl,
getAsset: getDocumentAsset,
});
},
[ensureAssetUrl, getDocumentAsset],
);
useEffect(() => { useEffect(() => {
if (!ensureAssetUrl) { if (!ensureAssetUrl) {
return; return;
} }
const now = Date.now();
const seen = new Set();
const queueAsset = (docId, asset) => {
if (!docId || !asset?.id || seen.has(asset.id)) {
return;
}
const type = asset.asset_type;
if (type && type !== 'thumbnail' && type !== 'preview') {
return;
}
const expiresAt = typeof asset.expiresAt === 'number' ? asset.expiresAt : null;
const hasFreshUrl = asset.url && (!expiresAt || expiresAt > now);
if (hasFreshUrl) {
return;
}
const force = Boolean(asset.url && expiresAt && expiresAt <= now);
seen.add(asset.id);
ensureAssetUrl(docId, asset, { force }).catch(() => {});
};
items.forEach((doc) => { items.forEach((doc) => {
const docId = doc?.id; resolveDocumentAssetUrl(doc, 'preview', {
if (!docId) { ensureAssetUrl,
return; getAsset: getDocumentAsset,
}
const previewAsset = getDocumentAsset(doc, 'preview');
if (previewAsset) {
queueAsset(docId, previewAsset);
}
const thumbnailAsset = getDocumentAsset(doc, 'thumbnail');
if (thumbnailAsset) {
queueAsset(docId, thumbnailAsset);
}
}); });
}, [items, ensureAssetUrl]); });
}, [items, ensureAssetUrl, getDocumentAsset]);
const focusCanvas = useCallback(() => { const focusCanvas = useCallback(() => {
const canvas = containerRef.current; const canvas = containerRef.current;
@@ -656,7 +605,7 @@ const SkeuomorphicWorkspace = ({
const size = { width, height }; const size = { width, height };
docSizeMapRef.current.set(key, size); docSizeMapRef.current.set(key, size);
return size; return size;
}, []); }, [resolvePreviewDimensions]);
const documentLookup = useMemo(() => { const documentLookup = useMemo(() => {
const map = new Map(); const map = new Map();
@@ -745,7 +694,7 @@ const resolveZoomMetrics = useCallback(
zoomCenterY, zoomCenterY,
}; };
}, },
[canvasSize.width, canvasSize.height], [canvasSize.width, canvasSize.height, resolvePreviewDimensions],
); );
const resolveBaseMetrics = useCallback( const resolveBaseMetrics = useCallback(
@@ -769,7 +718,7 @@ const resolveZoomMetrics = useCallback(
baseScale: 1, baseScale: 1,
}; };
}, },
[], [resolvePreviewDimensions],
); );
const syncLayoutSnapshot = useCallback(() => { const syncLayoutSnapshot = useCallback(() => {
@@ -1396,7 +1345,7 @@ const resolveZoomMetrics = useCallback(
<div <div
className="skeuo-canvas" className="skeuo-canvas"
ref={containerRef} ref={containerRef}
tabIndex={0} tabIndex={1}
data-active-tag={activeShelfTagId || undefined} data-active-tag={activeShelfTagId || undefined}
onDragOver={handleCanvasDragOver} onDragOver={handleCanvasDragOver}
onDragLeave={handleCanvasDragLeave} onDragLeave={handleCanvasDragLeave}
@@ -1455,7 +1404,7 @@ const resolveZoomMetrics = useCallback(
const totalScale = zoomScale > 0 ? zoomScale : 1; const totalScale = zoomScale > 0 ? zoomScale : 1;
const inverseTagScale = totalScale > 0 ? 1 / totalScale : 1; const inverseTagScale = totalScale > 0 ? 1 / totalScale : 1;
const tagsStyle = { '--tag-scale': inverseTagScale }; const tagsStyle = { '--tag-scale': inverseTagScale };
const previewUrl = resolvePreviewUrl(doc, resolveAssetPath); const previewUrl = resolvePreviewUrl(doc);
const imageUrl = previewUrl; const imageUrl = previewUrl;
const title = doc.title || doc.original_name || 'Document'; const title = doc.title || doc.original_name || 'Document';
const dragging = draggingId === doc.id; const dragging = draggingId === doc.id;