typescript
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user