typescript

This commit is contained in:
2025-11-13 02:37:16 +01:00
parent ada089c05b
commit 6d55e61cee
43 changed files with 923 additions and 406 deletions
@@ -93,6 +93,24 @@ const useDocumentCorrespondentActions = ({
[apiClient, refreshCurrentFolder, notifyApiError, setStatusMessage],
);
const normalizeOption = (
option: CorrespondentOption | string | null,
): CorrespondentOption | null => {
if (!option) {
return null;
}
if (typeof option === 'object' && 'id' in option) {
return option as CorrespondentOption;
}
if (typeof option === 'string') {
const trimmed = option.trim();
if (trimmed) {
return { id: null, name: trimmed };
}
}
return null;
};
const handleCorrespondentAdd = useCallback(
async ({ document, name, input = null, option = null }: { document?: { id?: string | number }; name?: string; input?: HTMLInputElement | null; option?: CorrespondentOption | string | null }) => {
if (!document?.id) {
@@ -104,12 +122,7 @@ const useDocumentCorrespondentActions = ({
return;
}
let target = null;
if (option && option.id) {
target = correspondentLookupByName.get(trimmed.toLowerCase()) || option;
} else {
target = correspondentLookupByName.get(trimmed.toLowerCase()) || null;
}
let target = correspondentLookupByName.get(trimmed.toLowerCase()) || normalizeOption(option);
if (!target) {
try {
target = await handleCorrespondentCreate({ name: trimmed });
@@ -102,12 +102,18 @@ const useDocumentDragHandlers = ({
if (item.type === 'document') {
const doc = item.payload;
const rowEl = doc?.id
? document.getElementById(`document-row-${doc.id}`)
|| document.getElementById(`document-card-${doc.id}`)
? (document.getElementById(`document-row-${doc.id}`)
|| document.getElementById(`document-card-${doc.id}`))
: null;
const wrapperEl = rowEl instanceof HTMLElement
? rowEl.querySelector<HTMLElement>('.document-thumbnail-wrapper')
: null;
const thumbnailEl = rowEl instanceof HTMLElement
? rowEl.querySelector<HTMLImageElement>('.document-thumbnail')
: null;
const placeholderEl = rowEl instanceof HTMLElement
? rowEl.querySelector<HTMLElement>('.thumb-placeholder')
: null;
const wrapperEl = rowEl?.querySelector('.document-thumbnail-wrapper');
const thumbnailEl = rowEl?.querySelector('.document-thumbnail');
const placeholderEl = rowEl?.querySelector('.thumb-placeholder');
const aspectAttr = wrapperEl?.dataset?.thumbnailAspect;
const aspectRatio = aspectAttr ? parseFloat(aspectAttr) : null;
@@ -130,7 +136,7 @@ const useDocumentDragHandlers = ({
layer.classList.add('document-drag-preview__item--image');
layer.style.backgroundImage = `url("${thumbSrc}")`;
} else if (placeholderEl instanceof HTMLElement) {
const clone = placeholderEl.cloneNode(true);
const clone = placeholderEl.cloneNode(true) as HTMLElement;
clone.style.pointerEvents = 'none';
layer.appendChild(clone);
} else {
@@ -138,27 +144,36 @@ const useDocumentDragHandlers = ({
}
} else {
const payload = item.payload;
const folderId = payload?.id ?? (typeof payload?.trim === 'function' ? payload : null);
const folderId = (payload && typeof payload === 'object' && 'id' in payload)
? (payload as { id?: FolderIdentifier }).id
: (typeof (payload as { trim?: () => string })?.trim === 'function'
? (payload as { trim: () => string }).trim()
: null);
const rowEl = folderId
? document.getElementById(`folder-row-${folderId}`)
|| document.getElementById(`folder-card-${folderId}`)
? (document.getElementById(`folder-row-${folderId}`)
|| document.getElementById(`folder-card-${folderId}`))
: null;
const iconEl = rowEl instanceof HTMLElement
? rowEl.querySelector('.thumb-icon, .folder-card__icon')
: null;
const iconEl = rowEl?.querySelector('.thumb-icon, .folder-card__icon');
layer.style.width = `${size}px`;
layer.style.height = `${size}px`;
layer.classList.add('document-drag-preview__item--folder');
let content = null;
let content: HTMLElement | null = null;
if (iconEl instanceof HTMLElement) {
const cloneSource = iconEl.classList.contains('folder-card__icon')
? iconEl.querySelector('svg') || iconEl
: iconEl;
content = cloneSource.cloneNode(true);
content.classList.add('document-drag-preview__folder-thumb');
const svg = content.querySelector('svg');
if (svg) {
svg.setAttribute('width', '48');
svg.setAttribute('height', '48');
const clone = cloneSource.cloneNode(true);
if (clone instanceof HTMLElement) {
content = clone;
content.classList.add('document-drag-preview__folder-thumb');
const svg = content.querySelector('svg');
if (svg) {
svg.setAttribute('width', '48');
svg.setAttribute('height', '48');
}
}
}
@@ -326,12 +326,12 @@ const useDocumentMutations = ({
return filtered.length === prev.length ? prev : filtered;
});
setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId)));
setFolderContents((prev) => {
setFolderContents((prev: Map<FolderId, FolderContents>) => {
if (!prev.size) {
return prev;
}
let changed = false;
const next = new Map(prev);
const next = new Map<FolderId, FolderContents>(prev);
movedDocs.forEach(({ id, sourceFolderId }) => {
const sourceKey = (sourceFolderId || 'root') as FolderId;
const entry = next.get(sourceKey);
@@ -714,8 +714,8 @@ const useDocumentMutations = ({
await api.delete(`/folders/${folderId}`);
setFolderNodes((prev) => {
const next = new Map(prev);
setFolderNodes((prev: Map<FolderId, FolderNode>) => {
const next = new Map<FolderId, FolderNode>(prev);
const node = next.get(folderId);
next.delete(folderId);
if (node) {
@@ -733,8 +733,8 @@ const useDocumentMutations = ({
return next;
});
setFolderContents((prev) => {
const next = new Map(prev);
setFolderContents((prev: Map<FolderId, FolderContents>) => {
const next = new Map<FolderId, FolderContents>(prev);
next.delete(folderId);
return next;
});
@@ -214,11 +214,15 @@ const useDocumentTagging = ({
setLoading(true);
try {
const { data } = await apiClient.post('/documents/bulk/reanalyze', {
document_ids: targetIds,
force: true,
});
const queued = data?.queued ?? targetIds.length;
const response = await apiClient.post<{ queued?: number }>(
'/documents/bulk/reanalyze',
{
document_ids: targetIds,
force: true,
},
);
const payload = 'data' in response ? response.data : response;
const queued = typeof payload?.queued === 'number' ? payload.queued : targetIds.length;
setStatusMessage(
`Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`,
'success',
@@ -28,6 +28,7 @@ type UploadQueueItem = {
interface UploadResponse {
reused?: boolean;
document?: unknown;
folder?: { id?: FolderId };
}
interface ApiClient {
@@ -35,15 +36,19 @@ interface ApiClient {
get<T = { document?: unknown }>(url: string): Promise<{ data: T }>;
}
type StatusLevel = 'success' | 'info' | 'warning' | 'error' | string;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type SetStatusMessage = (message: string, level?: StatusLevel) => void;
type DropOverlayState = {
active: boolean;
folderName: string;
};
type FileSystemEntryLike = FileSystemFileEntryLike | FileSystemDirectoryEntryLike;
type FileSystemEntryLike = FileSystemEntry;
type ExtendedDataTransferItem = DataTransferItem & {
webkitGetAsEntry?: () => FileSystemEntryLike | null;
webkitGetAsEntry?: () => FileSystemEntry | null;
};
interface FileSystemDirectoryReaderLike {
@@ -98,6 +103,8 @@ interface UseDocumentUploadsArgs {
refreshCurrentFolder: () => Promise<void>;
setLoading: (state: boolean) => void;
shellRef: MutableRefObject<HTMLElement | null>;
notifyApiError?: NotifyApiError;
setStatusMessage?: SetStatusMessage;
}
interface UseDocumentUploadsResult {
@@ -127,6 +134,8 @@ const useDocumentUploads = ({
refreshCurrentFolder,
setLoading,
shellRef,
notifyApiError,
setStatusMessage,
}: UseDocumentUploadsArgs): UseDocumentUploadsResult => {
const [dropOverlayState, setDropOverlayState] = useState<DropOverlayState>({
active: false,
@@ -145,8 +154,8 @@ const useDocumentUploads = ({
const formData = new FormData();
formData.append('file', file, file.name);
if (targetFolderId && targetFolderId !== 'root') {
formData.append('folder_id', targetFolderId);
if (targetFolderId != null && targetFolderId !== 'root') {
formData.append('folder_id', String(targetFolderId));
}
try {
@@ -179,11 +188,13 @@ const useDocumentUploads = ({
};
}
const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
notifyApiError?.(error, message);
setStatusMessage?.(message, 'error');
const wrapped = Object.assign(new Error(message), { response: error.response });
throw wrapped;
}
},
[apiClient],
[apiClient, notifyApiError, setStatusMessage],
);
const appendQueueItems = useCallback((entries: FileEntry[], targetFolderId?: FolderId) => {
@@ -235,9 +246,13 @@ const useDocumentUploads = ({
segments: trimmedSegments,
};
const { data } = await apiClient.post('/folders/path', payload);
cache.set(cacheKey, data.folder.id);
return data.folder.id;
const { data } = await apiClient.post<{ folder?: { id?: FolderId | null } }>(
'/folders/path',
payload,
);
const resolvedId = (data?.folder?.id ?? null) as FolderId;
cache.set(cacheKey, resolvedId);
return resolvedId;
},
[apiClient],
);
@@ -284,7 +299,7 @@ const useDocumentUploads = ({
if (entry.isFile) {
const file = await new Promise<File>((resolve, reject) => {
try {
(entry as FileSystemFileEntryLike).file(resolve, reject);
(entry as unknown as FileSystemFileEntryLike).file(resolve, reject);
} catch (error) {
console.warn('[Uploads] entry.file failed', error);
reject(error as Error);
@@ -295,7 +310,7 @@ const useDocumentUploads = ({
}
if (entry.isDirectory) {
const nextAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors];
const reader = (entry as FileSystemDirectoryEntryLike).createReader();
const reader = (entry as unknown as FileSystemDirectoryEntryLike).createReader();
const entries = await readAllEntries(reader);
for (const child of entries) {
await walkEntry(child, nextAncestors);
@@ -506,7 +521,6 @@ const useDocumentUploads = ({
hasFiles,
defaultFolderName: DEFAULT_FOLDER_NAME,
dragCounterRef,
dropOverlayState,
setDropOverlayState,
});
@@ -57,6 +57,44 @@ const EntryType = Object.freeze({
const noop = () => {};
type Identifier = string | number;
type DocumentId = Identifier;
type FolderId = Identifier | 'root';
interface DocumentLike {
id?: DocumentId | null;
title?: string | null;
[key: string]: unknown;
}
interface FolderNode {
id: FolderId;
name?: string | null;
parentId?: FolderId | null;
children: FolderId[];
hasChildren?: boolean;
expanded?: boolean;
loaded?: boolean;
[key: string]: unknown;
}
interface FolderContentsEntry {
folder?: { id?: FolderId; name?: string | null } | null;
documents?: DocumentLike[];
subfolders?: Array<{ id?: FolderId; name?: string | null; [key: string]: unknown }>;
__includesDocuments?: boolean;
__sortField?: string | null;
__sortDirection?: string | null;
[key: string]: unknown;
}
interface TenantOption {
id?: Identifier | null;
name?: string | null;
slug?: string | null;
[key: string]: unknown;
}
interface UseDocumentsWorkspaceOptions {
documentsViewMode?: string;
documentsSortField?: string;
@@ -118,9 +156,31 @@ const useDocumentsWorkspace = ({
const routeFolderId = folderMatch?.params?.folderId || null;
const routeDocumentId = docMatch?.params?.documentId || null;
const previewDocumentId = routeDocumentId;
const { status: appStatus, token, tenant, tenants: tenantOptions = [] } = appState;
const tenantName = tenant?.name || tenant?.slug || null;
const currentTenantId = tenant?.id || null;
const {
status: appStatus,
token,
tenant,
tenants: tenantOptionsRaw = [],
} = appState;
const tenantRecord = (tenant ?? null) as TenantOption | null;
const tenantName: string | null = typeof tenantRecord?.name === 'string'
? tenantRecord.name
: typeof tenantRecord?.slug === 'string'
? tenantRecord.slug
: null;
const currentTenantId: Identifier | null = (() => {
const value = tenantRecord?.id;
if (typeof value === 'string' || typeof value === 'number') {
return value as Identifier;
}
return null;
})();
const tenantOptions: TenantOption[] = Array.isArray(tenantOptionsRaw)
? (tenantOptionsRaw as TenantOption[])
: [];
const { status, setStatusMessage } = useDocumentsStore();
const handleApiReport = useCallback(
({ message, variant }) => setStatusMessage(message, variant),
@@ -172,9 +232,9 @@ const useDocumentsWorkspace = ({
documentsRouteMatch || documentsFolderRouteMatch || documentsDetailRouteMatch,
);
const [draggedDocumentIds, setDraggedDocumentIds] = useState([]);
const [draggedFolderId, setDraggedFolderId] = useState(null);
const [activePreviewId, setActivePreviewId] = useState(routeDocumentId || null);
const [draggedDocumentIds, setDraggedDocumentIds] = useState<DocumentId[]>([]);
const [draggedFolderId, setDraggedFolderId] = useState<FolderId | null>(null);
const [activePreviewId, setActivePreviewId] = useState<DocumentId | null>(routeDocumentId || null);
const shellRef = useRef(null);
const assetManagerRef = useRef(null);
if (!assetManagerRef.current) {
@@ -249,7 +309,9 @@ const useDocumentsWorkspace = ({
],
);
const [folderContents, setFolderContents] = useState(() => new Map());
const [folderContents, setFolderContents] = useState<Map<FolderId, FolderContentsEntry>>(
() => new Map(),
);
const folderContentsRef = useRef(folderContents);
useEffect(() => {
folderContentsRef.current = folderContents;
@@ -363,6 +425,17 @@ const useDocumentsWorkspace = ({
setActivePreviewId,
});
const openDocumentPreviewForDetail = useCallback(
({ documentIds }: { documentIds?: Identifier[] } = {}) => {
const targetId = documentIds?.find((value): value is Identifier => value != null);
if (targetId == null) {
return;
}
openDocumentPreview(targetId, { replace: true });
},
[openDocumentPreview],
);
const getDocumentAsset = useCallback((doc, type) => {
if (!doc || !type) return null;
return getAssetFromVersion(doc.current_version || null, type);
@@ -688,12 +761,13 @@ const useDocumentsWorkspace = ({
const removeDocumentsFromCaches = useCallback(
(documentIds) => {
if (!documentIds || documentIds.length === 0) {
(documentIds: DocumentId[] | null | undefined) => {
const safeIds = Array.isArray(documentIds) ? documentIds : [];
if (!safeIds.length) {
return;
}
const idSet = new Set(documentIds);
const idSet = new Set<DocumentId>(safeIds);
setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id)));
setSearchResults((prev) => {
@@ -704,12 +778,12 @@ const useDocumentsWorkspace = ({
return filtered.length === prev.length ? prev : filtered;
});
setFolderContents((prev) => {
setFolderContents((prev: Map<FolderId, FolderContentsEntry>) => {
if (!prev.size) {
return prev;
}
let changed = false;
const next = new Map();
const next = new Map<FolderId, FolderContentsEntry>();
prev.forEach((contents, key) => {
const docs = Array.isArray(contents?.documents) ? contents.documents : null;
if (!docs || docs.length === 0) {
@@ -1177,7 +1251,6 @@ const useDocumentsWorkspace = ({
documents,
searchResults,
previewDocuments,
focusedDocumentId,
selectionOrder,
selectedDocumentIds,
documentLookup,
@@ -1188,8 +1261,7 @@ const useDocumentsWorkspace = ({
previewEntries,
previewDocumentId,
activePreviewId,
openDocumentPreview,
promoteSelectionOrder,
openDocumentPreview: openDocumentPreviewForDetail,
handleDocumentTitleUpdate,
handleDocumentIssuedUpdate,
handleDocumentTagAdd,
@@ -1206,6 +1278,17 @@ const useDocumentsWorkspace = ({
tagLookupById,
});
const inspectDocumentForDesk = useCallback(
(doc: DocumentLike | null) => {
const docId = doc?.id;
if (docId == null) {
return;
}
inspectDocument(docId);
},
[inspectDocument],
);
const handleEntryPointerCore = useEntryPointerCore({
resolveDocumentRowKey,
resolveFolderRowKey,
@@ -1426,7 +1509,7 @@ const useDocumentsWorkspace = ({
handleDocumentsViewModeChange,
handleDeskExit: handleDeskExitSafe,
refreshCurrentFolder,
inspectDocument,
inspectDocument: inspectDocumentForDesk,
handleEntryPointerCore,
promoteSelectionOrder,
currentTenantId,
+4 -2
View File
@@ -1,5 +1,7 @@
import { MutableRefObject, useEffect } from 'react';
type FolderId = string | number | 'root' | null;
interface DropOverlayState {
active: boolean;
folderName: string | null;
@@ -9,8 +11,8 @@ interface UseFileDropOptions {
shellRef: MutableRefObject<HTMLElement | null>;
token?: string | null;
currentFolderName: string | null;
selectedFolder: string | null;
handleFileDrop: (dataTransfer: DataTransfer, folderId: string | null) => Promise<void>;
selectedFolder: FolderId;
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderId) => Promise<void>;
hasFiles: (event: DragEvent) => boolean;
defaultFolderName: string;
dragCounterRef: MutableRefObject<number>;
+128 -41
View File
@@ -1,4 +1,5 @@
import { useCallback, useMemo, useState } from 'react';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import {
DEFAULT_FOLDER_NAME,
createRootNode,
@@ -9,6 +10,80 @@ import {
resolveFolderRowKey,
} from '../../app/appLayoutUtils';
type Identifier = string | number;
type FolderId = Identifier | 'root';
interface DocumentLike {
id?: Identifier | null;
[key: string]: unknown;
}
interface FolderSummary {
id?: FolderId;
name?: string;
parent_id?: FolderId | null;
parentId?: FolderId | null;
children?: FolderId[];
subfolders?: FolderSummary[];
has_children?: boolean;
hasChildren?: boolean;
[key: string]: unknown;
}
interface FolderContentsEntry {
folder?: FolderSummary | null;
documents?: DocumentLike[];
subfolders?: FolderSummary[];
__includesDocuments?: boolean;
__sortField?: string | null;
__sortDirection?: string | null;
[key: string]: unknown;
}
interface FolderTreeNode extends FolderSummary {
id: FolderId;
children: FolderId[];
expanded?: boolean;
loaded?: boolean;
hasChildren?: boolean;
}
interface AssetManagerLike {
hydrateDocuments: (docs: DocumentLike[]) => DocumentLike[];
hydrateFolderContents: (payload: FolderContentsEntry) => FolderContentsEntry;
}
interface ApiClient {
get<T = FolderContentsEntry>(path: string, config?: { params?: Record<string, unknown> }): Promise<{ data: T }>;
}
interface SelectionHelpers {
focusedDocumentId: Identifier | null;
setFocusedDocumentId: Dispatch<SetStateAction<Identifier | null>>;
setSelectedEntries: Dispatch<SetStateAction<string[]>>;
setSelectionOrder: Dispatch<SetStateAction<string[]>>;
selectionOrderRef: MutableRefObject<string[] | null>;
selectionAnchorRef: MutableRefObject<string | null>;
}
interface UseFolderTreeOptions {
initialSelectedFolder?: FolderId;
assetManager: AssetManagerLike;
apiClient: ApiClient;
tenantIdRef: MutableRefObject<Identifier | null>;
documentsSortFieldRef: MutableRefObject<string>;
documentsSortDirectionRef: MutableRefObject<string>;
selectionHelpers: SelectionHelpers;
setDocuments: Dispatch<SetStateAction<DocumentLike[]>>;
setFolderContents: Dispatch<SetStateAction<Map<FolderId, FolderContentsEntry>>>;
folderContentsRef: MutableRefObject<Map<FolderId, FolderContentsEntry>>;
}
interface FolderOption {
id: FolderId;
label: string;
}
const useFolderTree = ({
initialSelectedFolder = 'root',
assetManager,
@@ -20,15 +95,15 @@ const useFolderTree = ({
setDocuments,
setFolderContents,
folderContentsRef,
}) => {
const [folderNodes, setFolderNodes] = useState(() => {
const rootNode = createRootNode();
}: UseFolderTreeOptions) => {
const [folderNodes, setFolderNodes] = useState<Map<FolderId, FolderTreeNode>>(() => {
const rootNode = createRootNode() as FolderTreeNode;
return new Map([[rootNode.id, rootNode]]);
});
const [selectedFolder, setSelectedFolder] = useState(initialSelectedFolder || 'root');
const [currentFolder, setCurrentFolder] = useState(null);
const [currentSubfolders, setCurrentSubfolders] = useState([]);
const [selectedFolder, setSelectedFolder] = useState<FolderId>(initialSelectedFolder || 'root');
const [currentFolder, setCurrentFolder] = useState<FolderSummary | null>(null);
const [currentSubfolders, setCurrentSubfolders] = useState<FolderSummary[]>([]);
const {
focusedDocumentId,
@@ -40,8 +115,8 @@ const useFolderTree = ({
} = selectionHelpers;
const applySelectedFolder = useCallback(
(folderId, contents) => {
const subfolders = contents?.subfolders ?? [];
(folderId: FolderId, contents?: FolderContentsEntry | null) => {
const subfolders = Array.isArray(contents?.subfolders) ? contents.subfolders : [];
const docs = assetManager.hydrateDocuments(contents?.documents ?? []);
const folderInfo = contents?.folder ?? null;
@@ -50,12 +125,12 @@ const useFolderTree = ({
setCurrentFolder(folderInfo);
const availableDocKeys = docs
.map((doc) => resolveDocumentRowKey(doc.id))
.map((doc) => resolveDocumentRowKey(doc?.id as Identifier))
.filter(Boolean);
const availableDocKeySet = new Set(availableDocKeys);
const availableFolderKeys = new Set(
subfolders
.map((folder) => resolveFolderRowKey(folder.id))
.map((folder) => resolveFolderRowKey(folder?.id as Identifier))
.filter(Boolean),
);
@@ -102,20 +177,20 @@ const useFolderTree = ({
],
);
const expandFolderAncestors = useCallback((targetId) => {
const expandFolderAncestors = useCallback((targetId: FolderId | null) => {
if (!targetId || targetId === 'root') {
setFolderNodes((prev) => {
setFolderNodes((prev: Map<FolderId, FolderTreeNode>) => {
const root = prev.get('root');
if (root?.expanded) return prev;
const next = new Map(prev);
const next = new Map<FolderId, FolderTreeNode>(prev);
next.set('root', { ...root, expanded: true });
return next;
});
return;
}
setFolderNodes((prev) => {
const next = new Map(prev);
setFolderNodes((prev: Map<FolderId, FolderTreeNode>) => {
const next = new Map<FolderId, FolderTreeNode>(prev);
let currentId = targetId;
let guard = 0;
while (currentId && guard < 32) {
@@ -133,15 +208,21 @@ const useFolderTree = ({
const ensureFolderData = useCallback(
async (
folderId,
folderId: FolderId,
{
includeDocuments = true,
prefetchDepth = 0,
force = false,
sortField = documentsSortFieldRef.current,
sortDirection = documentsSortDirectionRef.current,
}: {
includeDocuments?: boolean;
prefetchDepth?: number;
force?: boolean;
sortField?: string;
sortDirection?: string;
} = {},
) => {
): Promise<FolderContentsEntry> => {
const requestTenantId = tenantIdRef.current;
const cached = folderContentsRef.current.get(folderId);
const cachedSortField = cached?.__sortField || documentsSortFieldRef.current;
@@ -168,7 +249,7 @@ const useFolderTree = ({
}
const path = folderId === 'root' ? 'root' : folderId;
const params = {};
const params: Record<string, unknown> = {};
if (!includeDocuments) {
params.include_documents = false;
} else {
@@ -176,10 +257,12 @@ const useFolderTree = ({
params.dir = sortDirection;
}
const requestConfig = Object.keys(params).length ? { params } : {};
const { data } = await apiClient.get(`/folders/${path}/contents`, requestConfig);
const { data } = await apiClient.get<FolderContentsEntry>(`/folders/${path}/contents`, requestConfig);
const hydrated = assetManager.hydrateFolderContents(data);
const childFolders = Array.isArray(data.subfolders) ? data.subfolders : [];
const childIds = childFolders.map((child) => child.id);
const childIds = childFolders
.map((child) => (child?.id ?? null) as FolderId | null)
.filter((id): id is FolderId => Boolean(id));
const enriched = {
...hydrated,
@@ -192,8 +275,8 @@ const useFolderTree = ({
return enriched;
}
setFolderNodes((prev) => {
const next = new Map(prev);
setFolderNodes((prev: Map<FolderId, FolderTreeNode>) => {
const next = new Map<FolderId, FolderTreeNode>(prev);
const existingNode = next.get(folderId) || {
id: folderId,
name: folderId === 'root' ? DEFAULT_FOLDER_NAME : data.folder?.name || 'Folder',
@@ -215,7 +298,11 @@ const useFolderTree = ({
});
childFolders.forEach((child) => {
const childNode = next.get(child.id);
const childId = (child?.id ?? null) as FolderId | null;
if (!childId) {
return;
}
const childNode = next.get(childId);
const previousChildren = Array.isArray(childNode?.children) ? childNode.children : [];
const childHasChildren = (() => {
if (childNode?.loaded) {
@@ -235,10 +322,10 @@ const useFolderTree = ({
}
return false;
})();
next.set(child.id, {
id: child.id,
next.set(childId, {
id: childId,
name: child.name,
parentId: child.parent_id ?? 'root',
parentId: (child.parent_id ?? 'root') as FolderId,
children: previousChildren,
expanded: childNode?.expanded ?? false,
loaded: childNode?.loaded ?? false,
@@ -261,11 +348,11 @@ const useFolderTree = ({
);
}
setFolderContents((prev) => {
setFolderContents((prev: Map<FolderId, FolderContentsEntry>) => {
if (tenantIdRef.current !== requestTenantId) {
return prev;
}
const next = new Map(prev);
const next = new Map<FolderId, FolderContentsEntry>(prev);
if (includeDocuments) {
next.set(folderId, enriched);
} else {
@@ -302,7 +389,7 @@ const useFolderTree = ({
);
const ensureFolderAncestorsLoaded = useCallback(
async (targetId) => {
async (targetId: FolderId | null) => {
if (!targetId || targetId === 'root') {
return;
}
@@ -323,7 +410,7 @@ const useFolderTree = ({
);
const isInvalidFolderDrop = useCallback(
(sourceId, targetId) => {
(sourceId: FolderId | null, targetId: FolderId | null) => {
if (!sourceId) return false;
if (!targetId || targetId === 'root') {
return false;
@@ -349,9 +436,9 @@ const useFolderTree = ({
);
const resetFolderTreeState = useCallback(() => {
const rootNode = createRootNode();
setFolderNodes(new Map([[rootNode.id, rootNode]]));
setFolderContents(new Map());
const rootNode = createRootNode() as FolderTreeNode;
setFolderNodes(new Map<FolderId, FolderTreeNode>([[rootNode.id, rootNode]]));
setFolderContents(new Map<FolderId, FolderContentsEntry>());
setSelectedFolder('root');
setCurrentFolder(null);
setCurrentSubfolders([]);
@@ -362,11 +449,11 @@ const useFolderTree = ({
return currentFolder.name;
}, [selectedFolder, currentFolder]);
const folderOptions = useMemo(() => {
const cache = new Map();
const computePath = (id) => {
if (cache.has(id)) {
return cache.get(id);
const folderOptions: FolderOption[] = useMemo(() => {
const cache = new Map<FolderId, string>();
const computePath = (id: FolderId | null): string => {
if (cache.has(id as FolderId)) {
return cache.get(id as FolderId) as string;
}
if (!id || id === 'root') {
cache.set('root', DEFAULT_FOLDER_NAME);
@@ -376,7 +463,7 @@ const useFolderTree = ({
if (!node) {
return 'Folder';
}
const parentId = node.parentId || 'root';
const parentId = (node.parentId || 'root') as FolderId;
const parentPath = computePath(parentId);
const name = node.name || 'Folder';
const fullPath = parentId === 'root' ? name : `${parentPath}/${name}`;
@@ -384,7 +471,7 @@ const useFolderTree = ({
return fullPath;
};
const entries = [];
const entries: FolderOption[] = [];
folderNodes.forEach((node, id) => {
if (!node) return;
entries.push({ id, label: computePath(id) });
@@ -400,7 +487,7 @@ const useFolderTree = ({
}, [folderNodes]);
const folderLabelMap = useMemo(() => {
const map = new Map();
const map = new Map<FolderId, string>();
folderOptions.forEach((option) => {
map.set(option.id, option.label);
});
@@ -4,7 +4,7 @@ import type { NavigateFunction } from 'react-router-dom';
interface ApiClient {
get: (path: string) => Promise<{ data: unknown }>;
post: (path: string, body?: unknown) => Promise<{ data: any }>;
defaults: { headers: { common: Record<string, string> } };
defaults: { headers: { common: Record<string, unknown> } };
}
interface TenantOption {