|
|
|
@ -39,7 +39,6 @@ import { |
|
|
|
import { AssetLibraryUploadModal, type AssetLibraryUploadFolderOption } from "./AssetLibraryUploadModal"; |
|
|
|
|
|
|
|
type AssetLibraryFileKind = "image" | "video" | "audio" | "other"; |
|
|
|
type AssetLibraryActionMenuView = "actions" | "move"; |
|
|
|
|
|
|
|
interface AssetLibraryManagerModalProps { |
|
|
|
open: boolean; |
|
|
|
@ -158,7 +157,7 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
const [expandedMoveFolderIds, setExpandedMoveFolderIds] = useState<Set<number>>(new Set()); |
|
|
|
const [moveFolderAncestorIds, setMoveFolderAncestorIds] = useState<Record<number, Set<number>>>({}); |
|
|
|
const [actionMenuOpenId, setActionMenuOpenId] = useState<number | null>(null); |
|
|
|
const [actionMenuView, setActionMenuView] = useState<AssetLibraryActionMenuView>("actions"); |
|
|
|
const [movePanelOpenId, setMovePanelOpenId] = useState<number | null>(null); |
|
|
|
const [batchMode, setBatchMode] = useState(false); |
|
|
|
const [selectedItemIds, setSelectedItemIds] = useState<Set<number>>(new Set()); |
|
|
|
const [batchDeleting, setBatchDeleting] = useState(false); |
|
|
|
@ -213,7 +212,7 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
setExpandedMoveFolderIds(new Set()); |
|
|
|
setMoveFolderAncestorIds({}); |
|
|
|
setActionMenuOpenId(null); |
|
|
|
setActionMenuView("actions"); |
|
|
|
setMovePanelOpenId(null); |
|
|
|
setBatchMode(false); |
|
|
|
setSelectedItemIds(new Set()); |
|
|
|
setBatchDeleting(false); |
|
|
|
@ -331,7 +330,7 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
await loadItems(); |
|
|
|
await onChanged?.(parentId); |
|
|
|
setActionMenuOpenId(null); |
|
|
|
setActionMenuView("actions"); |
|
|
|
setMovePanelOpenId(null); |
|
|
|
void message.success(t("assetLibrary.moveSuccess")); |
|
|
|
} catch (err) { |
|
|
|
const messageText = err instanceof Error ? err.message : t("assetLibrary.moveFailed"); |
|
|
|
@ -578,7 +577,7 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
const isSelfOrDescendant = item.nodeType === "folder" && ( |
|
|
|
folder.id === item.id || Boolean(moveFolderAncestorIds[folder.id]?.has(item.id)) |
|
|
|
); |
|
|
|
const disabled = isSelfOrDescendant; |
|
|
|
const disabled = isSelfOrDescendant || isCurrentParent; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div key={folder.id}> |
|
|
|
@ -604,10 +603,6 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
if (disabled) return; |
|
|
|
if (isCurrentParent) { |
|
|
|
setActionMenuOpenId(null); |
|
|
|
return; |
|
|
|
} |
|
|
|
void handleMoveItem(item, folder.id); |
|
|
|
}} |
|
|
|
> |
|
|
|
@ -650,20 +645,18 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
const rootFolders = moveFoldersByParentId[ROOT_PARENT_ID] ?? []; |
|
|
|
const rootLoading = loadingMoveFolderIds.has(ROOT_PARENT_ID); |
|
|
|
return ( |
|
|
|
<div className="max-h-[280px] min-w-[220px] overflow-y-auto p-1 [scrollbar-color:var(--text-muted)_transparent] [scrollbar-width:thin]"> |
|
|
|
{item.nodeType === "folder" && ( |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
disabled={item.parentId === ROOT_PARENT_ID} |
|
|
|
className="flex h-8 w-full items-center rounded-md px-3 text-left text-sm text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:bg-transparent disabled:hover:text-[var(--text-secondary)]" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
if (item.parentId !== ROOT_PARENT_ID) void handleMoveItem(item, ROOT_PARENT_ID); |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("assetLibrary.rootFolder")} |
|
|
|
</button> |
|
|
|
)} |
|
|
|
<div className="max-h-[320px] w-[228px] overflow-y-auto p-1 [scrollbar-color:var(--text-muted)_transparent] [scrollbar-width:thin]"> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
disabled={item.parentId === ROOT_PARENT_ID} |
|
|
|
className="flex h-8 w-full items-center rounded-md px-3 text-left text-sm text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:bg-transparent disabled:hover:text-[var(--text-secondary)]" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
if (item.parentId !== ROOT_PARENT_ID) void handleMoveItem(item, ROOT_PARENT_ID); |
|
|
|
}} |
|
|
|
> |
|
|
|
{t("assetLibrary.rootFolder")} |
|
|
|
</button> |
|
|
|
{rootLoading ? ( |
|
|
|
<div className="px-3 py-2 text-sm text-[var(--text-muted)]">{t("common.processing")}</div> |
|
|
|
) : rootFolders.length > 0 ? ( |
|
|
|
@ -675,90 +668,85 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
); |
|
|
|
}, [handleMoveItem, loadingMoveFolderIds, moveFoldersByParentId, renderMoveTreeRows, t]); |
|
|
|
|
|
|
|
const handleOpenMovePanel = useCallback((item: UserFileItem) => { |
|
|
|
setMovePanelOpenId(item.id); |
|
|
|
void loadMoveFolderChildren(ROOT_PARENT_ID); |
|
|
|
}, [loadMoveFolderChildren]); |
|
|
|
|
|
|
|
const renderItemActions = useCallback((item: UserFileItem) => { |
|
|
|
const isFolder = item.nodeType === "folder"; |
|
|
|
const isMoveView = actionMenuView === "move"; |
|
|
|
const movePanelOpen = movePanelOpenId === item.id; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div |
|
|
|
className="nodrag nopan nowheel min-w-[168px] rounded-lg bg-[var(--surface-2)] p-1 text-sm text-[var(--text-secondary)] shadow-[var(--shadow-panel)]" |
|
|
|
className="nodrag nopan nowheel relative min-w-[168px] rounded-lg bg-[var(--surface-2)] p-1 text-sm text-[var(--text-secondary)] shadow-[var(--shadow-panel)]" |
|
|
|
onClick={(event) => event.stopPropagation()} |
|
|
|
> |
|
|
|
{isMoveView ? ( |
|
|
|
<> |
|
|
|
<div className="mb-1 flex h-8 items-center gap-1 px-1"> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
aria-label={t("common.back")} |
|
|
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-[var(--text-muted)] transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" |
|
|
|
onClick={() => setActionMenuView("actions")} |
|
|
|
> |
|
|
|
<LeftOutlined className="text-[12px]" /> |
|
|
|
</button> |
|
|
|
<span className="min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]"> |
|
|
|
{t("assetLibrary.moveTitle")} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
{renderMovePanel(item)} |
|
|
|
</> |
|
|
|
) : ( |
|
|
|
<div className="flex flex-col"> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="flex h-8 w-full items-center gap-2 rounded-md px-3 text-left transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" |
|
|
|
onClick={() => { |
|
|
|
setActionMenuView("move"); |
|
|
|
void loadMoveFolderChildren(ROOT_PARENT_ID); |
|
|
|
}} |
|
|
|
> |
|
|
|
<SwapOutlined className="text-[14px]" /> |
|
|
|
<span className="flex-1">{t("assetLibrary.moveTitle")}</span> |
|
|
|
<RightOutlined className="text-[10px]" /> |
|
|
|
</button> |
|
|
|
{isFolder && ( |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="flex h-8 items-center gap-2 rounded-md px-3 text-left transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
void message.info(t("assetLibrary.changeCoverUnsupported")); |
|
|
|
}} |
|
|
|
> |
|
|
|
<PictureOutlined className="text-[14px]" /> |
|
|
|
<span>{t("assetLibrary.changeCover")}</span> |
|
|
|
</button> |
|
|
|
)} |
|
|
|
<div className="flex flex-col"> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className={[ |
|
|
|
"flex h-8 w-full items-center gap-2 rounded-md px-3 text-left transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]", |
|
|
|
movePanelOpen ? "bg-[var(--surface-hover)] text-[var(--text-primary)]" : "", |
|
|
|
].join(" ")} |
|
|
|
onMouseEnter={() => handleOpenMovePanel(item)} |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
handleOpenMovePanel(item); |
|
|
|
}} |
|
|
|
> |
|
|
|
<SwapOutlined className="text-[14px]" /> |
|
|
|
<span className="flex-1">{t("assetLibrary.moveTitle")}</span> |
|
|
|
<RightOutlined className="text-[10px]" /> |
|
|
|
</button> |
|
|
|
{isFolder && ( |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="flex h-8 items-center gap-2 rounded-md px-3 text-left transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
setActionMenuOpenId(null); |
|
|
|
setActionMenuView("actions"); |
|
|
|
openRenameModal(item); |
|
|
|
void message.info(t("assetLibrary.changeCoverUnsupported")); |
|
|
|
}} |
|
|
|
> |
|
|
|
<EditOutlined className="text-[14px]" /> |
|
|
|
<span>{t("assetLibrary.rename")}</span> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="flex h-8 items-center gap-2 rounded-md px-3 text-left text-red-400 transition-colors hover:bg-red-500/10 hover:text-red-300" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
setActionMenuOpenId(null); |
|
|
|
setActionMenuView("actions"); |
|
|
|
handleDeleteItem(item); |
|
|
|
}} |
|
|
|
> |
|
|
|
<DeleteOutlined className="text-[14px]" /> |
|
|
|
<span>{t("assetLibrary.delete")}</span> |
|
|
|
<PictureOutlined className="text-[14px]" /> |
|
|
|
<span>{t("assetLibrary.changeCover")}</span> |
|
|
|
</button> |
|
|
|
)} |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="flex h-8 items-center gap-2 rounded-md px-3 text-left transition-colors hover:bg-[var(--surface-hover)] hover:text-[var(--text-primary)]" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
setActionMenuOpenId(null); |
|
|
|
setMovePanelOpenId(null); |
|
|
|
openRenameModal(item); |
|
|
|
}} |
|
|
|
> |
|
|
|
<EditOutlined className="text-[14px]" /> |
|
|
|
<span>{t("assetLibrary.rename")}</span> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="flex h-8 items-center gap-2 rounded-md px-3 text-left text-red-400 transition-colors hover:bg-red-500/10 hover:text-red-300" |
|
|
|
onClick={(event) => { |
|
|
|
event.stopPropagation(); |
|
|
|
setActionMenuOpenId(null); |
|
|
|
setMovePanelOpenId(null); |
|
|
|
handleDeleteItem(item); |
|
|
|
}} |
|
|
|
> |
|
|
|
<DeleteOutlined className="text-[14px]" /> |
|
|
|
<span>{t("assetLibrary.delete")}</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
{movePanelOpen && ( |
|
|
|
<div className="absolute top-0 left-full z-20 ml-2 rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-2)] text-sm text-[var(--text-secondary)] shadow-[var(--shadow-panel)]"> |
|
|
|
{renderMovePanel(item)} |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
); |
|
|
|
}, [actionMenuView, handleDeleteItem, loadMoveFolderChildren, openRenameModal, renderMovePanel, t]); |
|
|
|
}, [handleDeleteItem, handleOpenMovePanel, movePanelOpenId, openRenameModal, renderMovePanel, t]); |
|
|
|
|
|
|
|
const renderItem = (item: UserFileItem) => { |
|
|
|
const isFolder = item.nodeType === "folder"; |
|
|
|
@ -848,7 +836,7 @@ export function AssetLibraryManagerModal({ open, onClose, onChanged }: AssetLibr |
|
|
|
open={actionMenuOpenId === item.id} |
|
|
|
onOpenChange={(nextOpen) => { |
|
|
|
setActionMenuOpenId(nextOpen ? item.id : null); |
|
|
|
setActionMenuView("actions"); |
|
|
|
if (!nextOpen) setMovePanelOpenId(null); |
|
|
|
}} |
|
|
|
trigger={["click"]} |
|
|
|
placement="bottomRight" |
|
|
|
|