breadcrumbs in ML modal - CM

This commit is contained in:
Julie Plantey 2022-07-31 17:43:42 +02:00
parent 110317b6b1
commit bdc0454cd7
15 changed files with 740 additions and 755 deletions

View File

@ -4,7 +4,6 @@ import styled from 'styled-components';
import { useIntl } from 'react-intl';
import { Button } from '@strapi/design-system/Button';
import { Flex } from '@strapi/design-system/Flex';
import { Stack } from '@strapi/design-system/Stack';
import { Box } from '@strapi/design-system/Box';
import { Divider } from '@strapi/design-system/Divider';
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
@ -17,11 +16,14 @@ import PlusIcon from '@strapi/icons/Plus';
import { FolderDefinition, AssetDefinition } from '../../../constants';
import getTrad from '../../../utils/getTrad';
import { getBreadcrumbDataCM } from '../../../utils';
import getAllowedFiles from '../../../utils/getAllowedFiles';
import { AssetList } from '../../AssetList';
import { FolderList } from '../../FolderList';
import { EmptyAssets } from '../../EmptyAssets';
import { Breadcrumbs } from '../../Breadcrumbs';
import SortPicker from '../../SortPicker';
import { useFolder } from '../../../hooks/useFolder';
import { FolderCard, FolderCardBody, FolderCardBodyAction } from '../../FolderCard';
import { Filters } from './Filters';
import PaginationFooter from './PaginationFooter';
@ -32,7 +34,6 @@ const StartBlockActions = styled(Flex)`
& > * + * {
margin-left: ${({ theme }) => theme.spaces[2]};
}
margin-left: ${({ pullRight }) => (pullRight ? 'auto' : undefined)};
`;
@ -48,6 +49,7 @@ export const BrowseStep = ({
allowedTypes,
assets,
canCreate,
canRead,
folders,
multiple,
onAddAsset,
@ -66,6 +68,16 @@ export const BrowseStep = ({
selectedAssets,
}) => {
const { formatMessage } = useIntl();
const { data: currentFolder, isLoading: isCurrentFolderLoading } = useFolder(
queryObject?.folder,
{
enabled: canRead && !!queryObject?.folder,
}
);
const breadcrumbs = !isCurrentFolderLoading && getBreadcrumbDataCM(currentFolder);
const allAllowedAsset = getAllowedFiles(allowedTypes, assets);
const areAllAssetSelected =
allAllowedAsset.every(
@ -79,7 +91,6 @@ export const BrowseStep = ({
const isSearchingOrFiltering = isSearching || isFiltering;
const assetCount = assets.length;
const folderCount = folders.length;
const handleClickFolderCard = (...args) => {
// Search query will always fetch the same results
// we remove it here to allow navigating in a folder and see the result of this navigation
@ -88,48 +99,58 @@ export const BrowseStep = ({
};
return (
<Stack spacing={4}>
<Box>
{onSelectAllAsset && (
<Box>
<Box paddingBottom={4}>
<Flex justifyContent="space-between" alignItems="flex-start">
{(assetCount > 0 || folderCount > 0 || isFiltering) && (
<StartBlockActions wrap="wrap">
{multiple && (
<Flex
paddingLeft={2}
paddingRight={2}
background="neutral0"
hasRadius
borderColor="neutral200"
height={`${32 / 16}rem`}
>
<BaseCheckbox
aria-label={formatMessage({
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all assets',
})}
indeterminate={!areAllAssetSelected && hasSomeAssetSelected}
value={areAllAssetSelected}
onChange={onSelectAllAsset}
/>
</Flex>
)}
<SortPicker onChangeSort={onChangeSort} />
<Filters
appliedFilters={queryObject?.filters?.$and}
onChangeFilters={onChangeFilters}
/>
</StartBlockActions>
)}
<Flex justifyContent="space-between" alignItems="flex-start">
{(assetCount > 0 || folderCount > 0 || isFiltering) && (
<StartBlockActions wrap="wrap">
{multiple && (
<Flex
paddingLeft={2}
paddingRight={2}
background="neutral0"
hasRadius
borderColor="neutral200"
height={`${32 / 16}rem`}
>
<BaseCheckbox
aria-label={formatMessage({
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all assets',
})}
indeterminate={!areAllAssetSelected && hasSomeAssetSelected}
value={areAllAssetSelected}
onChange={onSelectAllAsset}
/>
</Flex>
)}
<SortPicker onChangeSort={onChangeSort} />
<Filters
appliedFilters={queryObject?.filters?.$and}
onChangeFilters={onChangeFilters}
/>
</StartBlockActions>
)}
{(assetCount > 0 || folderCount > 0 || isSearching) && (
<EndBlockActions pullRight>
<SearchAsset onChangeSearch={onChangeSearch} queryValue={queryObject._q || ''} />
</EndBlockActions>
)}
</Flex>
</Box>
{(assetCount > 0 || folderCount > 0 || isSearching) && (
<EndBlockActions pullRight>
<SearchAsset onChangeSearch={onChangeSearch} queryValue={queryObject._q || ''} />
</EndBlockActions>
)}
</Flex>
</Box>
)}
{canRead && !isCurrentFolderLoading && (
<Box paddingTop={3}>
<Breadcrumbs
onChangeFolder={onChangeFolder}
as="nav"
label="hello"
breadcrumbs={breadcrumbs}
currentFolderId={queryObject?.folder}
/>
</Box>
)}
@ -175,10 +196,13 @@ export const BrowseStep = ({
<FolderList
title={
(((isSearchingOrFiltering && assetCount > 0) || !isSearchingOrFiltering) &&
formatMessage({
id: getTrad('list.folders.title'),
defaultMessage: 'Folders',
})) ||
formatMessage(
{
id: getTrad('list.folders.title'),
defaultMessage: 'Folders ({count})',
},
{ count: folderCount }
)) ||
''
}
>
@ -209,7 +233,6 @@ export const BrowseStep = ({
{folder.name}
<VisuallyHidden>:</VisuallyHidden>
</TypographyMaxWidth>
<TypographyMaxWidth as="span" textColor="neutral600" variant="pi" ellipsis>
{formatMessage(
{
@ -234,33 +257,38 @@ export const BrowseStep = ({
)}
{assetCount > 0 && folderCount > 0 && (
<Box paddingTop={2}>
<Box paddingTop={6}>
<Divider />
</Box>
)}
{assetCount > 0 && (
<AssetList
allowedTypes={allowedTypes}
size="S"
assets={assets}
onSelectAsset={onSelectAsset}
selectedAssets={selectedAssets}
onEditAsset={onEditAsset}
title={
((!isSearchingOrFiltering || (isSearchingOrFiltering && folderCount > 0)) &&
queryObject.page === 1 &&
formatMessage({
id: getTrad('list.assets.title'),
defaultMessage: 'Assets',
})) ||
''
}
/>
<Box paddingTop={6}>
<AssetList
allowedTypes={allowedTypes}
size="S"
assets={assets}
onSelectAsset={onSelectAsset}
selectedAssets={selectedAssets}
onEditAsset={onEditAsset}
title={
((!isSearchingOrFiltering || (isSearchingOrFiltering && folderCount > 0)) &&
queryObject.page === 1 &&
formatMessage(
{
id: getTrad('list.assets.title'),
defaultMessage: 'Assets ({count})',
},
{ count: assetCount }
)) ||
''
}
/>
</Box>
)}
{pagination.pageCount > 0 && (
<Flex justifyContent="space-between">
<Flex justifyContent="space-between" paddingTop={4}>
<PageSize pageSize={queryObject.pageSize} onChangePageSize={onChangePageSize} />
<PaginationFooter
activePage={queryObject.page}
@ -269,7 +297,7 @@ export const BrowseStep = ({
/>
</Flex>
)}
</Stack>
</Box>
);
};
@ -281,11 +309,11 @@ BrowseStep.defaultProps = {
onEditAsset: undefined,
onEditFolder: undefined,
};
BrowseStep.propTypes = {
allowedTypes: PropTypes.arrayOf(PropTypes.string),
assets: PropTypes.arrayOf(AssetDefinition).isRequired,
canCreate: PropTypes.bool.isRequired,
canRead: PropTypes.bool.isRequired,
folders: PropTypes.arrayOf(FolderDefinition),
multiple: PropTypes.bool,
onAddAsset: PropTypes.func.isRequired,
@ -304,6 +332,7 @@ BrowseStep.propTypes = {
page: PropTypes.number.isRequired,
pageSize: PropTypes.number.isRequired,
_q: PropTypes.string,
folder: PropTypes.number,
}).isRequired,
pagination: PropTypes.shape({ pageCount: PropTypes.number.isRequired }).isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,

View File

@ -2,7 +2,9 @@ import React from 'react';
import { IntlProvider } from 'react-intl';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { render, fireEvent, screen } from '@testing-library/react';
import { NotificationsProvider } from '@strapi/helper-plugin';
import { MemoryRouter } from 'react-router-dom';
import { QueryClientProvider, QueryClient } from 'react-query';
import { BrowseStep } from '..';
@ -62,38 +64,50 @@ const FIXTURE_FOLDERS = [
},
];
const client = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const ComponentFixture = props => {
return (
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<IntlProvider messages={{}} locale="en">
<BrowseStep
assets={[]}
canCreate
folders={FIXTURE_FOLDERS}
onAddAsset={jest.fn()}
onChangeFilters={jest.fn()}
onChangePage={jest.fn()}
onChangePageSize={jest.fn()}
onChangeSearch={jest.fn()}
onChangeSort={jest.fn()}
onChangeFolder={jest.fn()}
onEditAsset={jest.fn()}
onSelectAllAsset={jest.fn()}
onSelectAsset={jest.fn()}
pagination={{ pageCount: 1 }}
queryObject={{ page: 1, pageSize: 10, filters: { $and: [] } }}
selectedAssets={[]}
{...props}
/>
</IntlProvider>
</MemoryRouter>
</ThemeProvider>
<QueryClientProvider client={client}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<IntlProvider messages={{}} locale="en">
<NotificationsProvider toggleNotification={() => {}}>
<BrowseStep
assets={[]}
canCreate
canRead
folders={FIXTURE_FOLDERS}
onAddAsset={jest.fn()}
onChangeFilters={jest.fn()}
onChangePage={jest.fn()}
onChangePageSize={jest.fn()}
onChangeSearch={jest.fn()}
onChangeSort={jest.fn()}
onChangeFolder={jest.fn()}
onEditAsset={jest.fn()}
onSelectAllAsset={jest.fn()}
onSelectAsset={jest.fn()}
pagination={{ pageCount: 1 }}
queryObject={{ page: 1, pageSize: 10, filters: { $and: [] } }}
selectedAssets={[]}
{...props}
/>
</NotificationsProvider>
</IntlProvider>
</MemoryRouter>
</ThemeProvider>
</QueryClientProvider>
);
};
const setup = props => render(<ComponentFixture {...props} />);
describe('BrowseStep', () => {
afterEach(() => {
jest.clearAllMocks();
@ -101,14 +115,12 @@ describe('BrowseStep', () => {
it('renders and match snapshot', () => {
const { container } = setup();
expect(container).toMatchSnapshot();
});
it('calls onAddAsset callback', () => {
const spy = jest.fn();
const { getByText } = setup({ onAddAsset: spy, folders: [] });
fireEvent.click(getByText('Add new assets'));
expect(spy).toHaveBeenCalled();
});
@ -116,19 +128,16 @@ describe('BrowseStep', () => {
it('calls onChangeFolder callback', () => {
const spy = jest.fn();
const { getByRole } = setup({ onChangeFolder: spy });
fireEvent.click(
getByRole('button', {
name: /folder 1 : 1 folder, 1 asset/i,
})
);
expect(spy).toHaveBeenCalled();
});
it('does display empty state upload first assets if no folder or assets', () => {
setup({ folders: [], assets: [] });
expect(screen.getByText('Upload your first assets...')).toBeInTheDocument();
});
@ -138,7 +147,6 @@ describe('BrowseStep', () => {
assets: [],
queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' },
});
expect(screen.getByText('There are no assets with the applied filters')).toBeInTheDocument();
});
@ -148,7 +156,6 @@ describe('BrowseStep', () => {
assets: [],
queryObject: { page: 1, pageSize: 10, filters: { $and: [{ mime: 'audio' }] }, _q: '' },
});
expect(screen.getByText('Filters')).toBeInTheDocument();
});
@ -158,7 +165,6 @@ describe('BrowseStep', () => {
assets: FIXTURE_ASSETS,
queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' },
});
expect(screen.queryByText('Assets')).not.toBeInTheDocument();
});
@ -166,7 +172,6 @@ describe('BrowseStep', () => {
setup({
queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' },
});
expect(screen.queryByText('Folders')).not.toBeInTheDocument();
});
@ -175,7 +180,7 @@ describe('BrowseStep', () => {
assets: FIXTURE_ASSETS,
});
expect(screen.getByText('Folders')).toBeInTheDocument();
expect(screen.getByText('Assets')).toBeInTheDocument();
expect(screen.getByText('Folders (1)')).toBeInTheDocument();
expect(screen.getByText('Assets (1)')).toBeInTheDocument();
});
});

View File

@ -1,91 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Breadcrumbs, Crumb } from '@strapi/design-system/Breadcrumbs';
import { ModalHeader } from '@strapi/design-system/ModalLayout';
import { Stack } from '@strapi/design-system/Stack';
import { Icon } from '@strapi/design-system/Icon';
import ArrowLeft from '@strapi/icons/ArrowLeft';
import { findRecursiveFolderMetadatas, getTrad } from '../../utils';
import { useFolderStructure } from '../../hooks/useFolderStructure';
const BackButton = styled.button`
height: ${({ theme }) => theme.spaces[4]};
color: ${({ theme }) => theme.colors.neutral500};
&:hover,
&:focus {
color: ${({ theme }) => theme.colors.neutral600};
}
`;
const BackIcon = styled(Icon)`
path {
fill: currentColor;
}
`;
export const DialogHeader = ({ currentFolder, onChangeFolder, canRead }) => {
const { formatMessage } = useIntl();
const { data, isLoading } = useFolderStructure({
enabled: canRead,
});
const folderMetadatas =
!isLoading && Array.isArray(data) && findRecursiveFolderMetadatas(data[0], currentFolder);
const folderLabel =
folderMetadatas?.currentFolderLabel &&
(folderMetadatas.currentFolderLabel.length > 60
? `${folderMetadatas.currentFolderLabel.slice(0, 60)}...`
: folderMetadatas.currentFolderLabel);
return (
<ModalHeader>
<Stack horizontal spacing={4}>
{currentFolder && (
<BackButton
aria-label={formatMessage({ id: 'modal.header.go-back', defaultMessage: 'Go back' })}
type="button"
onClick={() => onChangeFolder(folderMetadatas?.parentId)}
>
<BackIcon height="100%" as={ArrowLeft} />
</BackButton>
)}
<Breadcrumbs
label={`${formatMessage({
id: getTrad('header.actions.add-assets'),
defaultMessage: 'Add new assets',
})}${
folderLabel
? `, ${folderLabel} ${formatMessage({
id: 'header.actions.add-assets.folder',
defaultMessage: 'folder',
})}`
: ''
}`}
>
<Crumb>
{formatMessage({
id: getTrad('header.actions.add-assets'),
defaultMessage: 'Add new assets',
})}
</Crumb>
{folderLabel && <Crumb>{folderLabel}</Crumb>}
</Breadcrumbs>
</Stack>
</ModalHeader>
);
};
DialogHeader.defaultProps = {
currentFolder: undefined,
onChangeFolder: undefined,
};
DialogHeader.propTypes = {
canRead: PropTypes.bool.isRequired,
currentFolder: PropTypes.number,
onChangeFolder: PropTypes.func,
};

View File

@ -1,17 +1,17 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { ModalLayout, ModalBody } from '@strapi/design-system/ModalLayout';
import { useIntl } from 'react-intl';
import { ModalLayout, ModalBody, ModalHeader } from '@strapi/design-system/ModalLayout';
import { Flex } from '@strapi/design-system/Flex';
import { Button } from '@strapi/design-system/Button';
import { Divider } from '@strapi/design-system/Divider';
import { useIntl } from 'react-intl';
import { Typography } from '@strapi/design-system/Typography';
import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/design-system/Tabs';
import { Badge } from '@strapi/design-system/Badge';
import { Loader } from '@strapi/design-system/Loader';
import { Stack } from '@strapi/design-system/Stack';
import { NoPermissions, AnErrorOccurred, useSelectionState, pxToRem } from '@strapi/helper-plugin';
import { getTrad, containsAssetFilter } from '../../utils';
import { SelectedStep } from './SelectedStep';
import { BrowseStep } from './BrowseStep';
@ -21,7 +21,6 @@ import { useFolders } from '../../hooks/useFolders';
import useModalQueryParams from '../../hooks/useModalQueryParams';
import { AssetDefinition } from '../../constants';
import getAllowedFiles from '../../utils/getAllowedFiles';
import { DialogHeader } from './DialogHeader';
import { DialogFooter } from './DialogFooter';
import { EditAssetDialog } from '../EditAssetDialog';
import { moveElement } from '../../utils/moveElement';
@ -55,6 +54,7 @@ export const AssetDialog = ({
canCopyLink,
canDownload,
} = useMediaLibraryPermissions();
const [
{ queryObject },
{
@ -66,11 +66,13 @@ export const AssetDialog = ({
onChangeFolder: onChangeFolderParam,
},
] = useModalQueryParams({ folder: folderId });
const {
data: { pagination, results: assets } = {},
isLoading: isLoadingAssets,
error: errorAssets,
} = useAssets({ skipWhen: !canRead, query: queryObject });
const { data: folders, isLoading: isLoadingFolders, error: errorFolders } = useFolders({
enabled: canRead && !containsAssetFilter(queryObject) && pagination?.page === 1,
query: queryObject,
@ -84,6 +86,7 @@ export const AssetDialog = ({
const [initialSelectedTabIndex, setInitialSelectedTabIndex] = useState(
selectedAssets.length > 0 ? 1 : 0
);
const handleSelectAllAssets = () => {
const hasAllAssets = assets.every(
asset => selectedAssets.findIndex(curr => curr.id === asset.id) !== -1
@ -97,6 +100,7 @@ export const AssetDialog = ({
return multiple ? selectAll(allowedAssets) : undefined;
};
const handleSelectAsset = asset => {
return multiple ? selectOne(asset) : selectOnly(asset);
};
@ -107,7 +111,14 @@ export const AssetDialog = ({
if (isLoading) {
return (
<ModalLayout onClose={onClose} labelledBy="asset-dialog-title" aria-busy>
<DialogHeader canRead={canRead} />
<ModalHeader>
<Typography fontWeight="bold">
{formatMessage({
id: getTrad('header.actions.add-assets'),
defaultMessage: 'Add new assets',
})}
</Typography>
</ModalHeader>
<LoadingBody justifyContent="center" paddingTop={4} paddingBottom={4}>
<Loader>
{formatMessage({
@ -124,7 +135,14 @@ export const AssetDialog = ({
if (hasError) {
return (
<ModalLayout onClose={onClose} labelledBy="asset-dialog-title">
<DialogHeader canRead={canRead} />
<ModalHeader>
<Typography fontWeight="bold">
{formatMessage({
id: getTrad('header.actions.add-assets'),
defaultMessage: 'Add new assets',
})}
</Typography>
</ModalHeader>
<AnErrorOccurred />
<DialogFooter onClose={onClose} />
</ModalLayout>
@ -134,7 +152,14 @@ export const AssetDialog = ({
if (!canRead) {
return (
<ModalLayout onClose={onClose} labelledBy="asset-dialog-title">
<DialogHeader canRead={canRead} />
<ModalHeader fontWeight="bold">
<Typography>
{formatMessage({
id: getTrad('header.actions.add-assets'),
defaultMessage: 'Add new assets',
})}
</Typography>
</ModalHeader>
<NoPermissions />
<DialogFooter onClose={onClose} />
</ModalLayout>
@ -168,7 +193,6 @@ export const AssetDialog = ({
const offset = destIndex - hoverIndex;
const orderedAssetsClone = selectedAssets.slice();
const nextAssets = moveElement(orderedAssetsClone, hoverIndex, offset);
setSelections(nextAssets);
};
@ -179,11 +203,14 @@ export const AssetDialog = ({
return (
<ModalLayout onClose={onClose} labelledBy="asset-dialog-title" aria-busy={isLoading}>
<DialogHeader
currentFolder={queryObject?.folder}
onChangeFolder={handleFolderChange}
canRead={canRead}
/>
<ModalHeader>
<Typography fontWeight="bold">
{formatMessage({
id: getTrad('header.actions.add-assets'),
defaultMessage: 'Add new assets',
})}
</Typography>
</ModalHeader>
<TabGroup
label={formatMessage({
@ -210,7 +237,6 @@ export const AssetDialog = ({
<Badge marginLeft={2}>{selectedAssets.length}</Badge>
</Tab>
</Tabs>
<Stack horizontal spacing={2}>
<Button
variant="secondary"
@ -221,7 +247,6 @@ export const AssetDialog = ({
defaultMessage: 'Add folder',
})}
</Button>
<Button onClick={() => onAddAsset({ folderId: queryObject?.folder })}>
{formatMessage({
id: getTrad('modal.upload-list.sub-header.button'),
@ -238,6 +263,7 @@ export const AssetDialog = ({
allowedTypes={allowedTypes}
assets={assets}
canCreate={canCreate}
canRead={canRead}
folders={folders}
onSelectAsset={handleSelectAsset}
selectedAssets={selectedAssets}
@ -268,7 +294,6 @@ export const AssetDialog = ({
</TabPanel>
</TabPanels>
</TabGroup>
<DialogFooter onClose={onClose} onValidate={() => onValidate(selectedAssets)} />
</ModalLayout>
);

View File

@ -1,96 +0,0 @@
import React from 'react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider, QueryClient } from 'react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { useFolderStructure } from '../../../hooks/useFolderStructure';
import { DialogHeader } from '../DialogHeader';
jest.mock('../../../hooks/useFolderStructure');
const setup = props => {
const withDefaults = {
canRead: true,
currentFolder: null,
onChangeFolder: jest.fn(),
...props,
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en" messages={{}}>
<DialogHeader {...withDefaults} />
</IntlProvider>
</ThemeProvider>
</QueryClientProvider>
);
};
describe('Upload || components || DialogHeader', () => {
it('should render folder name and back button', () => {
const handleChangeFolderSpy = jest.fn();
const { queryByText } = setup({ currentFolder: 2, onChangeFolder: handleChangeFolderSpy });
expect(queryByText('second child')).toBeInTheDocument();
const goBackButton = screen.getByLabelText('Go back');
expect(goBackButton).toBeInTheDocument();
fireEvent.click(goBackButton);
expect(handleChangeFolderSpy).toHaveBeenCalled();
});
it('should truncate long folder name', () => {
useFolderStructure.mockReturnValueOnce({
isLoading: false,
error: null,
data: [
{
value: null,
label: 'Media Library',
children: [
{
value: 1,
label: 'This is a really really long folder name that should be truncated',
children: [],
},
],
},
],
});
const { queryByText } = setup({ currentFolder: 1 });
expect(
queryByText('This is a really really long folder name that should be trun...')
).toBeInTheDocument();
});
it('should not render folder name and back button if the current folder is root', () => {
const { queryByText } = setup();
expect(queryByText('Cats')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Go back')).not.toBeInTheDocument();
});
it('should not attempt to fetch the folder structure, if the user does not have permissions', () => {
const spy = jest.fn().mockReturnValueOnce({
isLoading: false,
error: null,
});
useFolderStructure.mockImplementation(spy);
setup({ canRead: false });
expect(spy).toHaveBeenCalledWith({ enabled: false });
});
});

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import {
@ -9,7 +10,7 @@ import {
import { CrumbSimpleMenuAsync } from './CrumbSimpleMenuAsync';
import { BreadcrumbsDefinition } from '../../constants';
export const Breadcrumbs = ({ breadcrumbs, ...props }) => (
export const Breadcrumbs = ({ breadcrumbs, onChangeFolder, currentFolderId, ...props }) => (
<BaseBreadcrumbs {...props}>
{breadcrumbs.map((crumb, index) => {
if (Array.isArray(crumb)) {
@ -19,13 +20,23 @@ export const Breadcrumbs = ({ breadcrumbs, ...props }) => (
.splice(index + 1, breadcrumbs.length - 1)
.map(parent => parent.id)}
key={`breadcrumb-${crumb?.id ?? 'menu'}`}
currentFolderId={currentFolderId}
onChangeFolder={onChangeFolder}
/>
);
}
if (crumb.href) {
const isCurrentFolderMediaLibrary = crumb.id === null && currentFolderId === undefined;
if (currentFolderId !== crumb.id && !isCurrentFolderMediaLibrary) {
return (
<CrumbLink key={`breadcrumb-${crumb?.id ?? 'root'}`} as={NavLink} to={crumb.href}>
<CrumbLink
key={`breadcrumb-${crumb?.id ?? 'root'}`}
as={onChangeFolder ? 'button' : NavLink}
type={onChangeFolder && 'button'}
to={onChangeFolder ? undefined : crumb.href}
onClick={onChangeFolder && (() => onChangeFolder(crumb.id))}
>
{crumb.label}
</CrumbLink>
);
@ -43,6 +54,13 @@ export const Breadcrumbs = ({ breadcrumbs, ...props }) => (
</BaseBreadcrumbs>
);
Breadcrumbs.defaultProps = {
currentFolderId: undefined,
onChangeFolder: undefined,
};
Breadcrumbs.propTypes = {
breadcrumbs: BreadcrumbsDefinition.isRequired,
currentFolderId: PropTypes.number,
onChangeFolder: PropTypes.func,
};

View File

@ -2,7 +2,6 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { NavLink, useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { useQueryParams } from '@strapi/helper-plugin';
import { CrumbSimpleMenu } from '@strapi/design-system/v2/Breadcrumbs';
import { MenuItem } from '@strapi/design-system/v2/SimpleMenu';
@ -10,14 +9,14 @@ import { Loader } from '@strapi/design-system/Loader';
import { useFolderStructure } from '../../hooks/useFolderStructure';
import { getFolderParents, getFolderURL, getTrad } from '../../utils';
export const CrumbSimpleMenuAsync = ({ parentsToOmit }) => {
export const CrumbSimpleMenuAsync = ({ parentsToOmit, currentFolderId, onChangeFolder }) => {
const [shouldFetch, setShouldFetch] = useState(false);
const { data, isLoading } = useFolderStructure({ enabled: shouldFetch });
const { pathname } = useLocation();
const [{ query }] = useQueryParams();
const { formatMessage } = useIntl();
const allAscendants = data && getFolderParents(data, Number(query?.folder));
const allAscendants = data && getFolderParents(data, currentFolderId);
const filteredAscendants =
allAscendants &&
allAscendants.filter(
@ -46,6 +45,19 @@ export const CrumbSimpleMenuAsync = ({ parentsToOmit }) => {
)}
{filteredAscendants &&
filteredAscendants.map(ascendant => {
if (onChangeFolder) {
return (
<MenuItem
as="button"
type="button"
onClick={() => onChangeFolder(ascendant.id)}
key={ascendant.id}
>
{ascendant.label}
</MenuItem>
);
}
const url = getFolderURL(pathname, query, ascendant);
return (
@ -59,9 +71,13 @@ export const CrumbSimpleMenuAsync = ({ parentsToOmit }) => {
};
CrumbSimpleMenuAsync.defaultProps = {
currentFolderId: undefined,
onChangeFolder: undefined,
parentsToOmit: [],
};
CrumbSimpleMenuAsync.propTypes = {
currentFolderId: PropTypes.number,
onChangeFolder: PropTypes.func,
parentsToOmit: PropTypes.arrayOf(PropTypes.number),
};

View File

@ -49,7 +49,7 @@ const setup = props =>
describe('Media Library | Breadcrumbs', () => {
test('should render and match snapshot', () => {
const { container } = setup();
const { container } = setup({ currentFolderId: 22 });
expect(container.querySelector('nav')).toBeInTheDocument();
expect(screen.getByText('parent folder')).toBeInTheDocument();
@ -59,7 +59,7 @@ describe('Media Library | Breadcrumbs', () => {
});
test('should store other ascendants in simple menu', async () => {
const { getByRole } = setup();
const { getByRole } = setup({ currentFolderId: 22 });
const simpleMenuButton = getByRole('button', { name: /get more ascendants folders/i });
fireEvent.mouseDown(simpleMenuButton);

View File

@ -33,7 +33,7 @@ import { FolderList } from '../../components/FolderList';
import SortPicker from '../../components/SortPicker';
import { useAssets } from '../../hooks/useAssets';
import { useFolders } from '../../hooks/useFolders';
import { getTrad, containsAssetFilter, getBreadcrumbData, getFolderURL } from '../../utils';
import { getTrad, containsAssetFilter, getBreadcrumbDataML, getFolderURL } from '../../utils';
import { PaginationFooter } from '../../components/PaginationFooter';
import { useMediaLibraryPermissions } from '../../hooks/useMediaLibraryPermissions';
import { useFolder } from '../../hooks/useFolder';
@ -151,7 +151,7 @@ export const MediaLibrary = () => {
<Main aria-busy={isLoading}>
<Header
breadcrumbs={
!isCurrentFolderLoading && getBreadcrumbData(currentFolder, { pathname, query })
!isCurrentFolderLoading && getBreadcrumbDataML(currentFolder, { pathname, query })
}
canCreate={canCreate}
onToggleEditFolderDialog={toggleEditFolderDialog}

View File

@ -17,9 +17,9 @@ import { Breadcrumbs } from '../../../components/Breadcrumbs';
export const Header = ({
breadcrumbs,
canCreate,
folder,
onToggleEditFolderDialog,
onToggleUploadAssetDialog,
folder,
}) => {
const { formatMessage } = useIntl();
const { pathname } = useLocation();
@ -45,6 +45,7 @@ export const Header = ({
defaultMessage: 'Folders navigation',
})}
breadcrumbs={breadcrumbs}
currentFolderId={folder?.id}
/>
)
}

View File

@ -0,0 +1,30 @@
const getBreadcrumbDataML = folder => {
let data = [
{
id: null,
label: 'Media Library',
},
];
if (folder?.parent?.parent) {
data.push([]);
}
if (folder?.parent) {
data.push({
id: folder.parent.id,
label: folder.parent.name,
});
}
if (folder) {
data.push({
id: folder.id,
label: folder.name,
});
}
return data;
};
export default getBreadcrumbDataML;

View File

@ -1,6 +1,6 @@
import getFolderURL from './getFolderURL';
const getBreadcrumbData = (folder, { pathname, query }) => {
const getBreadcrumbDataML = (folder, { pathname, query }) => {
let data = [
{
id: null,
@ -31,4 +31,4 @@ const getBreadcrumbData = (folder, { pathname, query }) => {
return data;
};
export default getBreadcrumbData;
export default getBreadcrumbDataML;

View File

@ -5,7 +5,8 @@ export { default as getTrad } from './getTrad';
export { default as findRecursiveFolderByValue } from './findRecursiveFolderByValue';
export { default as findRecursiveFolderMetadatas } from './findRecursiveFolderMetadatas';
export { default as containsAssetFilter } from './containsAssetFilter';
export { default as getBreadcrumbData } from './getBreadcrumbData';
export { default as getBreadcrumbDataML } from './getBreadcrumbDataML';
export { default as getBreadcrumbDataCM } from './getBreadcrumbDataCM';
export { default as getFolderURL } from './getFolderURL';
export { default as getFolderParents } from './getFolderParents';
export * from './formatDuration';

View File

@ -1,4 +1,4 @@
import { getBreadcrumbData } from '..';
import { getBreadcrumbDataML } from '..';
const FIXTURE_PATHNAME = '/media-library';
const FIXTURE_QUERY = {
@ -10,10 +10,10 @@ const FIXTURE_FOLDER = {
name: 'first-level',
};
describe('getBreadcrumData', () => {
describe('getBreadcrumbDataML', () => {
test('return one item at the root of the media library', () => {
expect(
getBreadcrumbData(null, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY })
getBreadcrumbDataML(null, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY })
).toStrictEqual([
{
href: undefined,
@ -25,7 +25,7 @@ describe('getBreadcrumData', () => {
test('returns two items for the first level of the media library', () => {
expect(
getBreadcrumbData(FIXTURE_FOLDER, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY })
getBreadcrumbDataML(FIXTURE_FOLDER, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY })
).toStrictEqual([
{
href: '/media-library?some=thing',
@ -42,7 +42,7 @@ describe('getBreadcrumData', () => {
test('returns three items for the second level of the media library', () => {
expect(
getBreadcrumbData(
getBreadcrumbDataML(
{ ...FIXTURE_FOLDER, parent: { id: 2, name: 'second-level' } },
{ pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY }
)
@ -68,7 +68,7 @@ describe('getBreadcrumData', () => {
test('returns four items for the third level of the media library', () => {
expect(
getBreadcrumbData(
getBreadcrumbDataML(
{
...FIXTURE_FOLDER,
parent: { id: 2, name: 'second-level', parent: { id: 3, name: 'third-level' } },