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
+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);
});