import { jsxs, jsx } from 'react/jsx-runtime'; import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react'; import { Typography, Box, Flex, Loader, IconButton, Tooltip } from '@strapi/design-system'; import { House, Folder, ChevronDown } from '@strapi/icons'; import { useIntl } from 'react-intl'; import { styled } from 'styled-components'; import { useGetFolderStructureQuery } from '../../../../services/folders.mjs'; import { getTranslationKey } from '../../../../utils/translations.mjs'; /* ------------------------------------------------------------------------------------------------- * RowButton — shared row styling aligned with admin SubNav.Link * -----------------------------------------------------------------------------------------------*/ const RowButton = styled.button` display: flex; align-items: center; gap: ${({ theme })=>theme.spaces[2]}; width: 100%; min-height: 3.2rem; padding: ${({ theme })=>`${theme.spaces[1]} ${theme.spaces[2]}`}; border: 0; background: ${({ $isActive, theme })=>$isActive ? theme.colors.primary100 : 'transparent'}; color: ${({ $isActive, theme })=>$isActive ? theme.colors.primary700 : theme.colors.neutral800}; border-radius: ${({ theme })=>theme.borderRadius}; cursor: pointer; text-align: left; font: inherit; &:hover { background: ${({ $isActive, theme })=>$isActive ? theme.colors.primary100 : theme.colors.neutral100}; } &:focus-visible { outline: 2px solid ${({ theme })=>theme.colors.primary600}; outline-offset: -2px; } `; /* ------------------------------------------------------------------------------------------------- * useExpandedFolders — local expand/collapse state * -----------------------------------------------------------------------------------------------*/ const findAncestorIds = (nodes, targetId, trail = [])=>{ for (const node of nodes){ if (node.id === targetId) { return trail; } if (node.children?.length) { const nextTrail = node.id != null ? [ ...trail, node.id ] : trail; const found = findAncestorIds(node.children, targetId, nextTrail); if (found !== null) { return found; } } } return null; }; const useExpandedFolders = (folderStructure, currentFolderId)=>{ const [expandedIds, setExpandedIds] = useState(()=>new Set()); useEffect(()=>{ if (currentFolderId == null) { return; } const ancestors = findAncestorIds(folderStructure, currentFolderId); if (!ancestors || ancestors.length === 0) { return; } setExpandedIds((prev)=>{ const next = new Set(prev); let changed = false; for (const id of ancestors){ if (!next.has(id)) { next.add(id); changed = true; } } return changed ? next : prev; }); }, [ folderStructure, currentFolderId ]); const toggleExpanded = useCallback((id)=>{ setExpandedIds((prev)=>{ const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); const isExpanded = useCallback((id)=>expandedIds.has(id), [ expandedIds ]); return { isExpanded, toggleExpanded }; }; /* ------------------------------------------------------------------------------------------------- * TruncatedFolderName — tooltip when the label is ellipsized * -----------------------------------------------------------------------------------------------*/ const TruncatedFolderName = ({ name, isActive })=>{ const textRef = useRef(null); const [isTruncated, setIsTruncated] = useState(false); useLayoutEffect(()=>{ const el = textRef.current; if (!el) { return; } const checkTruncation = ()=>{ setIsTruncated(el.scrollWidth > el.clientWidth); }; checkTruncation(); const observer = new ResizeObserver(checkTruncation); observer.observe(el); return ()=>observer.disconnect(); }, [ name ]); const label = /*#__PURE__*/ jsx(Typography, { ref: textRef, variant: "omega", fontWeight: isActive ? 'semiBold' : 'regular', ellipsis: true, children: name }); if (isTruncated) { return /*#__PURE__*/ jsx(Tooltip, { label: name, children: label }); } return label; }; /* ------------------------------------------------------------------------------------------------- * NavList — unstyled list for tree rows * -----------------------------------------------------------------------------------------------*/ const NavList = styled.ul` list-style: none; margin: 0; padding: 0; `; /* ------------------------------------------------------------------------------------------------- * FolderTreeItem — single tree row (internal) * -----------------------------------------------------------------------------------------------*/ const INDENT_PER_LEVEL_REM = 1.6; const RotatingChevron = styled(ChevronDown)` transform: rotate(${({ $expanded })=>$expanded ? '0deg' : '-90deg'}); transition: transform 0.2s ease; `; const FolderTreeItem = ({ node, level, currentFolderId, isExpanded, onToggle, onSelect })=>{ const { formatMessage } = useIntl(); if (node.id == null) { return null; } const id = node.id; const name = node.name ?? ''; const hasChildren = (node.children?.length ?? 0) > 0; const isFolderExpanded = isExpanded(id); const isActive = currentFolderId === id; // TODO: full `role="tree"` + arrow-key treeview navigation before revamp GA // if an accessibility audit requires it (CMS-133 v1 is button rows only). return /*#__PURE__*/ jsxs("li", { children: [ /*#__PURE__*/ jsxs(Flex, { alignItems: "center", paddingLeft: `${level * INDENT_PER_LEVEL_REM}rem`, gap: 1, children: [ /*#__PURE__*/ jsx(IconButton, { label: formatMessage({ id: getTranslationKey(isFolderExpanded ? 'sidebar.tree.collapse' : 'sidebar.tree.expand'), defaultMessage: isFolderExpanded ? 'Collapse {name}' : 'Expand {name}' }, { name }), onClick: (event)=>{ event.stopPropagation(); onToggle(id); }, variant: "ghost", withTooltip: false, "aria-expanded": isFolderExpanded, children: /*#__PURE__*/ jsx(RotatingChevron, { $expanded: isFolderExpanded, fill: "neutral500" }) }), /*#__PURE__*/ jsx(Box, { flex: "1", minWidth: 0, children: /*#__PURE__*/ jsx(RowButton, { type: "button", $isActive: isActive, "aria-current": isActive ? 'page' : undefined, onClick: ()=>onSelect(id), "data-testid": `folder-tree-node-${id}`, "data-folder-id": id, children: /*#__PURE__*/ jsx(TruncatedFolderName, { name: name, isActive: isActive }) }) }) ] }), hasChildren && isFolderExpanded && /*#__PURE__*/ jsx(NavList, { children: node.children.map((child)=>/*#__PURE__*/ jsx(FolderTreeItem, { node: child, level: level + 1, currentFolderId: currentFolderId, isExpanded: isExpanded, onToggle: onToggle, onSelect: onSelect }, child.id ?? child.name)) }) ] }); }; /* ------------------------------------------------------------------------------------------------- * FolderTree — public sidebar component * -----------------------------------------------------------------------------------------------*/ const SidebarNav = styled(Flex)` /* TODO: reconcile 25.6rem (Figma) with admin WIDTH_SIDE_NAVIGATION (23.2rem) */ width: 25.6rem; height: 100%; min-height: 100%; background: ${({ theme })=>theme.colors.neutral0}; flex-shrink: 0; flex-direction: column; border-right: 1px solid ${({ theme })=>theme.colors.neutral150}; `; const SidebarHeader = styled(Box)` flex-shrink: 0; border-bottom: 1px solid ${({ theme })=>theme.colors.neutral150}; `; const SidebarBody = styled(Flex)` flex: 1; min-height: 0; overflow-y: auto; `; /** * Left-rail navigation for the Media Library. Fetches folder structure internally * and renders: * * 1. A "Media library" title * 2. A "Home" entry that clears the folder query param * 3. A "FOLDERS" section header * 4. The folder tree itself * * Presentational with respect to routing — navigation is delegated to the parent * via `onSelectFolder` so the URL stays the single source of truth (see * `useFolderNavigation`). */ const FolderTree = ({ currentFolderId, onSelectFolder })=>{ const { formatMessage } = useIntl(); const { data: folderStructure = [], isLoading, isError } = useGetFolderStructureQuery(); const { isExpanded, toggleExpanded } = useExpandedFolders(folderStructure, currentFolderId); const isHomeActive = currentFolderId == null; return /*#__PURE__*/ jsxs(SidebarNav, { direction: "column", alignItems: "stretch", tag: "nav", "aria-label": formatMessage({ id: getTranslationKey('sidebar.tree.aria-label'), defaultMessage: 'Media library folders' }), children: [ /*#__PURE__*/ jsx(SidebarHeader, { paddingTop: 4, paddingBottom: 4, paddingLeft: 5, paddingRight: 5, children: /*#__PURE__*/ jsx(Typography, { variant: "beta", tag: "h2", children: formatMessage({ id: getTranslationKey('sidebar.title'), defaultMessage: 'Media library' }) }) }), /*#__PURE__*/ jsxs(SidebarBody, { direction: "column", alignItems: "stretch", gap: 1, padding: 3, children: [ /*#__PURE__*/ jsxs(RowButton, { type: "button", $isActive: isHomeActive, "aria-current": isHomeActive ? 'page' : undefined, onClick: ()=>onSelectFolder(null), "data-testid": "folder-tree-home", children: [ /*#__PURE__*/ jsx(House, { "aria-hidden": true, width: "1.6rem", height: "1.6rem" }), /*#__PURE__*/ jsx(Typography, { variant: "omega", fontWeight: isHomeActive ? 'semiBold' : 'regular', children: formatMessage({ id: getTranslationKey('sidebar.home'), defaultMessage: 'Home' }) }) ] }), /*#__PURE__*/ jsxs(Box, { children: [ /*#__PURE__*/ jsxs(Flex, { alignItems: "center", gap: 1, padding: 1, children: [ /*#__PURE__*/ jsx(Folder, { "aria-hidden": true, width: "1.6rem", height: "1.6rem", fill: "neutral500" }), /*#__PURE__*/ jsx(Typography, { variant: "sigma", textColor: "neutral600", style: { textTransform: 'uppercase' }, children: formatMessage({ id: getTranslationKey('sidebar.folders'), defaultMessage: 'Folders' }) }) ] }), isLoading ? // TODO: revisit loading state before revamp GA /*#__PURE__*/ jsx(Flex, { justifyContent: "center", padding: 1, paddingTop: 2, children: /*#__PURE__*/ jsx(Loader, { children: formatMessage({ id: getTranslationKey('sidebar.tree.loading'), defaultMessage: 'Loading folders...' }) }) }) : isError ? // TODO: revisit error state before revamp GA /*#__PURE__*/ jsx(Box, { padding: 1, paddingTop: 2, children: /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "danger600", children: formatMessage({ id: getTranslationKey('sidebar.tree.error'), defaultMessage: 'Could not load folders.' }) }) }) : folderStructure.length === 0 ? // TODO: revisit empty state before revamp GA /*#__PURE__*/ jsx(Box, { padding: 1, paddingTop: 2, children: /*#__PURE__*/ jsx(Typography, { variant: "pi", textColor: "neutral500", children: formatMessage({ id: getTranslationKey('sidebar.tree.empty'), defaultMessage: 'No folders yet' }) }) }) : /*#__PURE__*/ jsx(NavList, { children: folderStructure.map((node)=>/*#__PURE__*/ jsx(FolderTreeItem, { node: node, level: 0, currentFolderId: currentFolderId, isExpanded: isExpanded, onToggle: toggleExpanded, onSelect: onSelectFolder }, node.id ?? node.name)) }) ] }) ] }) ] }); }; export { FolderTree }; //# sourceMappingURL=FolderTree.mjs.map