"use client"; import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { CheckOutlined, CloseOutlined } from "@ant-design/icons"; import { Button, Empty, Modal, TreeSelect, message } from "antd"; import { useI18n } from "@/i18n"; import { authFetch } from "@/utils/authFetch"; import { getUserFileList, ROOT_PARENT_ID, USER_FILE_PAGE_SIZE, type UserFileItem, type UserFileListResponse, type UserFileMutationResponse, } from "@/utils/userFileLibrary"; interface SaveToAssetLibraryModalProps { open: boolean; mediaUrl: string | null; onClose: () => void; onSaved?: (folderId: number) => void | Promise; } interface FolderTreeNode { title: ReactNode; value: number; key: number; isLeaf: boolean; children?: FolderTreeNode[]; } function SaveToAssetLibraryModalHeader({ title, onClose, }: { title: string; onClose: () => void; }) { const { t } = useI18n(); return (

{title}

); } export function SaveToAssetLibraryModal({ open, mediaUrl, onClose, onSaved, }: SaveToAssetLibraryModalProps) { const { t } = useI18n(); const [foldersByParentId, setFoldersByParentId] = useState>({}); const [loadedFolderIds, setLoadedFolderIds] = useState>(new Set()); const [loadingFolderIds, setLoadingFolderIds] = useState>(new Set()); const [selectedFolderId, setSelectedFolderId] = useState(0); const [expandedFolderIds, setExpandedFolderIds] = useState<(string | number)[]>([]); const [submitting, setSubmitting] = useState(false); const rootFolders = foldersByParentId[ROOT_PARENT_ID] || []; const folderIdExists = useCallback((folderId: number) => { if (folderId === ROOT_PARENT_ID) return false; return Object.values(foldersByParentId).some((folders) => folders.some((folder) => folder.id === folderId)); }, [foldersByParentId]); const loadFolderChildren = useCallback(async (folderId: number) => { setLoadingFolderIds((current) => new Set(current).add(folderId)); try { const params = new URLSearchParams({ parentId: String(folderId), keyword: "", page: "1", pageSize: String(USER_FILE_PAGE_SIZE), }); const response = await authFetch(`/api/popi/user-file/list?${params.toString()}`); const payload = (await response.json().catch(() => null)) as UserFileListResponse | null; if (!response.ok || !payload?.success) { throw new Error(payload?.error || t("assetLibrary.loadFailed")); } const folders = getUserFileList(payload.data).filter((item) => item.nodeType === "folder"); setFoldersByParentId((current) => ({ ...current, [folderId]: folders })); setLoadedFolderIds((current) => new Set(current).add(folderId)); } catch (err) { const messageText = err instanceof Error ? err.message : t("assetLibrary.loadFailed"); void message.error(messageText); } finally { setLoadingFolderIds((current) => { const next = new Set(current); next.delete(folderId); return next; }); } }, [t]); useEffect(() => { if (!open) return; setFoldersByParentId({}); setLoadedFolderIds(new Set()); setLoadingFolderIds(new Set()); setSelectedFolderId(0); setExpandedFolderIds([]); setSubmitting(false); void loadFolderChildren(ROOT_PARENT_ID); }, [loadFolderChildren, open]); useEffect(() => { if (!open || selectedFolderId !== 0 || rootFolders.length === 0) return; setSelectedFolderId(rootFolders[0].id); }, [open, rootFolders, selectedFolderId]); const treeData = useMemo(() => { const buildNodes = (parentId: number): FolderTreeNode[] => { const folders = foldersByParentId[parentId] || []; return folders.map((folder) => { const loaded = loadedFolderIds.has(folder.id); const children = foldersByParentId[folder.id] || []; return { title: ( {folder.name} {selectedFolderId === folder.id ? : null} ), value: folder.id, key: folder.id, isLeaf: loaded && children.length === 0, children: loaded && children.length > 0 ? buildNodes(folder.id) : undefined, }; }); }; return buildNodes(ROOT_PARENT_ID); }, [foldersByParentId, loadedFolderIds, selectedFolderId]); const handleTreeExpand = useCallback((expandedKeys: (string | number)[]) => { setExpandedFolderIds(expandedKeys); expandedKeys.forEach((key) => { const folderId = Number(key); if (Number.isInteger(folderId) && folderId > 0) { void loadFolderChildren(folderId); } }); }, [loadFolderChildren]); const handleConfirm = useCallback(async () => { if (submitting || !mediaUrl || !folderIdExists(selectedFolderId)) return; setSubmitting(true); try { const response = await authFetch("/api/popi/user-file/upload", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parentId: selectedFolderId, urlList: [mediaUrl], }), }); const payload = (await response.json().catch(() => null)) as UserFileMutationResponse | null; if (!response.ok || !payload?.success) { throw new Error(payload?.error || t("assetLibrary.saveToLibraryFailed")); } await onSaved?.(selectedFolderId); void message.success(t("assetLibrary.saveToLibrarySuccess")); onClose(); } catch (err) { const messageText = err instanceof Error ? err.message : t("assetLibrary.saveToLibraryFailed"); void message.error(messageText); } finally { setSubmitting(false); } }, [folderIdExists, mediaUrl, onClose, onSaved, selectedFolderId, submitting, t]); const canConfirm = Boolean(mediaUrl) && folderIdExists(selectedFolderId) && !submitting; return (
{ if (!submitting) onClose(); }} />
{t("assetLibrary.uploadLocation")}
{rootFolders.length === 0 && loadedFolderIds.has(ROOT_PARENT_ID) ? (
{t("assetLibrary.noFolders")}} />
) : ( Promise.resolve(loadFolderChildren(Number(node.value)))} loading={loadingFolderIds.size > 0} disabled={submitting} onChange={setSelectedFolderId} placeholder={t("assetLibrary.selectFolder")} treeDefaultExpandAll={false} treeExpandedKeys={expandedFolderIds} onTreeExpand={handleTreeExpand} className="w-full [&_.ant-select-selector]:!border-[var(--border-subtle)] [&_.ant-select-selector]:!bg-[var(--surface-2)] [&_.ant-select-selection-item]:!text-[var(--text-primary)] [&_.ant-select-selection-placeholder]:!text-[var(--text-muted)]" popupClassName="nodrag nopan nowheel" /> )}
); }