frontend
This commit is contained in:
+318
-124
@@ -313,6 +313,7 @@ const createRootNode = () => ({
|
||||
children: [],
|
||||
expanded: true,
|
||||
loaded: false,
|
||||
hasChildren: false,
|
||||
});
|
||||
|
||||
const StatusBanner = ({ status }) => {
|
||||
@@ -421,7 +422,9 @@ const AppLayout = () => {
|
||||
const routeFolderId = folderMatch?.params?.folderId || null;
|
||||
const routeDocumentId = docMatch?.params?.documentId || null;
|
||||
const previewDocumentId = routeDocumentId;
|
||||
const { status: appStatus, token } = appState;
|
||||
const { status: appStatus, token, tenant, tenants: tenantOptions = [] } = appState;
|
||||
const tenantSlug = tenant?.slug || null;
|
||||
const currentTenantId = tenant?.tenant_id || null;
|
||||
const [status, setStatus] = useState(null);
|
||||
const setStatusMessage = useCallback((message, variant = 'info') => {
|
||||
setStatus(message ? { message, variant } : null);
|
||||
@@ -475,6 +478,7 @@ const AppLayout = () => {
|
||||
const refreshPromiseRef = useRef(null);
|
||||
const breadcrumbFetchRef = useRef(new Set());
|
||||
const tagRemovalCursorActiveRef = useRef(false);
|
||||
const tenantIdRef = useRef(currentTenantId);
|
||||
const setTagRemovalCursor = useCallback((active) => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
@@ -584,6 +588,7 @@ const AppLayout = () => {
|
||||
}
|
||||
const tagManager = tagManagerRef.current;
|
||||
|
||||
|
||||
const buildTagPayload = useCallback(
|
||||
({ label, color } = {}) => tagManager.buildPayload({ label, color }),
|
||||
[tagManager],
|
||||
@@ -680,7 +685,6 @@ const AppLayout = () => {
|
||||
const bootstrapInitializedRef = useRef(false);
|
||||
const selectionInitializedRef = useRef(false);
|
||||
const dragCounterRef = useRef(0);
|
||||
const prefetchedFoldersRef = useRef(new Set(['root']));
|
||||
const detailFolderFetchRef = useRef(new Set());
|
||||
const selectionAnchorRef = useRef(initialRowSelection[initialRowSelection.length - 1] || null);
|
||||
const selectionOrderRef = useRef(initialRowSelection);
|
||||
@@ -729,11 +733,11 @@ const AppLayout = () => {
|
||||
setPreviewEntries(() => new Map());
|
||||
previewInflightRef.current = new Map();
|
||||
dragCounterRef.current = 0;
|
||||
prefetchedFoldersRef.current = new Set(['root']);
|
||||
breadcrumbFetchRef.current = new Set();
|
||||
detailFolderFetchRef.current = new Set();
|
||||
bootstrapInitializedRef.current = false;
|
||||
selectionInitializedRef.current = false;
|
||||
tenantIdRef.current = null;
|
||||
}, [assetManager]);
|
||||
|
||||
const tagLookupById = useMemo(() => {
|
||||
@@ -797,6 +801,10 @@ const AppLayout = () => {
|
||||
tokenRef.current = token;
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
tenantIdRef.current = currentTenantId;
|
||||
}, [currentTenantId]);
|
||||
|
||||
useEffect(() => {
|
||||
const requestInterceptor = api.interceptors.request.use((config) => {
|
||||
const currentToken = tokenRef.current;
|
||||
@@ -1401,9 +1409,29 @@ const AppLayout = () => {
|
||||
|
||||
|
||||
const ensureFolderData = useCallback(
|
||||
async (folderId, { force = false, includeDocuments = true } = {}) => {
|
||||
if (!force && folderContents.has(folderId)) {
|
||||
return folderContents.get(folderId);
|
||||
async (
|
||||
folderId,
|
||||
{ force = false, includeDocuments = true, prefetchDepth = 0 } = {},
|
||||
) => {
|
||||
const requestTenantId = tenantIdRef.current;
|
||||
const cached = folderContents.get(folderId);
|
||||
if (!force && cached) {
|
||||
const includesDocuments = Boolean(cached.__includesDocuments);
|
||||
if (!includeDocuments || includesDocuments) {
|
||||
if (prefetchDepth > 0) {
|
||||
const subfolders = Array.isArray(cached.subfolders) ? cached.subfolders : [];
|
||||
await Promise.allSettled(
|
||||
subfolders.map((entry) =>
|
||||
ensureFolderData(entry.id, {
|
||||
includeDocuments: false,
|
||||
prefetchDepth: prefetchDepth - 1,
|
||||
force: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
const path = folderId === 'root' ? 'root' : folderId;
|
||||
@@ -1414,6 +1442,12 @@ const AppLayout = () => {
|
||||
params,
|
||||
});
|
||||
const hydrated = assetManager.hydrateFolderContents(data);
|
||||
const childFolders = Array.isArray(data.subfolders) ? data.subfolders : [];
|
||||
const childIds = childFolders.map((child) => child.id);
|
||||
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return { ...hydrated, __includesDocuments: includeDocuments };
|
||||
}
|
||||
|
||||
setFolderNodes((prev) => {
|
||||
const next = new Map(prev);
|
||||
@@ -1424,9 +1458,9 @@ const AppLayout = () => {
|
||||
children: [],
|
||||
expanded: folderId === 'root',
|
||||
loaded: false,
|
||||
hasChildren: false,
|
||||
};
|
||||
|
||||
const childIds = (data.subfolders || []).map((child) => child.id);
|
||||
next.set(folderId, {
|
||||
...existingNode,
|
||||
name: folderId === 'root' ? DEFAULT_FOLDER_NAME : data.folder?.name || existingNode.name,
|
||||
@@ -1434,62 +1468,96 @@ const AppLayout = () => {
|
||||
children: childIds,
|
||||
expanded: folderId === 'root' ? true : existingNode.expanded,
|
||||
loaded: true,
|
||||
hasChildren: childIds.length > 0,
|
||||
});
|
||||
|
||||
(data.subfolders || []).forEach((child) => {
|
||||
childFolders.forEach((child) => {
|
||||
const childNode = next.get(child.id);
|
||||
const previousChildren = Array.isArray(childNode?.children) ? childNode.children : [];
|
||||
const childHasChildren = (() => {
|
||||
if (childNode?.loaded) {
|
||||
return previousChildren.length > 0;
|
||||
}
|
||||
if (Array.isArray(child?.subfolders)) {
|
||||
return child.subfolders.length > 0;
|
||||
}
|
||||
if (typeof child?.has_children === 'boolean') {
|
||||
return child.has_children;
|
||||
}
|
||||
if (typeof child?.hasChildren === 'boolean') {
|
||||
return child.hasChildren;
|
||||
}
|
||||
if (typeof childNode?.hasChildren === 'boolean') {
|
||||
return childNode.hasChildren;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
next.set(child.id, {
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
parentId: child.parent_id ?? 'root',
|
||||
children: childNode?.children ?? [],
|
||||
children: previousChildren,
|
||||
expanded: childNode?.expanded ?? false,
|
||||
loaded: childNode?.loaded ?? false,
|
||||
hasChildren: childHasChildren,
|
||||
});
|
||||
});
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
setFolderContents((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(folderId, hydrated);
|
||||
return next;
|
||||
});
|
||||
|
||||
return hydrated;
|
||||
},
|
||||
[assetManager, folderContents],
|
||||
);
|
||||
const greedyPrefetchFolders = useCallback(
|
||||
async (startIds) => {
|
||||
const queue = Array.isArray(startIds) ? [...startIds] : [];
|
||||
const visited = prefetchedFoldersRef.current;
|
||||
|
||||
while (queue.length) {
|
||||
const nextId = queue.shift();
|
||||
if (!nextId || visited.has(nextId)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(nextId);
|
||||
|
||||
try {
|
||||
const contents = await ensureFolderData(nextId, {
|
||||
force: false,
|
||||
includeDocuments: false,
|
||||
});
|
||||
const subfolders = Array.isArray(contents?.subfolders) ? contents.subfolders : [];
|
||||
subfolders.forEach((entry) => {
|
||||
if (entry?.id && !visited.has(entry.id)) {
|
||||
queue.push(entry.id);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[Folders] Failed to prefetch folder tree for', nextId, error);
|
||||
}
|
||||
if (prefetchDepth > 0 && childIds.length > 0 && tenantIdRef.current === requestTenantId) {
|
||||
await Promise.allSettled(
|
||||
childIds.map((childId) =>
|
||||
ensureFolderData(childId, {
|
||||
includeDocuments: false,
|
||||
force: false,
|
||||
prefetchDepth: prefetchDepth - 1,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const enriched = {
|
||||
...hydrated,
|
||||
__includesDocuments: includeDocuments,
|
||||
};
|
||||
|
||||
if (includeDocuments) {
|
||||
setFolderContents((prev) => {
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.set(folderId, enriched);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setFolderContents((prev) => {
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
const existingEntry = next.get(folderId);
|
||||
if (existingEntry) {
|
||||
next.set(folderId, {
|
||||
...existingEntry,
|
||||
...hydrated,
|
||||
documents: existingEntry.__includesDocuments
|
||||
? existingEntry.documents
|
||||
: hydrated.documents,
|
||||
__includesDocuments: existingEntry.__includesDocuments || false,
|
||||
});
|
||||
} else {
|
||||
next.set(folderId, enriched);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
return enriched;
|
||||
},
|
||||
[ensureFolderData],
|
||||
[api, assetManager, folderContents],
|
||||
);
|
||||
|
||||
const isInvalidFolderDrop = useCallback(
|
||||
@@ -1550,9 +1618,13 @@ const AppLayout = () => {
|
||||
|
||||
const previousParent = next.get(previousParentKey);
|
||||
if (previousParent) {
|
||||
const remainingChildren = (previousParent.children || []).filter(
|
||||
(childId) => childId !== folderId,
|
||||
);
|
||||
next.set(previousParentKey, {
|
||||
...previousParent,
|
||||
children: (previousParent.children || []).filter((childId) => childId !== folderId),
|
||||
children: remainingChildren,
|
||||
hasChildren: remainingChildren.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1564,6 +1636,7 @@ const AppLayout = () => {
|
||||
children: [],
|
||||
expanded: targetKey === 'root',
|
||||
loaded: false,
|
||||
hasChildren: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1572,6 +1645,7 @@ const AppLayout = () => {
|
||||
next.set(targetKey, {
|
||||
...targetNode,
|
||||
children: [...targetNode.children, folderId],
|
||||
hasChildren: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1581,14 +1655,14 @@ const AppLayout = () => {
|
||||
const refreshTargets = new Set([previousParentKey, targetKey]);
|
||||
for (const key of refreshTargets) {
|
||||
if (key === 'root') {
|
||||
await ensureFolderData('root', { force: true });
|
||||
await ensureFolderData('root', { force: true, prefetchDepth: 1 });
|
||||
} else {
|
||||
await ensureFolderData(key, { force: true });
|
||||
await ensureFolderData(key, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFolder === folderId) {
|
||||
await ensureFolderData(folderId, { force: true });
|
||||
await ensureFolderData(folderId, { force: true, prefetchDepth: 1 });
|
||||
setSelectedFolder(folderId);
|
||||
}
|
||||
|
||||
@@ -1600,9 +1674,9 @@ const AppLayout = () => {
|
||||
const refreshTargets = new Set([previousParentKey, targetKey]);
|
||||
for (const key of refreshTargets) {
|
||||
if (key === 'root') {
|
||||
await ensureFolderData('root', { force: true });
|
||||
await ensureFolderData('root', { force: true, prefetchDepth: 1 });
|
||||
} else {
|
||||
await ensureFolderData(key, { force: true });
|
||||
await ensureFolderData(key, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1611,19 +1685,33 @@ const AppLayout = () => {
|
||||
);
|
||||
|
||||
const refreshTags = useCallback(async () => {
|
||||
const requestTenantId = tenantIdRef.current;
|
||||
try {
|
||||
const { data } = await api.get('/tags');
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return;
|
||||
}
|
||||
setTags(data || []);
|
||||
} catch (error) {
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return;
|
||||
}
|
||||
notifyApiError(error, 'Unable to load tags.');
|
||||
}
|
||||
}, [api, notifyApiError]);
|
||||
|
||||
const refreshCorrespondents = useCallback(async () => {
|
||||
const requestTenantId = tenantIdRef.current;
|
||||
try {
|
||||
const { data } = await api.get('/correspondents');
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return;
|
||||
}
|
||||
setCorrespondents(data || []);
|
||||
} catch (error) {
|
||||
if (tenantIdRef.current !== requestTenantId) {
|
||||
return;
|
||||
}
|
||||
notifyApiError(error, 'Unable to load correspondents.');
|
||||
}
|
||||
}, [api, notifyApiError]);
|
||||
@@ -1914,10 +2002,14 @@ const AppLayout = () => {
|
||||
setSelectedFolder(targetId);
|
||||
if (showLoading) setLoading(true);
|
||||
try {
|
||||
const contents = await ensureFolderData(targetId, { force: true });
|
||||
const contents = await ensureFolderData(targetId, { force: true, prefetchDepth: 1 });
|
||||
if (targetId !== 'root') {
|
||||
try {
|
||||
await ensureFolderData('root', { force: false });
|
||||
await ensureFolderData('root', {
|
||||
force: false,
|
||||
includeDocuments: false,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh root folder tree', error);
|
||||
}
|
||||
@@ -1935,27 +2027,6 @@ const AppLayout = () => {
|
||||
[ensureFolderData, applySelectedFolder, notifyApiError],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!folderContents || typeof folderContents.forEach !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingIds = [];
|
||||
folderContents.forEach((contents) => {
|
||||
const subfolders = Array.isArray(contents?.subfolders) ? contents.subfolders : [];
|
||||
subfolders.forEach((entry) => {
|
||||
if (entry?.id && !prefetchedFoldersRef.current.has(entry.id)) {
|
||||
pendingIds.push(entry.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!pendingIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
greedyPrefetchFolders(pendingIds).catch(() => {});
|
||||
}, [folderContents, greedyPrefetchFolders]);
|
||||
|
||||
const expandFolderAncestors = useCallback(
|
||||
(targetId) => {
|
||||
@@ -2051,7 +2122,11 @@ const AppLayout = () => {
|
||||
}
|
||||
if (!folderNodes.has(ancestorId)) {
|
||||
try {
|
||||
await ensureFolderData(ancestorId, { includeDocuments: false, force: false });
|
||||
await ensureFolderData(ancestorId, {
|
||||
includeDocuments: false,
|
||||
force: false,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to ensure ancestor folder for navigation', ancestorId, error);
|
||||
}
|
||||
@@ -2136,7 +2211,10 @@ const AppLayout = () => {
|
||||
const refreshCurrentFolder = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const contents = await ensureFolderData(selectedFolder, { force: true });
|
||||
const contents = await ensureFolderData(selectedFolder, {
|
||||
force: true,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
applySelectedFolder(selectedFolder, contents);
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to refresh folder.');
|
||||
@@ -2311,7 +2389,9 @@ const AppLayout = () => {
|
||||
const target = form.get('target')?.toString() || 'root';
|
||||
const folderId = target === 'root' ? null : target;
|
||||
|
||||
setLoading(true);
|
||||
if (!refreshOnly) {
|
||||
setLoading(true);
|
||||
}
|
||||
try {
|
||||
await api.post('/documents/bulk/move', {
|
||||
document_ids: selectedDocumentIds,
|
||||
@@ -2331,9 +2411,9 @@ const AppLayout = () => {
|
||||
|
||||
applySelection([], { anchor: null });
|
||||
|
||||
await refreshCurrentFolder();
|
||||
await refreshCurrentFolder();
|
||||
if (folderId && folderId !== selectedFolder) {
|
||||
await ensureFolderData(folderId, { force: true });
|
||||
await ensureFolderData(folderId, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
|
||||
event.currentTarget.reset();
|
||||
@@ -2433,7 +2513,9 @@ const AppLayout = () => {
|
||||
notifyApiError(error, message);
|
||||
return { ok: false, reason: 'request-failed' };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!refreshOnly) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -3224,7 +3306,7 @@ const AppLayout = () => {
|
||||
targetFolderId !== 'root' &&
|
||||
targetFolderId !== selectedFolder
|
||||
) {
|
||||
await ensureFolderData(targetFolderId, { force: true });
|
||||
await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to upload files.');
|
||||
@@ -3285,7 +3367,7 @@ const AppLayout = () => {
|
||||
|
||||
await refreshCurrentFolder();
|
||||
if (targetFolderId && targetFolderId !== selectedFolder) {
|
||||
await ensureFolderData(targetFolderId, { force: true });
|
||||
await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.error || 'Failed to move documents.';
|
||||
@@ -3728,7 +3810,10 @@ const AppLayout = () => {
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const contents = await ensureFolderData(folderId, { force: true });
|
||||
const contents = await ensureFolderData(folderId, {
|
||||
force: true,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
const hasChildren = (contents.subfolders || []).length > 0;
|
||||
const hasDocs = (contents.documents || []).length > 0;
|
||||
if (hasChildren || hasDocs) {
|
||||
@@ -3744,9 +3829,11 @@ const AppLayout = () => {
|
||||
const parentId = node.parentId || 'root';
|
||||
const parentNode = next.get(parentId);
|
||||
if (parentNode) {
|
||||
const remaining = parentNode.children.filter((id) => id !== folderId);
|
||||
next.set(parentId, {
|
||||
...parentNode,
|
||||
children: parentNode.children.filter((id) => id !== folderId),
|
||||
children: remaining,
|
||||
hasChildren: remaining.length > 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3761,11 +3848,14 @@ const AppLayout = () => {
|
||||
const node = folderNodes.get(folderId);
|
||||
const parentId = node?.parentId || 'root';
|
||||
setSelectedFolder(parentId);
|
||||
const parentContents = await ensureFolderData(parentId, { force: true });
|
||||
const parentContents = await ensureFolderData(parentId, {
|
||||
force: true,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
applySelectedFolder(parentId, parentContents);
|
||||
} else if (selectedFolder !== 'root') {
|
||||
await ensureFolderData(selectedFolder, { force: true });
|
||||
}
|
||||
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
|
||||
}
|
||||
setStatusMessage('Folder deleted.', 'success');
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.error || 'Failed to delete folder.';
|
||||
@@ -3871,6 +3961,7 @@ const AppLayout = () => {
|
||||
...parentNode,
|
||||
children: parentNode.children.concat([data.folder.id]),
|
||||
loaded: true,
|
||||
hasChildren: true,
|
||||
});
|
||||
}
|
||||
next.set(data.folder.id, {
|
||||
@@ -3880,10 +3971,11 @@ const AppLayout = () => {
|
||||
children: [],
|
||||
expanded: false,
|
||||
loaded: false,
|
||||
hasChildren: false,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
await ensureFolderData(selectedFolder, { force: true });
|
||||
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
|
||||
succeeded = true;
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.error || 'Failed to create folder.';
|
||||
@@ -4350,9 +4442,22 @@ const AppLayout = () => {
|
||||
const folderClickHandlers = {
|
||||
onToggle: async (folderId) => {
|
||||
const node = folderNodes.get(folderId);
|
||||
if (node && !node.loaded) {
|
||||
const nextExpanded = !(node?.expanded ?? false);
|
||||
if (nextExpanded) {
|
||||
try {
|
||||
await ensureFolderData(folderId, { includeDocuments: false });
|
||||
await ensureFolderData(folderId, {
|
||||
includeDocuments: false,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to load folder.');
|
||||
}
|
||||
} else if (node && !node.loaded) {
|
||||
try {
|
||||
await ensureFolderData(folderId, {
|
||||
includeDocuments: false,
|
||||
prefetchDepth: 1,
|
||||
});
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to load folder.');
|
||||
}
|
||||
@@ -4361,7 +4466,7 @@ const AppLayout = () => {
|
||||
const next = new Map(prev);
|
||||
const current = next.get(folderId);
|
||||
if (!current) return prev;
|
||||
next.set(folderId, { ...current, expanded: !current.expanded });
|
||||
next.set(folderId, { ...current, expanded: nextExpanded });
|
||||
return next;
|
||||
});
|
||||
},
|
||||
@@ -4789,38 +4894,6 @@ const AppLayout = () => {
|
||||
|
||||
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
|
||||
|
||||
const sidebarProps = {
|
||||
folderNodes,
|
||||
onToggle: folderClickHandlers.onToggle,
|
||||
onSelect: folderClickHandlers.onSelect,
|
||||
onDrop: folderClickHandlers.onDrop,
|
||||
onDragOver: folderClickHandlers.onDragOver,
|
||||
onDragLeave: folderClickHandlers.onDragLeave,
|
||||
onDeleteFolder: handleFolderDelete,
|
||||
onRenameFolder: handleFolderRename,
|
||||
selectedFolder,
|
||||
onFolderDragStart: handleFolderDragStart,
|
||||
onFolderDragEnd: handleFolderDragEnd,
|
||||
draggedFolderId,
|
||||
tags,
|
||||
activeTagIds: activeTagFilters,
|
||||
onToggleTagFilter: toggleTagFilter,
|
||||
correspondents,
|
||||
activeCorrespondentIds: activeCorrespondentFilters,
|
||||
onToggleCorrespondentFilter: toggleCorrespondentFilter,
|
||||
appStatus,
|
||||
loading,
|
||||
previewActive,
|
||||
searchQuery,
|
||||
onSearchChange: handleSearchChange,
|
||||
onSearchSubmit: handleSearchSubmit,
|
||||
onSearchClear: clearFilters,
|
||||
isFilterActive,
|
||||
onLogout: handleLogout,
|
||||
status,
|
||||
onCollapse: collapseSidebar,
|
||||
};
|
||||
|
||||
const resolveThumbnailUrlForDoc = useCallback(
|
||||
(doc) =>
|
||||
resolveDocumentAssetUrl(doc, 'thumbnail', {
|
||||
@@ -4842,6 +4915,81 @@ const AppLayout = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleTenantSelect = useCallback(
|
||||
async (tenantOption, { refreshOnly = false } = {}) => {
|
||||
const requestedTenantId =
|
||||
tenantOption?.tenant_id || tenantOption?.tenantId || tenantOption?.id || null;
|
||||
if (!refreshOnly && (!requestedTenantId || requestedTenantId === currentTenantId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (!refreshOnly) {
|
||||
setStatusMessage('Switching tenant…', 'info');
|
||||
}
|
||||
|
||||
if (refreshOnly) {
|
||||
const { data } = await api.get('/auth/tenants');
|
||||
appDispatch({
|
||||
type: 'SET_TENANTS',
|
||||
tenants: Array.isArray(data?.tenants) ? data.tenants : [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await api.post('/auth/select-tenant', { tenant_id: requestedTenantId });
|
||||
if (!data?.access_token) {
|
||||
throw new Error('Missing access token in tenant switch response.');
|
||||
}
|
||||
|
||||
appDispatch({ type: 'LOGOUT' });
|
||||
resetWorkspaceState();
|
||||
|
||||
appDispatch({
|
||||
type: 'LOGIN_SUCCESS',
|
||||
token: data.access_token,
|
||||
tenant: data.tenant || null,
|
||||
});
|
||||
|
||||
api.defaults.headers.common.Authorization = `Bearer ${data.access_token}`;
|
||||
tokenRef.current = data.access_token;
|
||||
tenantIdRef.current = data?.tenant?.tenant_id || null;
|
||||
|
||||
if (Array.isArray(data?.tenants)) {
|
||||
appDispatch({ type: 'SET_TENANTS', tenants: data.tenants });
|
||||
}
|
||||
|
||||
setWorkspaceMode('table');
|
||||
navigate('/documents', { replace: true });
|
||||
|
||||
await Promise.all([refreshTags(), refreshCorrespondents()]);
|
||||
await loadFolder('root', { showLoading: false, preserveSearch: false });
|
||||
|
||||
setStatusMessage(
|
||||
`Switched to ${data?.tenant?.slug || data?.tenant?.tenant_id || 'tenant'}.`,
|
||||
'info',
|
||||
);
|
||||
} catch (error) {
|
||||
notifyApiError(error, 'Failed to switch tenant.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
currentTenantId,
|
||||
appDispatch,
|
||||
notifyApiError,
|
||||
setStatusMessage,
|
||||
resetWorkspaceState,
|
||||
setWorkspaceMode,
|
||||
navigate,
|
||||
refreshTags,
|
||||
refreshCorrespondents,
|
||||
loadFolder,
|
||||
],
|
||||
);
|
||||
|
||||
const documentsTableProps = {
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
@@ -4894,6 +5042,52 @@ const AppLayout = () => {
|
||||
onClearSelection: clearDocumentSelection,
|
||||
};
|
||||
|
||||
const sidebarProps = {
|
||||
folderNodes,
|
||||
onToggle: folderClickHandlers.onToggle,
|
||||
onSelect: folderClickHandlers.onSelect,
|
||||
onDrop: folderClickHandlers.onDrop,
|
||||
onDragOver: folderClickHandlers.onDragOver,
|
||||
onDragLeave: folderClickHandlers.onDragLeave,
|
||||
onDeleteFolder: handleFolderDelete,
|
||||
onRenameFolder: handleFolderRename,
|
||||
selectedFolder,
|
||||
onFolderDragStart: handleFolderDragStart,
|
||||
onFolderDragEnd: handleFolderDragEnd,
|
||||
draggedFolderId,
|
||||
tags,
|
||||
activeTagIds: activeTagFilters,
|
||||
onToggleTagFilter: toggleTagFilter,
|
||||
correspondents,
|
||||
activeCorrespondentIds: activeCorrespondentFilters,
|
||||
onToggleCorrespondentFilter: toggleCorrespondentFilter,
|
||||
appStatus,
|
||||
loading,
|
||||
previewActive,
|
||||
searchQuery,
|
||||
onSearchChange: handleSearchChange,
|
||||
onSearchSubmit: handleSearchSubmit,
|
||||
onSearchClear: clearFilters,
|
||||
isFilterActive,
|
||||
onLogout: handleLogout,
|
||||
status,
|
||||
onCollapse: collapseSidebar,
|
||||
tenantSlug,
|
||||
tenants: tenantOptions,
|
||||
activeTenantId: currentTenantId,
|
||||
onSelectTenant: handleTenantSelect,
|
||||
};
|
||||
|
||||
const sidebarPropsWithActions = useMemo(
|
||||
() => ({
|
||||
...sidebarProps,
|
||||
onManageTags: openTagsModal,
|
||||
onManageCorrespondents: openCorrespondentsModal,
|
||||
onCollapse: collapseSidebar,
|
||||
}),
|
||||
[sidebarProps, openTagsModal, openCorrespondentsModal, collapseSidebar],
|
||||
);
|
||||
|
||||
const detailPanelProps = {
|
||||
selectedDocuments: orderedSelectedDocuments,
|
||||
tags,
|
||||
|
||||
Reference in New Issue
Block a user