backend: generate high resolution preview
This commit is contained in:
+257
-114
@@ -517,6 +517,7 @@ const DocumentsTable = ({
|
||||
onDocumentOpen,
|
||||
selectedDocumentIds,
|
||||
focusedDocumentId,
|
||||
focusedRowKey,
|
||||
draggingDocumentIds = [],
|
||||
onDocumentDragStart,
|
||||
onDocumentDragEnd,
|
||||
@@ -525,6 +526,7 @@ const DocumentsTable = ({
|
||||
tagLookupById,
|
||||
onDocumentListFocus,
|
||||
onDocumentListKeyDown,
|
||||
onFocusedRowChange,
|
||||
}) => {
|
||||
const showingSearchResults = searchResults !== null;
|
||||
const rows = showingSearchResults ? searchResults : documents;
|
||||
@@ -538,10 +540,19 @@ const DocumentsTable = ({
|
||||
);
|
||||
const scrollRef = useRef(null);
|
||||
const ensureFocusedRowVisible = useCallback(() => {
|
||||
if (!focusedDocumentId) return;
|
||||
if (!focusedRowKey) return;
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
const row = container.querySelector(`#document-row-${focusedDocumentId}`);
|
||||
let selector = null;
|
||||
if (focusedRowKey.startsWith('document:')) {
|
||||
selector = `#document-row-${focusedRowKey.slice('document:'.length)}`;
|
||||
} else if (focusedRowKey.startsWith('folder:')) {
|
||||
selector = `#folder-row-${focusedRowKey.slice('folder:'.length)}`;
|
||||
}
|
||||
if (!selector) {
|
||||
return;
|
||||
}
|
||||
const row = container.querySelector(selector);
|
||||
if (!row || !container.contains(row)) {
|
||||
return;
|
||||
}
|
||||
@@ -562,12 +573,23 @@ const DocumentsTable = ({
|
||||
const nextScrollTop = rowBottom - container.clientHeight;
|
||||
container.scrollTop = Math.max(nextScrollTop, 0);
|
||||
}
|
||||
}, [focusedDocumentId]);
|
||||
}, [focusedRowKey]);
|
||||
|
||||
useEffect(() => {
|
||||
ensureFocusedRowVisible();
|
||||
}, [ensureFocusedRowVisible]);
|
||||
|
||||
const activeDescendantId = useMemo(() => {
|
||||
if (!focusedRowKey) return undefined;
|
||||
if (focusedRowKey.startsWith('document:')) {
|
||||
return `document-row-${focusedRowKey.slice('document:'.length)}`;
|
||||
}
|
||||
if (focusedRowKey.startsWith('folder:')) {
|
||||
return `folder-row-${focusedRowKey.slice('folder:'.length)}`;
|
||||
}
|
||||
return undefined;
|
||||
}, [focusedRowKey]);
|
||||
|
||||
return (
|
||||
<section className="documents-panel column">
|
||||
<div className="column-header">
|
||||
@@ -624,9 +646,7 @@ const DocumentsTable = ({
|
||||
onDocumentListKeyDown(event);
|
||||
}
|
||||
}}
|
||||
aria-activedescendant={
|
||||
focusedDocumentId ? `document-row-${focusedDocumentId}` : undefined
|
||||
}
|
||||
aria-activedescendant={activeDescendantId}
|
||||
>
|
||||
{!showingSearchResults && !subfolders.length && rows.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
@@ -651,9 +671,13 @@ const DocumentsTable = ({
|
||||
return (
|
||||
<tr
|
||||
key={folder.id}
|
||||
className={`folder${isDraggingFolder ? ' is-dragging' : ''}`}
|
||||
className={`folder${isDraggingFolder ? ' is-dragging' : ''}${
|
||||
focusedRowKey === `folder:${folder.id}` ? ' focused' : ''
|
||||
}`}
|
||||
id={`folder-row-${folder.id}`}
|
||||
onClick={() => {
|
||||
scrollRef.current?.focus({ preventScroll: true });
|
||||
onFocusedRowChange?.(`folder:${folder.id}`);
|
||||
onFolderSelect(folder.id);
|
||||
}}
|
||||
onDragOver={(event) => onFolderDragOver(event, folder.id)}
|
||||
@@ -698,7 +722,11 @@ const DocumentsTable = ({
|
||||
const isSelected = selectedSet.has(doc.id);
|
||||
const rowClasses = ['document'];
|
||||
if (isSelected) rowClasses.push('selected');
|
||||
if (focusedDocumentId === doc.id) rowClasses.push('focused');
|
||||
if (
|
||||
focusedDocumentId === doc.id || focusedRowKey === `document:${doc.id}`
|
||||
) {
|
||||
rowClasses.push('focused');
|
||||
}
|
||||
if (draggingSet.has(doc.id)) {
|
||||
rowClasses.push('dragging');
|
||||
}
|
||||
@@ -711,6 +739,7 @@ const DocumentsTable = ({
|
||||
aria-selected={isSelected}
|
||||
onClick={(event) => {
|
||||
scrollRef.current?.focus({ preventScroll: true });
|
||||
onFocusedRowChange?.(`document:${doc.id}`);
|
||||
onDocumentRowClick(doc.id, event);
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
@@ -1813,6 +1842,9 @@ const AppLayout = () => {
|
||||
const [selectedDocumentIds, setSelectedDocumentIds] = useState(initialSelection);
|
||||
const [selectionOrder, setSelectionOrder] = useState(initialSelection);
|
||||
const [focusedDocumentId, setFocusedDocumentId] = useState(routeDocumentId);
|
||||
const [focusedRowKey, setFocusedRowKey] = useState(() =>
|
||||
routeDocumentId ? `document:${routeDocumentId}` : null,
|
||||
);
|
||||
const [documentDetails, setDocumentDetails] = useState(() => new Map());
|
||||
const documentDetailsRef = useRef(documentDetails);
|
||||
const tokenRef = useRef(token);
|
||||
@@ -2091,9 +2123,11 @@ const AppLayout = () => {
|
||||
[focusedDocumentId, setSelectionOrder],
|
||||
);
|
||||
|
||||
const showingSearchResults = searchResults !== null;
|
||||
|
||||
const visibleDocuments = useMemo(
|
||||
() => (searchResults !== null ? searchResults : documents),
|
||||
[searchResults, documents],
|
||||
() => (showingSearchResults ? searchResults : documents),
|
||||
[showingSearchResults, searchResults, documents],
|
||||
);
|
||||
|
||||
const visibleDocumentIds = useMemo(
|
||||
@@ -2277,116 +2311,57 @@ const AppLayout = () => {
|
||||
],
|
||||
);
|
||||
|
||||
const handleDocumentListFocus = useCallback(() => {
|
||||
if (focusedDocumentId && visibleDocumentIds.includes(focusedDocumentId)) {
|
||||
const navigableRows = useMemo(() => {
|
||||
const entries = [];
|
||||
if (!showingSearchResults) {
|
||||
currentSubfolders.forEach((folder) => {
|
||||
entries.push({ key: `folder:${folder.id}`, type: 'folder', id: folder.id });
|
||||
});
|
||||
}
|
||||
visibleDocuments.forEach((doc) => {
|
||||
entries.push({ key: `document:${doc.id}`, type: 'document', id: doc.id });
|
||||
});
|
||||
return entries;
|
||||
}, [showingSearchResults, currentSubfolders, visibleDocuments]);
|
||||
|
||||
const navigableRowKeys = useMemo(
|
||||
() => navigableRows.map((entry) => entry.key),
|
||||
[navigableRows],
|
||||
);
|
||||
|
||||
const prevFocusedDocIdRef = useRef(focusedDocumentId);
|
||||
useEffect(() => {
|
||||
const previous = prevFocusedDocIdRef.current;
|
||||
if (previous === focusedDocumentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = selectedDocumentIds[index];
|
||||
if (visibleDocumentIds.includes(candidate)) {
|
||||
setFocusedDocumentId(candidate);
|
||||
return;
|
||||
}
|
||||
prevFocusedDocIdRef.current = focusedDocumentId;
|
||||
if (focusedDocumentId) {
|
||||
setFocusedRowKey(`document:${focusedDocumentId}`);
|
||||
} else {
|
||||
setFocusedRowKey((current) => (current?.startsWith('folder:') ? current : null));
|
||||
}
|
||||
}, [focusedDocumentId]);
|
||||
|
||||
if (visibleDocumentIds.length) {
|
||||
const firstId = visibleDocumentIds[0];
|
||||
applySelection([firstId], { anchor: firstId, interactedIds: [firstId] });
|
||||
useEffect(() => {
|
||||
if (!focusedRowKey) {
|
||||
return;
|
||||
}
|
||||
}, [
|
||||
focusedDocumentId,
|
||||
visibleDocumentIds,
|
||||
selectedDocumentIds,
|
||||
setFocusedDocumentId,
|
||||
applySelection,
|
||||
]);
|
||||
if (navigableRowKeys.includes(focusedRowKey)) {
|
||||
return;
|
||||
}
|
||||
const docKey = focusedDocumentId ? `document:${focusedDocumentId}` : null;
|
||||
if (docKey && navigableRowKeys.includes(docKey)) {
|
||||
setFocusedRowKey(docKey);
|
||||
return;
|
||||
}
|
||||
if (navigableRowKeys.length) {
|
||||
setFocusedRowKey(navigableRowKeys[0]);
|
||||
} else {
|
||||
setFocusedRowKey(null);
|
||||
}
|
||||
}, [focusedRowKey, navigableRowKeys, focusedDocumentId]);
|
||||
|
||||
const handleDocumentListKeyDown = useCallback(
|
||||
(event) => {
|
||||
const { key, shiftKey } = event;
|
||||
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visibleDocumentIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const activeId = (() => {
|
||||
if (focusedDocumentId && visibleDocumentIds.includes(focusedDocumentId)) {
|
||||
return focusedDocumentId;
|
||||
}
|
||||
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = selectedDocumentIds[index];
|
||||
if (visibleDocumentIds.includes(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
let currentIndex = activeId ? visibleDocumentIds.indexOf(activeId) : -1;
|
||||
let nextIndex = currentIndex;
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, visibleDocumentIds.length - 1);
|
||||
} else if (key === 'ArrowUp') {
|
||||
if (currentIndex === -1) {
|
||||
nextIndex = visibleDocumentIds.length - 1;
|
||||
} else {
|
||||
nextIndex = Math.max(currentIndex - 1, 0);
|
||||
}
|
||||
} else if (key === 'Home') {
|
||||
nextIndex = 0;
|
||||
} else if (key === 'End') {
|
||||
nextIndex = visibleDocumentIds.length - 1;
|
||||
}
|
||||
|
||||
if (nextIndex === -1 || nextIndex >= visibleDocumentIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex === currentIndex && key !== 'Home' && key !== 'End') {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = visibleDocumentIds[nextIndex];
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
let anchorId = selectionAnchorRef.current;
|
||||
if (!anchorId || !visibleDocumentIds.includes(anchorId)) {
|
||||
anchorId = activeId || targetId;
|
||||
}
|
||||
|
||||
const anchorIndex = visibleDocumentIds.indexOf(anchorId);
|
||||
const boundedAnchorIndex = anchorIndex === -1 ? nextIndex : anchorIndex;
|
||||
const start = Math.min(boundedAnchorIndex, nextIndex);
|
||||
const end = Math.max(boundedAnchorIndex, nextIndex);
|
||||
const range = visibleDocumentIds.slice(start, end + 1);
|
||||
applySelection(range, { anchor: anchorId, interactedIds: [targetId] });
|
||||
} else {
|
||||
applySelection([targetId], { anchor: targetId, interactedIds: [targetId] });
|
||||
}
|
||||
|
||||
if (focusedDocumentId !== targetId) {
|
||||
setFocusedDocumentId(targetId);
|
||||
}
|
||||
},
|
||||
[
|
||||
visibleDocumentIds,
|
||||
focusedDocumentId,
|
||||
selectedDocumentIds,
|
||||
applySelection,
|
||||
selectionAnchorRef,
|
||||
setFocusedDocumentId,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDocumentDragStart = useCallback(
|
||||
(event, documentId) => {
|
||||
@@ -3523,6 +3498,172 @@ const AppLayout = () => {
|
||||
[ensureDocumentDetail, ensurePreviewUrl, navigate],
|
||||
);
|
||||
|
||||
const handleDocumentListFocus = useCallback(() => {
|
||||
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedKey = null;
|
||||
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = selectedDocumentIds[index];
|
||||
const rowKey = `document:${candidate}`;
|
||||
if (navigableRowKeys.includes(rowKey)) {
|
||||
resolvedKey = rowKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedKey && navigableRows.length) {
|
||||
resolvedKey = navigableRows[0].key;
|
||||
}
|
||||
|
||||
if (!resolvedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocusedRowKey(resolvedKey);
|
||||
|
||||
if (resolvedKey.startsWith('document:')) {
|
||||
const docId = resolvedKey.slice('document:'.length);
|
||||
if (!selectedDocumentIds.includes(docId)) {
|
||||
applySelection([docId], { anchor: docId, interactedIds: [docId] });
|
||||
}
|
||||
}
|
||||
}, [
|
||||
focusedRowKey,
|
||||
navigableRowKeys,
|
||||
selectedDocumentIds,
|
||||
navigableRows,
|
||||
applySelection,
|
||||
]);
|
||||
|
||||
const handleDocumentListKeyDown = useCallback(
|
||||
(event) => {
|
||||
const { key, shiftKey } = event;
|
||||
const triggers = ['ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter', ' ', 'Space', 'Spacebar'];
|
||||
if (!triggers.includes(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigableRows.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
let activeKey =
|
||||
focusedRowKey && navigableRowKeys.includes(focusedRowKey)
|
||||
? focusedRowKey
|
||||
: null;
|
||||
|
||||
if (!activeKey) {
|
||||
for (let index = selectedDocumentIds.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = selectedDocumentIds[index];
|
||||
const rowKey = `document:${candidate}`;
|
||||
if (navigableRowKeys.includes(rowKey)) {
|
||||
activeKey = rowKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeKey) {
|
||||
activeKey = navigableRows[0].key;
|
||||
setFocusedRowKey(activeKey);
|
||||
}
|
||||
|
||||
let currentIndex = navigableRowKeys.indexOf(activeKey);
|
||||
|
||||
if (key === 'Enter' || key === ' ' || key === 'Space' || key === 'Spacebar') {
|
||||
const row = currentIndex === -1 ? navigableRows[0] : navigableRows[currentIndex];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
if (row.type === 'folder') {
|
||||
setFocusedRowKey(`folder:${row.id}`);
|
||||
selectFolder(row.id);
|
||||
} else {
|
||||
setFocusedRowKey(`document:${row.id}`);
|
||||
applySelection([row.id], { anchor: row.id, interactedIds: [row.id] });
|
||||
openDocumentPreview(row.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let nextIndex = currentIndex;
|
||||
|
||||
if (key === 'ArrowDown') {
|
||||
nextIndex = currentIndex === -1 ? 0 : Math.min(currentIndex + 1, navigableRows.length - 1);
|
||||
} else if (key === 'ArrowUp') {
|
||||
nextIndex = currentIndex === -1 ? navigableRows.length - 1 : Math.max(currentIndex - 1, 0);
|
||||
} else if (key === 'Home') {
|
||||
nextIndex = 0;
|
||||
} else if (key === 'End') {
|
||||
nextIndex = navigableRows.length - 1;
|
||||
}
|
||||
|
||||
if (nextIndex === -1 || nextIndex >= navigableRows.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex === currentIndex && key !== 'Home' && key !== 'End') {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRow = navigableRows[nextIndex];
|
||||
if (!targetRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocusedRowKey(targetRow.key);
|
||||
|
||||
if (targetRow.type === 'folder') {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = targetRow.id;
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
let anchorId = selectionAnchorRef.current;
|
||||
if (!anchorId || !visibleDocumentIds.includes(anchorId)) {
|
||||
if (focusedDocumentId && visibleDocumentIds.includes(focusedDocumentId)) {
|
||||
anchorId = focusedDocumentId;
|
||||
} else {
|
||||
anchorId = targetId;
|
||||
}
|
||||
}
|
||||
|
||||
const anchorIndex = visibleDocumentIds.indexOf(anchorId);
|
||||
const targetIndex = visibleDocumentIds.indexOf(targetId);
|
||||
if (anchorIndex !== -1 && targetIndex !== -1) {
|
||||
const start = Math.min(anchorIndex, targetIndex);
|
||||
const end = Math.max(anchorIndex, targetIndex);
|
||||
const range = visibleDocumentIds.slice(start, end + 1);
|
||||
applySelection(range, { anchor: anchorId, interactedIds: [targetId] });
|
||||
} else {
|
||||
applySelection([targetId], { anchor: targetId, interactedIds: [targetId] });
|
||||
}
|
||||
} else {
|
||||
applySelection([targetId], { anchor: targetId, interactedIds: [targetId] });
|
||||
}
|
||||
},
|
||||
[
|
||||
navigableRows,
|
||||
navigableRowKeys,
|
||||
focusedRowKey,
|
||||
selectedDocumentIds,
|
||||
selectFolder,
|
||||
openDocumentPreview,
|
||||
visibleDocumentIds,
|
||||
focusedDocumentId,
|
||||
applySelection,
|
||||
selectionAnchorRef,
|
||||
],
|
||||
);
|
||||
|
||||
const closeDocumentPreview = useCallback(
|
||||
(folderId = null) => {
|
||||
setPreviewDocumentId(null);
|
||||
@@ -4501,6 +4642,7 @@ const AppLayout = () => {
|
||||
onDocumentDelete: handleDocumentDelete,
|
||||
selectedDocumentIds,
|
||||
focusedDocumentId,
|
||||
focusedRowKey,
|
||||
draggingDocumentIds: draggedDocumentIds,
|
||||
onDocumentDragStart: handleDocumentDragStart,
|
||||
onDocumentDragEnd: handleDocumentDragEnd,
|
||||
@@ -4508,6 +4650,7 @@ const AppLayout = () => {
|
||||
tagLookupById,
|
||||
onDocumentListFocus: handleDocumentListFocus,
|
||||
onDocumentListKeyDown: handleDocumentListKeyDown,
|
||||
onFocusedRowChange: setFocusedRowKey,
|
||||
};
|
||||
|
||||
const detailPanelProps = {
|
||||
|
||||
@@ -738,6 +738,11 @@ button.icon-button.ghost:hover:not([disabled]) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.documents-panel tbody tr.folder.focused {
|
||||
box-shadow: inset 2px 0 0 rgba(43, 92, 255, 0.45);
|
||||
background: rgba(43, 92, 255, 0.08);
|
||||
}
|
||||
|
||||
.documents-panel tbody tr.document {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user