feat: Refactor frontend authentication and tenant switching to use global state
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAppState } from '../lib/store/appState';
|
||||||
import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
|
import { TAG_FILTER_UNTAGGED } from './workspaceUtils';
|
||||||
import { listDocuments } from '../lib/api/apiClient';
|
import { listDocuments } from '../lib/api/apiClient';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
@@ -15,7 +16,6 @@ import useNotifyApiError from '../hooks/useNotifyApiError';
|
|||||||
|
|
||||||
interface UseDocumentsSearchArgs {
|
interface UseDocumentsSearchArgs {
|
||||||
api: ApiClient;
|
api: ApiClient;
|
||||||
token?: string | null;
|
|
||||||
selectedFolder?: Identifier | 'root' | null;
|
selectedFolder?: Identifier | 'root' | null;
|
||||||
locationPathname?: string;
|
locationPathname?: string;
|
||||||
isDocumentsRoute?: boolean;
|
isDocumentsRoute?: boolean;
|
||||||
@@ -65,7 +65,6 @@ interface UseDocumentsSearchResult {
|
|||||||
|
|
||||||
const useDocumentsSearch = ({
|
const useDocumentsSearch = ({
|
||||||
api,
|
api,
|
||||||
token,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
locationPathname,
|
locationPathname,
|
||||||
isDocumentsRoute,
|
isDocumentsRoute,
|
||||||
@@ -75,6 +74,7 @@ const useDocumentsSearch = ({
|
|||||||
setSearchIncludeDescendants,
|
setSearchIncludeDescendants,
|
||||||
documentsManager,
|
documentsManager,
|
||||||
}: UseDocumentsSearchArgs): UseDocumentsSearchResult => {
|
}: UseDocumentsSearchArgs): UseDocumentsSearchResult => {
|
||||||
|
const { token } = useAppState();
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
const [activeTagFilters, setActiveTagFilters] = useState<Identifier[]>([]);
|
const [activeTagFilters, setActiveTagFilters] = useState<Identifier[]>([]);
|
||||||
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState<Identifier[]>([]);
|
const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState<Identifier[]>([]);
|
||||||
|
|||||||
@@ -3,15 +3,9 @@ import type { MutableRefObject } from 'react';
|
|||||||
import { clearAuthToken, logoutSession, refreshSession, setAuthToken } from '../../lib/api/apiClient';
|
import { clearAuthToken, logoutSession, refreshSession, setAuthToken } from '../../lib/api/apiClient';
|
||||||
import { useStatusToast } from '../../lib/context/StatusToastContext';
|
import { useStatusToast } from '../../lib/context/StatusToastContext';
|
||||||
|
|
||||||
type AppStatus = string;
|
import { useAppDispatch, useAppState } from '../../lib/store/appState';
|
||||||
|
|
||||||
type AppDispatch = (action: { type: string;[key: string]: unknown }) => void;
|
interface UseAuthManagerArgs { }
|
||||||
|
|
||||||
interface UseAuthManagerArgs {
|
|
||||||
token?: string | null;
|
|
||||||
appStatus: AppStatus;
|
|
||||||
appDispatch: AppDispatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseAuthManagerResult {
|
interface UseAuthManagerResult {
|
||||||
tokenRef: MutableRefObject<string | null>;
|
tokenRef: MutableRefObject<string | null>;
|
||||||
@@ -19,11 +13,9 @@ interface UseAuthManagerResult {
|
|||||||
handleLogout: () => Promise<void>;
|
handleLogout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAuthManager = ({
|
const useAuthManager = (_: UseAuthManagerArgs = {}): UseAuthManagerResult => {
|
||||||
token,
|
const { token, status: appStatus } = useAppState();
|
||||||
appStatus,
|
const appDispatch = useAppDispatch();
|
||||||
appDispatch,
|
|
||||||
}: UseAuthManagerArgs): UseAuthManagerResult => {
|
|
||||||
const tokenRef = useRef<string | null>(token);
|
const tokenRef = useRef<string | null>(token);
|
||||||
const initialRefreshAttemptedRef = useRef(Boolean(token));
|
const initialRefreshAttemptedRef = useRef(Boolean(token));
|
||||||
const { showToast } = useStatusToast();
|
const { showToast } = useStatusToast();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
import { useStatusToast } from '../../lib/context/StatusToastContext';
|
import { useStatusToast } from '../../lib/context/StatusToastContext';
|
||||||
|
|
||||||
import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
|
import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
|
||||||
import { getEntryId, isDocumentEntry } from '../../app/entryKey';
|
import { getEntryId, isDocumentEntry } from '../../app/entryKey';
|
||||||
import {
|
import {
|
||||||
@@ -74,7 +75,6 @@ interface DocumentTagExtras {
|
|||||||
import useNotifyApiError from '../../hooks/useNotifyApiError';
|
import useNotifyApiError from '../../hooks/useNotifyApiError';
|
||||||
|
|
||||||
interface UseDocumentMutationsArgs {
|
interface UseDocumentMutationsArgs {
|
||||||
token?: string | null;
|
|
||||||
documentLookup: Map<DocumentId, Document>;
|
documentLookup: Map<DocumentId, Document>;
|
||||||
folderLabelMap: Map<FolderId, string>;
|
folderLabelMap: Map<FolderId, string>;
|
||||||
ensureFolderData: EnsureFolderData;
|
ensureFolderData: EnsureFolderData;
|
||||||
@@ -143,7 +143,6 @@ const normalizeDocumentId = (value: unknown): DocumentId | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useDocumentMutations = ({
|
const useDocumentMutations = ({
|
||||||
token,
|
|
||||||
documentLookup,
|
documentLookup,
|
||||||
folderLabelMap,
|
folderLabelMap,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
@@ -338,10 +337,6 @@ const useDocumentMutations = ({
|
|||||||
|
|
||||||
const handleThumbnailRegeneration = useCallback(
|
const handleThumbnailRegeneration = useCallback(
|
||||||
async (documentId: DocumentId) => {
|
async (documentId: DocumentId) => {
|
||||||
if (!token) {
|
|
||||||
showToast('Log in to manage assets.', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await queueDocumentReanalysis(documentId, { force: true });
|
await queueDocumentReanalysis(documentId, { force: true });
|
||||||
showToast('Document re-analysis queued.', 'info');
|
showToast('Document re-analysis queued.', 'info');
|
||||||
@@ -351,7 +346,7 @@ const useDocumentMutations = ({
|
|||||||
notifyApiError(error, message);
|
notifyApiError(error, message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[token, refreshCurrentFolder, notifyApiError, showToast],
|
[refreshCurrentFolder, notifyApiError, showToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDocumentsDelete = useCallback(
|
const handleDocumentsDelete = useCallback(
|
||||||
@@ -360,11 +355,6 @@ const useDocumentMutations = ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
showToast('Log in to manage documents.', 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(documentIds.map((documentId) => trashDocument(documentId)));
|
await Promise.all(documentIds.map((documentId) => trashDocument(documentId)));
|
||||||
|
|
||||||
@@ -386,8 +376,6 @@ const useDocumentMutations = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
token,
|
|
||||||
|
|
||||||
removeDocumentsFromCaches,
|
removeDocumentsFromCaches,
|
||||||
previewDocumentId,
|
previewDocumentId,
|
||||||
closeDocumentPreview,
|
closeDocumentPreview,
|
||||||
@@ -625,12 +613,6 @@ const useDocumentMutations = ({
|
|||||||
|
|
||||||
const handleFolderDelete = useCallback(
|
const handleFolderDelete = useCallback(
|
||||||
async (folderId?: FolderId, { showMessage = true }: MessageOptions = {}) => {
|
async (folderId?: FolderId, { showMessage = true }: MessageOptions = {}) => {
|
||||||
if (!token) {
|
|
||||||
if (showMessage) {
|
|
||||||
showToast('Log in to manage folders.', 'error');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!folderId || folderId === 'root') {
|
if (!folderId || folderId === 'root') {
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
showToast('The root folder cannot be removed.', 'error');
|
showToast('The root folder cannot be removed.', 'error');
|
||||||
@@ -695,7 +677,6 @@ const useDocumentMutations = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
token,
|
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
folderNodes,
|
folderNodes,
|
||||||
|
|||||||
@@ -145,15 +145,8 @@ const useDocumentsWorkspace = ({
|
|||||||
: [];
|
: [];
|
||||||
const { showToast } = useStatusToast();
|
const { showToast } = useStatusToast();
|
||||||
const notifyApiError = useNotifyApiError();
|
const notifyApiError = useNotifyApiError();
|
||||||
|
|
||||||
const [creatingFolder, setCreatingFolder] = useState(false);
|
const [creatingFolder, setCreatingFolder] = useState(false);
|
||||||
|
const { handleLogout } = useAuthManager({});
|
||||||
const { tokenRef, handleLogout } = useAuthManager({
|
|
||||||
token,
|
|
||||||
appStatus,
|
|
||||||
appDispatch,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const tagRemovalCursorActiveRef = useRef(false);
|
const tagRemovalCursorActiveRef = useRef(false);
|
||||||
const tenantIdRef = useRef(currentTenantId);
|
const tenantIdRef = useRef(currentTenantId);
|
||||||
@@ -287,63 +280,59 @@ const useDocumentsWorkspace = ({
|
|||||||
folderId: FolderNodeId,
|
folderId: FolderNodeId,
|
||||||
options: { includeDocuments?: boolean } = {}
|
options: { includeDocuments?: boolean } = {}
|
||||||
) => {
|
) => {
|
||||||
try {
|
|
||||||
const path = folderId === 'root' ? 'root' : folderId;
|
|
||||||
const includeDocuments = options.includeDocuments ?? true;
|
|
||||||
const params: Record<string, unknown> = {
|
|
||||||
include_documents: includeDocuments,
|
|
||||||
sort: activeSortFieldRef.current,
|
|
||||||
dir: activeSortDirectionRef.current,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await listFolderContents(path, params);
|
const path = folderId === 'root' ? 'root' : folderId;
|
||||||
|
const includeDocuments = options.includeDocuments ?? true;
|
||||||
|
const params: Record<string, unknown> = {
|
||||||
|
include_documents: includeDocuments,
|
||||||
|
sort: activeSortFieldRef.current,
|
||||||
|
dir: activeSortDirectionRef.current,
|
||||||
|
};
|
||||||
|
|
||||||
// Only update UI state if we are fetching for the currently selected folder
|
const data = await listFolderContents(path, params);
|
||||||
if (folderId === selectedFolder) {
|
|
||||||
// Update documents state if included
|
|
||||||
if (includeDocuments) {
|
|
||||||
setDocuments((data.documents || []) as Document[]);
|
|
||||||
}
|
|
||||||
setCurrentSubfolders((data.subfolders || []) as any[]);
|
|
||||||
|
|
||||||
// Update selection state based on new documents
|
// Only update UI state if we are fetching for the currently selected folder
|
||||||
if (includeDocuments) {
|
if (folderId === selectedFolder) {
|
||||||
const docs = (data.documents || []) as Document[];
|
// Update documents state if included
|
||||||
const subfolders = (data.subfolders || []) as any[];
|
if (includeDocuments) {
|
||||||
|
setDocuments((data.documents || []) as Document[]);
|
||||||
|
}
|
||||||
|
setCurrentSubfolders((data.subfolders || []) as any[]);
|
||||||
|
|
||||||
const availableDocKeys = docs
|
// Update selection state based on new documents
|
||||||
.map((doc) => createDocumentEntryKey(doc?.id as Identifier))
|
if (includeDocuments) {
|
||||||
.filter(Boolean);
|
const docs = (data.documents || []) as Document[];
|
||||||
const availableDocKeySet = new Set(availableDocKeys);
|
const subfolders = (data.subfolders || []) as any[];
|
||||||
const availableFolderKeys = new Set(
|
|
||||||
subfolders
|
|
||||||
.map((folder) => createFolderEntryKey(folder?.id as Identifier))
|
|
||||||
.filter(Boolean),
|
|
||||||
);
|
|
||||||
|
|
||||||
setSelectedEntries((previous) => {
|
const availableDocKeys = docs
|
||||||
const previousFolderKeys = previous
|
.map((doc) => createDocumentEntryKey(doc?.id as Identifier))
|
||||||
.filter(isFolderEntry)
|
.filter(Boolean);
|
||||||
.filter((key) => availableFolderKeys.has(key));
|
const availableDocKeySet = new Set(availableDocKeys);
|
||||||
const previousDocKeys = previous.filter(isDocumentEntry);
|
const availableFolderKeys = new Set(
|
||||||
const nextDocKeys = previousDocKeys.filter((key) => availableDocKeySet.has(key));
|
subfolders
|
||||||
const mergedSelection = [...previousFolderKeys, ...nextDocKeys];
|
.map((folder) => createFolderEntryKey(folder?.id as Identifier))
|
||||||
return mergedSelection;
|
.filter(Boolean),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
setSelectedEntries((previous) => {
|
||||||
|
const previousFolderKeys = previous
|
||||||
|
.filter(isFolderEntry)
|
||||||
|
.filter((key) => availableFolderKeys.has(key));
|
||||||
|
const previousDocKeys = previous.filter(isDocumentEntry);
|
||||||
|
const nextDocKeys = previousDocKeys.filter((key) => availableDocKeySet.has(key));
|
||||||
|
const mergedSelection = [...previousFolderKeys, ...nextDocKeys];
|
||||||
|
return mergedSelection;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return data; // Return data for consumers (e.g. useDocumentMutations)
|
|
||||||
} catch (error) {
|
|
||||||
notifyApiError(error, 'Failed to fetch folder contents');
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return data; // Return data for consumers (e.g. useDocumentMutations)
|
||||||
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeSortFieldRef,
|
activeSortFieldRef,
|
||||||
activeSortDirectionRef,
|
activeSortDirectionRef,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
notifyApiError,
|
|
||||||
setCurrentSubfolders,
|
setCurrentSubfolders,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
]
|
]
|
||||||
@@ -351,9 +340,11 @@ const useDocumentsWorkspace = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
ensureFolderData(selectedFolder);
|
ensureFolderData(selectedFolder).catch((error) => {
|
||||||
|
notifyApiError(error, 'Failed to fetch folder contents');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [selectedFolder, documentsSortField, documentsSortDirection, ensureFolderData]);
|
}, [selectedFolder, documentsSortField, documentsSortDirection, ensureFolderData, notifyApiError]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -369,7 +360,6 @@ const useDocumentsWorkspace = ({
|
|||||||
documentsFilterValue,
|
documentsFilterValue,
|
||||||
} = useDocumentsSearch({
|
} = useDocumentsSearch({
|
||||||
api: apiClient,
|
api: apiClient,
|
||||||
token,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
locationPathname: location.pathname,
|
locationPathname: location.pathname,
|
||||||
isDocumentsRoute,
|
isDocumentsRoute,
|
||||||
@@ -542,9 +532,7 @@ const useDocumentsWorkspace = ({
|
|||||||
refreshPasskeys,
|
refreshPasskeys,
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
revokePasskey,
|
revokePasskey,
|
||||||
} = usePasskeys({
|
} = usePasskeys({});
|
||||||
token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resolveTargetDocumentIds = useCallback(
|
const resolveTargetDocumentIds = useCallback(
|
||||||
(candidateIds) => {
|
(candidateIds) => {
|
||||||
@@ -585,7 +573,6 @@ const useDocumentsWorkspace = ({
|
|||||||
resetUploadsState,
|
resetUploadsState,
|
||||||
handleFileSelection,
|
handleFileSelection,
|
||||||
} = useDocumentUploads({
|
} = useDocumentUploads({
|
||||||
token,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
@@ -709,7 +696,6 @@ const useDocumentsWorkspace = ({
|
|||||||
handleDocumentIssuedUpdate,
|
handleDocumentIssuedUpdate,
|
||||||
handleTagRemove,
|
handleTagRemove,
|
||||||
} = useDocumentMutations({
|
} = useDocumentMutations({
|
||||||
token,
|
|
||||||
documentLookup,
|
documentLookup,
|
||||||
folderLabelMap,
|
folderLabelMap,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
@@ -749,7 +735,6 @@ const useDocumentsWorkspace = ({
|
|||||||
handleFolderDelete,
|
handleFolderDelete,
|
||||||
folderClickHandlers,
|
folderClickHandlers,
|
||||||
} = useFolderTreeActions({
|
} = useFolderTreeActions({
|
||||||
token,
|
|
||||||
folderNodes,
|
folderNodes,
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
@@ -1078,13 +1063,7 @@ const useDocumentsWorkspace = ({
|
|||||||
|
|
||||||
const { handleTenantSelect } = useTenantManager({
|
const { handleTenantSelect } = useTenantManager({
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
resetWorkspaceState,
|
|
||||||
refreshTags,
|
|
||||||
refreshCorrespondents,
|
|
||||||
loadFolder,
|
|
||||||
handleDocumentsViewModeChange,
|
handleDocumentsViewModeChange,
|
||||||
tokenRef,
|
|
||||||
tenantIdRef,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionContext = {
|
const sessionContext = {
|
||||||
|
|||||||
@@ -1,68 +1,65 @@
|
|||||||
import { MutableRefObject, useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import type { FolderId, TenantId } from '../../types/identifiers';
|
import type { TenantId } from '../../types/identifiers';
|
||||||
import { useStatusToast } from '../../lib/context/StatusToastContext';
|
import { useStatusToast } from '../../lib/context/StatusToastContext';
|
||||||
import { useAppDispatch } from '../../lib/store/appState';
|
import { useAppDispatch } from '../../lib/store/appState';
|
||||||
|
|
||||||
import { api, listTenants, switchTenant } from '../../lib/api/apiClient';
|
import { api, listTenants, switchTenant } from '../../lib/api/apiClient';
|
||||||
|
|
||||||
|
import useNotifyApiError from '../../hooks/useNotifyApiError';
|
||||||
|
|
||||||
interface TenantOption {
|
interface TenantOption {
|
||||||
id?: TenantId;
|
id?: TenantId;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
import useNotifyApiError from '../../hooks/useNotifyApiError';
|
|
||||||
|
|
||||||
interface UseTenantManagerOptions {
|
interface UseTenantManagerOptions {
|
||||||
currentTenantId: TenantId | null;
|
currentTenantId: TenantId | null;
|
||||||
resetWorkspaceState: () => void;
|
|
||||||
refreshTags: () => Promise<void>;
|
|
||||||
refreshCorrespondents: () => Promise<void>;
|
|
||||||
loadFolder: (folderId: FolderId, options?: { preserveSearch?: boolean }) => Promise<void>;
|
|
||||||
handleDocumentsViewModeChange: (mode: string) => void;
|
handleDocumentsViewModeChange: (mode: string) => void;
|
||||||
tokenRef?: MutableRefObject<string | null>;
|
|
||||||
tenantIdRef?: MutableRefObject<TenantId | null>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTenantManager = ({
|
const useTenantManager = ({
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
resetWorkspaceState,
|
|
||||||
refreshTags,
|
|
||||||
refreshCorrespondents,
|
|
||||||
loadFolder,
|
|
||||||
handleDocumentsViewModeChange,
|
handleDocumentsViewModeChange,
|
||||||
tokenRef,
|
|
||||||
tenantIdRef,
|
|
||||||
}: UseTenantManagerOptions) => {
|
}: UseTenantManagerOptions) => {
|
||||||
const { showToast } = useStatusToast();
|
const { showToast } = useStatusToast();
|
||||||
const notifyApiError = useNotifyApiError();
|
const notifyApiError = useNotifyApiError();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleTenantSelect = useCallback(
|
const handleTenantSelect = useCallback(
|
||||||
async (tenantOption: TenantOption | null, { refreshOnly = false }: { refreshOnly?: boolean } = {}) => {
|
async (tenantOption: TenantOption | null, { refreshOnly = false }: { refreshOnly?: boolean } = {}) => {
|
||||||
const requestedTenantId = tenantOption?.id ?? null;
|
const requestedTenantId = tenantOption?.id ?? null;
|
||||||
|
|
||||||
|
// 1. Guard Clauses
|
||||||
if (!refreshOnly && (!requestedTenantId || requestedTenantId === currentTenantId)) {
|
if (!refreshOnly && (!requestedTenantId || requestedTenantId === currentTenantId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 2. Refresh Logic
|
||||||
if (refreshOnly) {
|
if (refreshOnly) {
|
||||||
const data = await listTenants();
|
const data = await listTenants();
|
||||||
appDispatch({
|
appDispatch({ type: 'SET_TENANTS', tenants: data });
|
||||||
type: 'SET_TENANTS',
|
|
||||||
tenants: data,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Switch Logic
|
||||||
const data = await switchTenant(requestedTenantId);
|
const data = await switchTenant(requestedTenantId);
|
||||||
|
|
||||||
if (!data?.access_token) {
|
if (!data?.access_token) {
|
||||||
throw new Error('Missing access token in tenant switch response.');
|
throw new Error('Missing access token in tenant switch response.');
|
||||||
}
|
}
|
||||||
|
|
||||||
appDispatch({ type: 'LOGOUT' });
|
// 4. Reset UI to safe state BEFORE updating global auth
|
||||||
resetWorkspaceState();
|
// This prevents old components from reacting to state changes.
|
||||||
|
|
||||||
|
|
||||||
|
// 5. Update Global State IMMEDIATELY
|
||||||
|
// Don't wait for navigation. Data consistency comes first.
|
||||||
|
handleDocumentsViewModeChange('list');
|
||||||
|
api.defaults.headers.common.Authorization = `Bearer ${data.access_token}`;
|
||||||
|
|
||||||
appDispatch({
|
appDispatch({
|
||||||
type: 'LOGIN_SUCCESS',
|
type: 'LOGIN_SUCCESS',
|
||||||
@@ -70,26 +67,16 @@ const useTenantManager = ({
|
|||||||
tenant: data.tenant || null,
|
tenant: data.tenant || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
api.defaults.headers.common.Authorization = `Bearer ${data.access_token}`;
|
|
||||||
if (tokenRef) {
|
|
||||||
tokenRef.current = data.access_token;
|
|
||||||
}
|
|
||||||
if (tenantIdRef) {
|
|
||||||
tenantIdRef.current = data?.tenant?.id ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data?.tenants)) {
|
if (Array.isArray(data?.tenants)) {
|
||||||
appDispatch({ type: 'SET_TENANTS', tenants: data.tenants });
|
appDispatch({ type: 'SET_TENANTS', tenants: data.tenants });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDocumentsViewModeChange('list');
|
|
||||||
navigate('/documents', { replace: true });
|
|
||||||
|
|
||||||
await Promise.all([refreshTags(), refreshCorrespondents()]);
|
|
||||||
await loadFolder('root', { preserveSearch: false });
|
|
||||||
|
|
||||||
const tenantLabel = data?.tenant?.name || data?.tenant?.id || 'tenant';
|
const tenantLabel = data?.tenant?.name || data?.tenant?.id || 'tenant';
|
||||||
showToast(`Switched to ${tenantLabel}.`, 'info');
|
showToast(`Switched to ${tenantLabel}.`, 'info');
|
||||||
|
|
||||||
|
// 5. Handle UI/Navigation changes AFTER state is secure
|
||||||
|
navigate('/documents', { replace: true });
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifyApiError(error, 'Failed to switch tenant.');
|
notifyApiError(error, 'Failed to switch tenant.');
|
||||||
}
|
}
|
||||||
@@ -98,15 +85,9 @@ const useTenantManager = ({
|
|||||||
appDispatch,
|
appDispatch,
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
handleDocumentsViewModeChange,
|
handleDocumentsViewModeChange,
|
||||||
loadFolder,
|
|
||||||
navigate,
|
navigate,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
refreshCorrespondents,
|
|
||||||
refreshTags,
|
|
||||||
resetWorkspaceState,
|
|
||||||
showToast,
|
showToast,
|
||||||
tenantIdRef,
|
|
||||||
tokenRef,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ interface FolderClickHandlers {
|
|||||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||||
|
|
||||||
interface UseFolderTreeActionsOptions {
|
interface UseFolderTreeActionsOptions {
|
||||||
token?: string | null;
|
|
||||||
folderNodes: Map<FolderKey, FolderNode>;
|
folderNodes: Map<FolderKey, FolderNode>;
|
||||||
setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void;
|
setFolderNodes: (updater: (prev: Map<FolderKey, FolderNode>) => Map<FolderKey, FolderNode>) => void;
|
||||||
selectedFolder: FolderKey;
|
selectedFolder: FolderKey;
|
||||||
@@ -49,7 +48,6 @@ interface UseFolderTreeActionsOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useFolderTreeActions = ({
|
const useFolderTreeActions = ({
|
||||||
token,
|
|
||||||
folderNodes,
|
folderNodes,
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
@@ -181,10 +179,6 @@ const useFolderTreeActions = ({
|
|||||||
|
|
||||||
const handleFolderRename = useCallback(
|
const handleFolderRename = useCallback(
|
||||||
async (folderId: FolderKey, nextName: string) => {
|
async (folderId: FolderKey, nextName: string) => {
|
||||||
if (!token) {
|
|
||||||
showToast('Log in to rename folders.', 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const trimmed = nextName?.trim?.() || '';
|
const trimmed = nextName?.trim?.() || '';
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
showToast('Folder name cannot be empty.', 'error');
|
showToast('Folder name cannot be empty.', 'error');
|
||||||
@@ -214,16 +208,11 @@ const useFolderTreeActions = ({
|
|||||||
notifyApiError,
|
notifyApiError,
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
showToast,
|
showToast,
|
||||||
token,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFolderCreate = useCallback(
|
const handleFolderCreate = useCallback(
|
||||||
async (name: string, parentId?: FolderKey | null) => {
|
async (name: string, parentId?: FolderKey | null) => {
|
||||||
if (!token) {
|
|
||||||
showToast('Log in to create folders.', 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
showToast('Folder name cannot be empty.', 'error');
|
showToast('Folder name cannot be empty.', 'error');
|
||||||
return false;
|
return false;
|
||||||
@@ -291,18 +280,11 @@ const useFolderTreeActions = ({
|
|||||||
setCreatingFolder,
|
setCreatingFolder,
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
showToast,
|
showToast,
|
||||||
token,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFolderDelete = useCallback(
|
const handleFolderDelete = useCallback(
|
||||||
async (folderId: FolderKey, { showMessage = true }: MessageOptions = {}) => {
|
async (folderId: FolderKey, { showMessage = true }: MessageOptions = {}) => {
|
||||||
if (!token) {
|
|
||||||
if (showMessage) {
|
|
||||||
showToast('Log in to manage folders.', 'error');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!folderId || folderId === 'root') {
|
if (!folderId || folderId === 'root') {
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
showToast('The root folder cannot be removed.', 'error');
|
showToast('The root folder cannot be removed.', 'error');
|
||||||
@@ -352,7 +334,6 @@ const useFolderTreeActions = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
token,
|
|
||||||
folderNodes,
|
folderNodes,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ const mapFilesToEntries = (filesInput?: FileList | File[] | null): FileEntry[] =
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface UseDocumentUploadsArgs {
|
interface UseDocumentUploadsArgs {
|
||||||
token?: string | null;
|
|
||||||
selectedFolder?: FolderId;
|
selectedFolder?: FolderId;
|
||||||
currentFolderName?: string | null;
|
currentFolderName?: string | null;
|
||||||
ensureFolderData: (folderId: FolderId, options?: { [key: string]: unknown }) => Promise<void>;
|
ensureFolderData: (folderId: FolderId, options?: { [key: string]: unknown }) => Promise<void>;
|
||||||
@@ -121,7 +120,6 @@ interface UseDocumentUploadsResult {
|
|||||||
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
||||||
|
|
||||||
const useDocumentUploads = ({
|
const useDocumentUploads = ({
|
||||||
token,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
@@ -370,19 +368,6 @@ const useDocumentUploads = ({
|
|||||||
|
|
||||||
const queueItems = appendQueueItems(entries, targetFolderId);
|
const queueItems = appendQueueItems(entries, targetFolderId);
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
queueItems.forEach((item) => {
|
|
||||||
const patch = {
|
|
||||||
status: 'error',
|
|
||||||
error: 'Please log in before uploading.',
|
|
||||||
code: null,
|
|
||||||
};
|
|
||||||
updateQueueItem(item.id, patch);
|
|
||||||
Object.assign(item, patch);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
folderPathCacheRef.current.clear();
|
folderPathCacheRef.current.clear();
|
||||||
|
|
||||||
@@ -461,7 +446,6 @@ const useDocumentUploads = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
token,
|
|
||||||
ensureFolderPathOnServer,
|
ensureFolderPathOnServer,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
refreshCurrentFolder,
|
refreshCurrentFolder,
|
||||||
@@ -497,7 +481,6 @@ const useDocumentUploads = ({
|
|||||||
|
|
||||||
useFileDrop({
|
useFileDrop({
|
||||||
shellRef,
|
shellRef,
|
||||||
token,
|
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ interface DropOverlayState {
|
|||||||
|
|
||||||
interface UseFileDropOptions {
|
interface UseFileDropOptions {
|
||||||
shellRef: MutableRefObject<HTMLElement | null>;
|
shellRef: MutableRefObject<HTMLElement | null>;
|
||||||
token?: string | null;
|
|
||||||
currentFolderName: string | null;
|
currentFolderName: string | null;
|
||||||
selectedFolder: FolderId;
|
selectedFolder: FolderId;
|
||||||
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderId) => Promise<void>;
|
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderId) => Promise<void>;
|
||||||
@@ -21,7 +20,6 @@ interface UseFileDropOptions {
|
|||||||
|
|
||||||
const useFileDrop = ({
|
const useFileDrop = ({
|
||||||
shellRef,
|
shellRef,
|
||||||
token,
|
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
@@ -32,12 +30,6 @@ const useFileDrop = ({
|
|||||||
}: UseFileDropOptions) => {
|
}: UseFileDropOptions) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
|
||||||
setDropOverlayState((prev) => ({ ...prev, active: false }));
|
|
||||||
dragCounterRef.current = 0;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnter = (event: DragEvent) => {
|
const handleDragEnter = (event: DragEvent) => {
|
||||||
if (!hasFiles(event)) return;
|
if (!hasFiles(event)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -86,7 +78,6 @@ const useFileDrop = ({
|
|||||||
setDropOverlayState((prev) => ({ ...prev, active: false }));
|
setDropOverlayState((prev) => ({ ...prev, active: false }));
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
token,
|
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
defaultFolderName,
|
defaultFolderName,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useAppState } from '../lib/store/appState';
|
||||||
import { useStatusToast } from '../lib/context/StatusToastContext';
|
import { useStatusToast } from '../lib/context/StatusToastContext';
|
||||||
/* global PublicKeyCredentialCreationOptions, CredentialCreationOptions */
|
/* global PublicKeyCredentialCreationOptions, CredentialCreationOptions */
|
||||||
|
|
||||||
@@ -69,9 +70,7 @@ type RevokePasskeyResult =
|
|||||||
|
|
||||||
import useNotifyApiError from '../hooks/useNotifyApiError';
|
import useNotifyApiError from '../hooks/useNotifyApiError';
|
||||||
|
|
||||||
interface UsePasskeysArgs {
|
interface UsePasskeysArgs { }
|
||||||
token?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UsePasskeysResult {
|
interface UsePasskeysResult {
|
||||||
passkeys: PasskeyRecord[];
|
passkeys: PasskeyRecord[];
|
||||||
@@ -87,7 +86,8 @@ interface UsePasskeysResult {
|
|||||||
) => Promise<RevokePasskeyResult>;
|
) => Promise<RevokePasskeyResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usePasskeys = ({ token }: UsePasskeysArgs): UsePasskeysResult => {
|
const usePasskeys = (_: UsePasskeysArgs = {}): UsePasskeysResult => {
|
||||||
|
const { token } = useAppState();
|
||||||
const [passkeys, setPasskeys] = useState<PasskeyRecord[]>([]);
|
const [passkeys, setPasskeys] = useState<PasskeyRecord[]>([]);
|
||||||
const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null);
|
const [passkeysSupported, setPasskeysSupported] = useState<boolean | null>(null);
|
||||||
const [passkeysLoading, setPasskeysLoading] = useState(false);
|
const [passkeysLoading, setPasskeysLoading] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user