Merge pull request #13917 from strapi/features/folder-breadcrumbs

[Features] Breadcrumbs Folders navigation
This commit is contained in:
Julie Plantey 2022-08-05 12:55:53 +02:00 committed by GitHub
commit c0398521e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1921 additions and 896 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,61 @@ 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 && breadcrumbs?.length > 0 && currentFolder && (
<Box paddingTop={3}>
<Breadcrumbs
onChangeFolder={onChangeFolder}
as="nav"
label={formatMessage({
id: getTrad('header.breadcrumbs.nav.label'),
defaultMessage: 'Folders navigation',
})}
breadcrumbs={breadcrumbs}
currentFolderId={queryObject?.folder}
/>
</Box>
)}
@ -175,10 +199,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 +236,6 @@ export const BrowseStep = ({
{folder.name}
<VisuallyHidden>:</VisuallyHidden>
</TypographyMaxWidth>
<TypographyMaxWidth as="span" textColor="neutral600" variant="pi" ellipsis>
{formatMessage(
{
@ -234,33 +260,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 +300,7 @@ export const BrowseStep = ({
/>
</Flex>
)}
</Stack>
</Box>
);
};
@ -281,11 +312,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 +335,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,10 +2,15 @@ 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 { useFolder } from '../../../../hooks/useFolder';
import { BrowseStep } from '..';
jest.mock('../../../../hooks/useFolder');
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useTracking: jest.fn(() => ({ trackUsage: jest.fn() })),
@ -62,38 +67,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 +118,37 @@ describe('BrowseStep', () => {
it('renders and match snapshot', () => {
const { container } = setup();
expect(container).toMatchSnapshot();
});
it('should not fetch folder if the user does not have the permission', () => {
const spy = jest.fn().mockReturnValueOnce({ isLoading: false });
useFolder.mockImplementationOnce(spy);
setup({
canRead: false,
queryObject: { folder: 1, page: 1, pageSize: 10, filters: { $and: [] } },
});
expect(spy).toHaveBeenCalledWith(1, { enabled: false });
});
it('should show breadcrumbs navigation', () => {
setup();
expect(screen.queryByLabelText('Folders navigation')).toBeInTheDocument();
});
it('should hide breadcrumbs navigation if in root folder', () => {
useFolder.mockReturnValueOnce({ isLoading: false, data: undefined });
setup();
expect(screen.queryByLabelText('Folders navigation')).not.toBeInTheDocument();
});
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 +156,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 +175,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 +184,6 @@ describe('BrowseStep', () => {
assets: [],
queryObject: { page: 1, pageSize: 10, filters: { $and: [{ mime: 'audio' }] }, _q: '' },
});
expect(screen.getByText('Filters')).toBeInTheDocument();
});
@ -158,7 +193,6 @@ describe('BrowseStep', () => {
assets: FIXTURE_ASSETS,
queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' },
});
expect(screen.queryByText('Assets')).not.toBeInTheDocument();
});
@ -166,7 +200,6 @@ describe('BrowseStep', () => {
setup({
queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' },
});
expect(screen.queryByText('Folders')).not.toBeInTheDocument();
});
@ -175,7 +208,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,11 +111,18 @@ 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({
id: getTrad('list.asset.load'),
id: getTrad('content.isLoading'),
defaultMessage: 'Content is loading.',
})}
</Loader>
@ -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

@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
Crumb,
CrumbLink,
Breadcrumbs as BaseBreadcrumbs,
} from '@strapi/design-system/v2/Breadcrumbs';
import { CrumbSimpleMenuAsync } from './CrumbSimpleMenuAsync';
import { BreadcrumbsDefinition } from '../../constants';
export const Breadcrumbs = ({ breadcrumbs, onChangeFolder, currentFolderId, ...props }) => {
const { formatMessage } = useIntl();
return (
<BaseBreadcrumbs {...props}>
{breadcrumbs.map((crumb, index) => {
if (Array.isArray(crumb)) {
return (
<CrumbSimpleMenuAsync
parentsToOmit={[...breadcrumbs]
.splice(index + 1, breadcrumbs.length - 1)
.map(parent => parent.id)}
key={`breadcrumb-${crumb?.id ?? 'menu'}`}
currentFolderId={currentFolderId}
onChangeFolder={onChangeFolder}
/>
);
}
const isCurrentFolderMediaLibrary = crumb.id === null && currentFolderId === undefined;
if (currentFolderId !== crumb.id && !isCurrentFolderMediaLibrary) {
return (
<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?.id ? formatMessage(crumb.label) : crumb.label}
</CrumbLink>
);
}
return (
<Crumb
key={`breadcrumb-${crumb?.id ?? 'root'}`}
isCurrent={index + 1 === breadcrumbs.length}
>
{crumb.label?.id ? formatMessage(crumb.label) : crumb.label}
</Crumb>
);
})}
</BaseBreadcrumbs>
);
};
Breadcrumbs.defaultProps = {
currentFolderId: undefined,
onChangeFolder: undefined,
};
Breadcrumbs.propTypes = {
breadcrumbs: BreadcrumbsDefinition.isRequired,
currentFolderId: PropTypes.number,
onChangeFolder: PropTypes.func,
};

View File

@ -0,0 +1,83 @@
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';
import { Loader } from '@strapi/design-system/Loader';
import { useFolderStructure } from '../../hooks/useFolderStructure';
import { getFolderParents, getFolderURL, getTrad } from '../../utils';
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, currentFolderId);
const filteredAscendants =
allAscendants &&
allAscendants.filter(
ascendant => !parentsToOmit.includes(ascendant.id) && ascendant.id !== null
);
return (
<CrumbSimpleMenu
onOpen={() => setShouldFetch(true)}
onClose={() => setShouldFetch(false)}
aria-label={formatMessage({
id: getTrad('header.breadcrumbs.menu.label'),
defaultMessage: 'Get more ascendants folders',
})}
label="..."
>
{isLoading && (
<MenuItem>
<Loader small>
{formatMessage({
id: getTrad('content.isLoading'),
defaultMessage: 'Content is loading.',
})}
</Loader>
</MenuItem>
)}
{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 (
<MenuItem isLink as={NavLink} to={url} key={ascendant.id}>
{ascendant.label}
</MenuItem>
);
})}
</CrumbSimpleMenu>
);
};
CrumbSimpleMenuAsync.defaultProps = {
currentFolderId: undefined,
onChangeFolder: undefined,
parentsToOmit: [],
};
CrumbSimpleMenuAsync.propTypes = {
currentFolderId: PropTypes.number,
onChangeFolder: PropTypes.func,
parentsToOmit: PropTypes.arrayOf(PropTypes.number),
};

View File

@ -0,0 +1 @@
export { Breadcrumbs } from './Breadcrumbs';

View File

@ -0,0 +1,393 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Media Library | Breadcrumbs should render and match snapshot 1`] = `
.c15 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.c5 {
padding-right: 4px;
padding-left: 4px;
}
.c12 {
padding-left: 8px;
}
.c14 {
padding-top: 4px;
padding-right: 8px;
padding-bottom: 4px;
padding-left: 8px;
}
.c7 {
color: #8e8ea9;
font-size: 0.75rem;
line-height: 1.33;
}
.c11 {
font-weight: 600;
color: #32324d;
font-size: 0.75rem;
line-height: 1.33;
}
.c1 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
.c3 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
.c2:first-child {
margin-left: calc(-1*8px);
}
.c4 {
border-radius: 4px;
color: #666687;
font-size: 0.75rem;
line-height: 1.43;
padding: 4px 8px;
-webkit-text-decoration: none;
text-decoration: none;
}
.c4:hover,
.c4:focus {
background-color: #dcdce4;
color: #4a4a6a;
}
.c8 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
cursor: pointer;
padding: 8px;
border-radius: 4px;
background: #ffffff;
border: 1px solid #dcdce4;
position: relative;
outline: none;
}
.c8 svg {
height: 12px;
width: 12px;
}
.c8 svg > g,
.c8 svg path {
fill: #ffffff;
}
.c8[aria-disabled='true'] {
pointer-events: none;
}
.c8:after {
-webkit-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.2s;
transition-duration: 0.2s;
border-radius: 8px;
content: '';
position: absolute;
top: -4px;
bottom: -4px;
left: -4px;
right: -4px;
border: 2px solid transparent;
}
.c8:focus-visible {
outline: none;
}
.c8:focus-visible:after {
border-radius: 8px;
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: -5px;
right: -5px;
border: 2px solid #4945ff;
}
.c9 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
padding: 8px 16px;
background: #4945ff;
border: 1px solid #4945ff;
border: 1px solid transparent;
background: transparent;
}
.c9 .c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c9 .c6 {
color: #ffffff;
}
.c9[aria-disabled='true'] {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c9[aria-disabled='true'] .c6 {
color: #666687;
}
.c9[aria-disabled='true'] svg > g,
.c9[aria-disabled='true'] svg path {
fill: #666687;
}
.c9[aria-disabled='true']:active {
border: 1px solid #dcdce4;
background: #eaeaef;
}
.c9[aria-disabled='true']:active .c6 {
color: #666687;
}
.c9[aria-disabled='true']:active svg > g,
.c9[aria-disabled='true']:active svg path {
fill: #666687;
}
.c9:hover {
background-color: #f6f6f9;
}
.c9:active {
border: 1px solid undefined;
background: undefined;
}
.c9 .c6 {
color: #32324d;
}
.c9 svg > g,
.c9 svg path {
fill: #8e8ea9;
}
.c13 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c13 svg {
height: 4px;
width: 6px;
}
.c10 {
padding: 4px 12px;
}
.c10:hover,
.c10:focus {
background-color: #dcdce4;
}
<div>
<nav
class="c0 "
>
<ol
class="c0 c1 c2"
>
<li
class="c0 c3"
>
<a
aria-current="page"
class="c4 active"
href="/"
>
Media Library
</a>
<div
aria-hidden="true"
class="c0 c5"
>
<span
class="c6 c7"
>
/
</span>
</div>
</li>
<li
class="c0 c3"
>
<div>
<button
aria-controls="simplemenu-1"
aria-disabled="false"
aria-expanded="false"
aria-haspopup="true"
aria-label="Get more ascendants folders"
class="c8 c9 c10"
label="..."
type="button"
>
<span
class="c6 c11"
>
...
</span>
<div
aria-hidden="true"
class="c0 c12"
>
<span
class="c13"
>
<svg
aria-hidden="true"
fill="none"
height="1em"
viewBox="0 0 14 8"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</button>
</div>
<div
aria-hidden="true"
class="c0 c5"
>
<span
class="c6 c7"
>
/
</span>
</div>
</li>
<li
class="c0 c3"
>
<a
aria-current="page"
class="c4 active"
href="/"
>
parent folder
</a>
<div
aria-hidden="true"
class="c0 c5"
>
<span
class="c6 c7"
>
/
</span>
</div>
</li>
<li
class="c0 c3"
>
<div
class="c0 c14"
>
<span
aria-current="true"
class="c6 c11"
>
current folder
</span>
</div>
</li>
</ol>
</nav>
<div
class="c15"
>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
aria-relevant="all"
id="live-region-alert"
role="alert"
/>
</div>
</div>
`;

View File

@ -0,0 +1,71 @@
import React from 'react';
import { render as renderTL, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClientProvider, QueryClient } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { IntlProvider } from 'react-intl';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { Breadcrumbs } from '../index';
jest.mock('../../../hooks/useFolderStructure');
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useQueryParams: jest.fn().mockReturnValue([{ query: { folder: 22 } }]),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});
const defaultBreadcrumbs = [
{
href: '/',
id: null,
label: 'Media Library',
},
[],
{ href: '/', id: 21, label: 'parent folder' },
{ id: 22, label: 'current folder' },
];
const setup = props =>
renderTL(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<Breadcrumbs breadcrumbs={defaultBreadcrumbs} as="nav" {...props} />
</ThemeProvider>
</IntlProvider>
</MemoryRouter>
</QueryClientProvider>
);
describe('Media Library | Breadcrumbs', () => {
test('should render and match snapshot', () => {
const { container } = setup({ currentFolderId: 22 });
expect(container.querySelector('nav')).toBeInTheDocument();
expect(screen.getByText('parent folder')).toBeInTheDocument();
expect(screen.getByText('current folder')).toBeInTheDocument();
expect(screen.getByText('Media Library')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
test('should store other ascendants in simple menu', async () => {
const { getByRole } = setup({ currentFolderId: 22 });
const simpleMenuButton = getByRole('button', { name: /get more ascendants folders/i });
fireEvent.mouseDown(simpleMenuButton);
await waitFor(() => {
expect(screen.getByText('second child')).toBeInTheDocument();
});
});
});

View File

@ -62,7 +62,7 @@ export const BulkMoveDialog = ({ onClose, selected, currentFolder }) => {
<Flex justifyContent="center" paddingTop={4} paddingBottom={4}>
<Loader>
{formatMessage({
id: getTrad('list.asset.load'),
id: getTrad('content.isLoading'),
defaultMessage: 'Content is loading.',
})}
</Loader>

View File

@ -146,7 +146,7 @@ export const EditAssetDialog = ({
<LoadingBody minHeight="60vh" justifyContent="center" paddingTop={4} paddingBottom={4}>
<Loader>
{formatMessage({
id: getTrad('list.asset.load'),
id: getTrad('content.isLoading'),
defaultMessage: 'Content is loading.',
})}
</Loader>

View File

@ -130,7 +130,7 @@ export const EditFolderDialog = ({ onClose, folder, location, parentFolderId })
<Flex justifyContent="center" paddingTop={4} paddingBottom={4}>
<Loader>
{formatMessage({
id: getTrad('list.asset.load'),
id: getTrad('content.isLoading'),
defaultMessage: 'Content is loading.',
})}
</Loader>

View File

@ -21,6 +21,8 @@ const ParentFolderDefinition = PropTypes.shape({
path: PropTypes.string.isRequired,
});
ParentFolderDefinition.parent = PropTypes.shape(ParentFolderDefinition);
export const FolderDefinition = PropTypes.shape({
id: PropTypes.number.isRequired,
children: PropTypes.shape({
@ -32,7 +34,7 @@ export const FolderDefinition = PropTypes.shape({
count: PropTypes.number.isRequired,
}),
name: PropTypes.string.isRequired,
parent: PropTypes.oneOf([ParentFolderDefinition, PropTypes.number]),
parent: PropTypes.oneOfType([ParentFolderDefinition, PropTypes.number]),
updatedAt: PropTypes.string.isRequired,
updatedBy: PropTypes.shape(),
pathId: PropTypes.number.isRequired,
@ -72,3 +74,15 @@ export const AssetDefinition = PropTypes.shape({
}),
}),
});
export const CrumbDefinition = PropTypes.shape({
id: PropTypes.number,
label: PropTypes.string.isRequired,
href: PropTypes.string,
});
export const CrumbMenuDefinition = PropTypes.arrayOf(CrumbDefinition);
export const BreadcrumbsDefinition = PropTypes.arrayOf(
PropTypes.oneOfType([CrumbDefinition, CrumbMenuDefinition])
);

View File

@ -19,7 +19,13 @@ export const useFolderStructure = jest.fn().mockReturnValue({
{
value: 21,
name: 'first child of the second child',
children: [],
children: [
{
value: 22,
name: 'another child',
children: [],
},
],
},
],
},

View File

@ -81,7 +81,9 @@ describe('useFolder', () => {
await waitFor(() => result.current.isSuccess);
await waitForNextUpdate();
expect(axiosInstance.get).toBeCalledWith(`/upload/folders/1?populate=parent`);
expect(axiosInstance.get).toBeCalledWith(
'/upload/folders/1?populate[parent][populate][parent]=*'
);
});
test('it does not fetch, if enabled is set to false', async () => {

View File

@ -10,7 +10,9 @@ export const useFolder = (id, { enabled = true }) => {
const fetchFolder = async () => {
try {
const { data } = await axiosInstance.get(`${dataRequestURL}/${id}?populate=parent`);
const { data } = await axiosInstance.get(
`${dataRequestURL}/${id}?populate[parent][populate][parent]=*`
);
return data.data;
} catch (err) {

View File

@ -2,7 +2,6 @@ import React, { useState, useRef } from 'react'; // useState
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { useLocation, useHistory } from 'react-router-dom';
import { stringify } from 'qs';
import {
LoadingIndicatorPage,
useFocusWhenNavigate,
@ -31,7 +30,7 @@ import { FolderList } from '../../components/FolderList';
import SortPicker from '../../components/SortPicker';
import { useAssets } from '../../hooks/useAssets';
import { useFolders } from '../../hooks/useFolders';
import { getTrad, containsAssetFilter } from '../../utils';
import { getTrad, containsAssetFilter, getBreadcrumbDataML, getFolderURL } from '../../utils';
import { PaginationFooter } from '../../components/PaginationFooter';
import { useMediaLibraryPermissions } from '../../hooks/useMediaLibraryPermissions';
import { useFolder } from '../../hooks/useFolder';
@ -148,8 +147,9 @@ export const MediaLibrary = () => {
<Layout>
<Main aria-busy={isLoading}>
<Header
assetCount={assetCount}
folderCount={folderCount}
breadcrumbs={
!isCurrentFolderLoading && getBreadcrumbDataML(currentFolder, { pathname, query })
}
canCreate={canCreate}
onToggleEditFolderDialog={toggleEditFolderDialog}
onToggleUploadAssetDialog={toggleUploadAssetDialog}
@ -230,10 +230,13 @@ export const MediaLibrary = () => {
<FolderList
title={
(((isFiltering && assetCount > 0) || !isFiltering) &&
formatMessage({
id: getTrad('list.folders.title'),
defaultMessage: 'Folders',
})) ||
formatMessage(
{
id: getTrad('list.folders.title'),
defaultMessage: 'Folders ({count})',
},
{ count: folderCount }
)) ||
''
}
>
@ -243,13 +246,7 @@ export const MediaLibrary = () => {
currentFolder => currentFolder.id === folder.id
);
// 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
const { _q, ...queryParamsWithoutQ } = query;
const url = `${pathname}?${stringify({
...queryParamsWithoutQ,
folder: folder.id,
})}`;
const url = getFolderURL(pathname, query, folder);
return (
<GridItem col={3} key={`folder-${folder.id}`}>
@ -334,10 +331,13 @@ export const MediaLibrary = () => {
title={
((!isFiltering || (isFiltering && folderCount > 0)) &&
assetsData?.pagination?.page === 1 &&
formatMessage({
id: getTrad('list.assets.title'),
defaultMessage: 'Assets',
})) ||
formatMessage(
{
id: getTrad('list.assets.title'),
defaultMessage: 'Assets ({count})',
},
{ count: assetCount }
)) ||
''
}
/>

View File

@ -11,15 +11,15 @@ import { Link } from '@strapi/design-system/Link';
import ArrowLeft from '@strapi/icons/ArrowLeft';
import Plus from '@strapi/icons/Plus';
import { getTrad } from '../../../utils';
import { FolderDefinition } from '../../../constants';
import { FolderDefinition, BreadcrumbsDefinition } from '../../../constants';
import { Breadcrumbs } from '../../../components/Breadcrumbs';
export const Header = ({
breadcrumbs,
canCreate,
folder,
onToggleEditFolderDialog,
onToggleUploadAssetDialog,
folder,
assetCount,
folderCount,
}) => {
const { formatMessage } = useIntl();
const { pathname } = useLocation();
@ -28,22 +28,27 @@ export const Header = ({
...query,
folder: folder?.parent?.id ?? undefined,
};
const name = folder?.name?.length > 30 ? `${folder.name.slice(0, 30)}...` : folder?.name;
return (
<HeaderLayout
title={`${formatMessage({
title={formatMessage({
id: getTrad('plugin.name'),
defaultMessage: `Media Library`,
})}${name ? ` - ${name}` : ''}`}
subtitle={formatMessage(
{
id: getTrad('header.content.assets'),
defaultMessage:
'{numberFolders, plural, one {1 folder} other {# folders}} - {numberAssets, plural, one {1 asset} other {# assets}}',
},
{ numberAssets: assetCount, numberFolders: folderCount }
)}
})}
subtitle={
breadcrumbs &&
folder && (
<Breadcrumbs
as="nav"
label={formatMessage({
id: getTrad('header.breadcrumbs.nav.label'),
defaultMessage: 'Folders navigation',
})}
breadcrumbs={breadcrumbs}
currentFolderId={folder?.id}
/>
)
}
navigationAction={
folder && (
<Link
@ -81,14 +86,14 @@ export const Header = ({
};
Header.defaultProps = {
breadcrumbs: false,
folder: null,
};
Header.propTypes = {
assetCount: PropTypes.number.isRequired,
breadcrumbs: PropTypes.oneOfType([BreadcrumbsDefinition, PropTypes.bool]),
canCreate: PropTypes.bool.isRequired,
folder: FolderDefinition,
folderCount: PropTypes.number.isRequired,
onToggleEditFolderDialog: PropTypes.func.isRequired,
onToggleUploadAssetDialog: PropTypes.func.isRequired,
};

View File

@ -85,15 +85,6 @@ describe('Header', () => {
expect(container).toMatchSnapshot();
});
test('truncates long folder lavels', () => {
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { folder: 2 } }, jest.fn()]);
const { queryByText } = setup({
folder: { ...FIXTURE_FOLDER, name: 'The length of this label exceeds the maximum length' },
});
expect(queryByText('Media Library - The length of this label excee...')).toBeInTheDocument();
});
test('does not render a back button at the root level of the media library', () => {
const { queryByText } = setup({ folder: null });

View File

@ -4,12 +4,13 @@ import { QueryClientProvider, QueryClient } from 'react-query';
import { render as renderTL, screen, waitFor, fireEvent } from '@testing-library/react';
import { useSelectionState, useQueryParams, TrackingContext } from '@strapi/helper-plugin';
import { MemoryRouter } from 'react-router-dom';
import { IntlProvider } from 'react-intl';
import { useMediaLibraryPermissions } from '../../../hooks/useMediaLibraryPermissions';
import { useFolders } from '../../../hooks/useFolders';
import { useAssets } from '../../../hooks/useAssets';
import { useFolder } from '../../../hooks/useFolder';
import { MediaLibrary } from '../MediaLibrary';
import en from '../../../translations/en.json';
const FIXTURE_ASSET_PAGINATION = {
pageCount: 1,
@ -56,7 +57,6 @@ jest.mock('../../../hooks/useMediaLibraryPermissions');
jest.mock('../../../hooks/useFolders');
jest.mock('../../../hooks/useFolder');
jest.mock('../../../hooks/useAssets');
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useRBAC: jest.fn(),
@ -66,17 +66,11 @@ jest.mock('@strapi/helper-plugin', () => ({
.fn()
.mockReturnValue([[], { selectOne: jest.fn(), selectAll: jest.fn() }]),
}));
jest.mock('../../../utils', () => ({
...jest.requireActual('../../../utils'),
getTrad: x => x,
}));
jest.mock('react-intl', () => ({
FormattedMessage: ({ id }) => id,
useIntl: () => ({ formatMessage: jest.fn(({ id }) => en[id] || id) }),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@ -89,13 +83,15 @@ const queryClient = new QueryClient({
const renderML = () =>
renderTL(
<QueryClientProvider client={queryClient}>
<TrackingContext.Provider value={{ uuid: false, telemetryProperties: undefined }}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<MediaLibrary />
</MemoryRouter>
</ThemeProvider>
</TrackingContext.Provider>
<IntlProvider locale="en" messages={{}}>
<TrackingContext.Provider value={{ uuid: false, telemetryProperties: undefined }}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>
<MediaLibrary />
</MemoryRouter>
</ThemeProvider>
</TrackingContext.Provider>
</IntlProvider>
</QueryClientProvider>
);
@ -107,7 +103,6 @@ describe('Media library homepage', () => {
describe('navigation', () => {
it('focuses the title when mounting the component', () => {
renderML();
expect(screen.getByRole('main')).toHaveFocus();
});
});
@ -119,27 +114,21 @@ describe('Media library homepage', () => {
canCreate: false,
canRead: false,
});
renderML();
expect(screen.getByRole('main').getAttribute('aria-busy')).toBe('true');
expect(screen.getByText('Loading content.')).toBeInTheDocument();
});
it('shows a loader while resolving assets', () => {
useAssets.mockReturnValueOnce({ isLoading: true });
renderML();
expect(screen.getByRole('main').getAttribute('aria-busy')).toBe('true');
expect(screen.getByText('Loading content.')).toBeInTheDocument();
});
it('shows a loader while resolving folders', () => {
useFolders.mockReturnValueOnce({ isLoading: true });
renderML();
expect(screen.getByRole('main').getAttribute('aria-busy')).toBe('true');
expect(screen.getByText('Loading content.')).toBeInTheDocument();
});
@ -150,7 +139,7 @@ describe('Media library homepage', () => {
it('shows the filters dropdown when the user is allowed to read', () => {
renderML();
expect(screen.getByText('app.utils.filters')).toBeInTheDocument();
expect(screen.getByText('Filters')).toBeInTheDocument();
});
it('hides the filters dropdown when the user is not allowed to read', () => {
@ -159,9 +148,7 @@ describe('Media library homepage', () => {
canRead: false,
canCreate: false,
});
renderML();
expect(screen.queryByText('app.utils.filters')).not.toBeInTheDocument();
});
});
@ -169,7 +156,6 @@ describe('Media library homepage', () => {
describe('sort by', () => {
it('shows the sort by dropdown when the user is allowed to read', () => {
renderML();
expect(screen.getByText('Sort by')).toBeInTheDocument();
});
@ -179,30 +165,28 @@ describe('Media library homepage', () => {
canRead: false,
canCreate: false,
});
renderML();
expect(screen.queryByText('Sort by')).not.toBeInTheDocument();
});
[
['Most recent uploads', 'createdAt:DESC'],
['Oldest uploads', 'createdAt:ASC'],
['Alphabetical order (A to Z)', 'name:ASC'],
['Reverse alphabetical order (Z to A)', 'name:DESC'],
['Most recent updates', 'updatedAt:DESC'],
['Oldest updates', 'updatedAt:ASC'],
].forEach(([label, sortKey]) => {
it(`modifies the URL with the according params: ${label} ${sortKey}`, async () => {
'createdAt:DESC',
'createdAt:ASC',
'name:ASC',
'name:DESC',
'updatedAt:DESC',
'updatedAt:ASC',
].forEach(sortKey => {
it(`modifies the URL with the according params: ${sortKey}`, async () => {
const setQueryMock = jest.fn();
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: {} }, setQueryMock]);
renderML();
fireEvent.mouseDown(screen.getByText('Sort by'));
await waitFor(() => expect(screen.getByText(label)).toBeInTheDocument());
fireEvent.mouseDown(screen.getByText(label));
await waitFor(() => expect(screen.queryByText(label)).not.toBeInTheDocument());
await waitFor(() => expect(screen.getByText(sortKey)).toBeInTheDocument());
fireEvent.mouseDown(screen.getByText(sortKey));
await waitFor(() => expect(screen.queryByText(sortKey)).not.toBeInTheDocument());
expect(setQueryMock).toBeCalledWith({ sort: sortKey });
});
@ -210,7 +194,13 @@ describe('Media library homepage', () => {
});
describe('select all', () => {
it('is not visible if there are not folders and assets', () => {
it('shows the select all button when the user is allowed to update', () => {
renderML();
expect(screen.getByLabelText('Select all folders & assets')).toBeInTheDocument();
});
it('hides the select all if there are not folders and assets', () => {
useAssets.mockReturnValueOnce({
isLoading: false,
error: null,
@ -221,17 +211,10 @@ describe('Media library homepage', () => {
isLoading: false,
error: null,
});
renderML();
expect(
screen.queryByText('There are no elements with the applied filters')
).not.toBeInTheDocument();
});
it('shows the select all button when the user is allowed to update', () => {
renderML();
expect(screen.getByLabelText('Select all assets')).toBeInTheDocument();
expect(screen.queryByLabelText('Select all assets')).not.toBeInTheDocument();
});
it('hides the select all button when the user is not allowed to update', () => {
@ -241,9 +224,7 @@ describe('Media library homepage', () => {
canCreate: true,
canUpdate: false,
});
renderML();
expect(screen.queryByLabelText('Select all assets')).not.toBeInTheDocument();
});
});
@ -255,9 +236,7 @@ describe('Media library homepage', () => {
canRead: false,
canCreate: false,
});
renderML();
await waitFor(() => expect(screen.queryByText(`Add new assets`)).not.toBeInTheDocument());
});
@ -267,9 +246,7 @@ describe('Media library homepage', () => {
canRead: true,
canCreate: true,
});
renderML();
await waitFor(() => expect(screen.getByText(`Add new assets`)).toBeInTheDocument());
});
});
@ -277,7 +254,6 @@ describe('Media library homepage', () => {
describe('create folder', () => {
it('shows the create button if the user has create permissions', () => {
renderML();
expect(screen.getByText('Add new folder')).toBeInTheDocument();
});
@ -286,28 +262,39 @@ describe('Media library homepage', () => {
isLoading: false,
canCreate: false,
});
renderML();
expect(screen.queryByText('Add new folder')).not.toBeInTheDocument();
});
});
});
describe('content', () => {
it('should show breadcrumbs navigation', () => {
renderML();
expect(screen.queryByLabelText('Folders navigation')).toBeInTheDocument();
});
it('should hide breadcrumbs navigation if in root folder', () => {
useFolder.mockReturnValueOnce({ isLoading: false, data: undefined });
renderML();
expect(screen.queryByLabelText('Folders navigation')).not.toBeInTheDocument();
});
it('does display empty state upload first assets if no folder or assets', () => {
useFolders.mockReturnValueOnce({
data: [],
isLoading: false,
error: null,
});
useAssets.mockReturnValueOnce({
isLoading: false,
error: null,
data: {},
});
renderML();
expect(screen.queryByText('Upload your first assets...')).toBeInTheDocument();
});
@ -324,7 +311,6 @@ describe('Media library homepage', () => {
});
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]);
renderML();
expect(
screen.queryByText('There are no elements with the applied filters')
).toBeInTheDocument();
@ -338,7 +324,6 @@ describe('Media library homepage', () => {
});
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]);
renderML();
expect(screen.queryByText('Assets')).not.toBeInTheDocument();
});
@ -350,15 +335,14 @@ describe('Media library homepage', () => {
});
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]);
renderML();
expect(screen.queryByText('Folders')).not.toBeInTheDocument();
});
it('displays folders and folders title', () => {
renderML();
expect(screen.queryByText('Folders')).toBeInTheDocument();
expect(screen.getByText('Folder 1')).toBeInTheDocument();
expect(screen.getByText('Folders (1)')).toBeInTheDocument();
expect(screen.getByText('1 folder, 1 asset')).toBeInTheDocument();
});
it('displays folder with checked checkbox when is selected', () => {
@ -379,13 +363,11 @@ describe('Media library homepage', () => {
{ selectOne: jest.fn(), selectAll: jest.fn() },
]);
renderML();
expect(screen.getByTestId('folder-checkbox-1')).toBeChecked();
});
it('doest not displays folder with checked checkbox when is not selected', () => {
renderML();
expect(screen.getByTestId('folder-checkbox-1')).not.toBeChecked();
});
@ -398,8 +380,8 @@ describe('Media library homepage', () => {
renderML();
expect(screen.queryByText('list.folders.title')).not.toBeInTheDocument();
expect(screen.queryByText('Folder 1')).not.toBeInTheDocument();
expect(screen.queryByText('1 folder, 1 asset')).not.toBeInTheDocument();
expect(screen.queryByText('Folders (1)')).not.toBeInTheDocument();
});
it('does display folders if a search is performed', () => {
@ -407,17 +389,17 @@ describe('Media library homepage', () => {
renderML();
expect(screen.queryByText('Folders')).toBeInTheDocument();
expect(screen.queryByText('Folder 1')).toBeInTheDocument();
expect(screen.queryByText('1 folder, 1 asset')).toBeInTheDocument();
expect(screen.queryByText('Folders (1)')).toBeInTheDocument();
});
it('does not display folders if the media library is being filtered', () => {
it('does display folders if the media library is being filtered', () => {
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { filters: 'true' } }, jest.fn()]);
renderML();
expect(screen.queryByText('Folders')).toBeInTheDocument();
expect(screen.queryByText('Folder 1')).toBeInTheDocument();
expect(screen.queryByText('1 folder, 1 asset')).toBeInTheDocument();
expect(screen.queryByText('Folders (1)')).toBeInTheDocument();
});
it('does not fetch folders if the current page !== 1', () => {
@ -434,9 +416,7 @@ describe('Media library homepage', () => {
},
});
useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]);
renderML();
expect(useFolders).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
});
@ -457,15 +437,12 @@ describe('Media library homepage', () => {
{ rawQuery: '', query: { _q: '', filters: { $and: { mime: 'audio' } } } },
jest.fn(),
]);
renderML();
expect(useFolders).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
});
it('displays assets', () => {
renderML();
expect(screen.getByText('3874873.jpg')).toBeInTheDocument();
});
@ -475,9 +452,7 @@ describe('Media library homepage', () => {
canRead: false,
canCreate: false,
});
renderML();
expect(screen.queryByText('3874873.jpg')).not.toBeInTheDocument();
});
@ -489,14 +464,11 @@ describe('Media library homepage', () => {
results: [],
},
});
useFolders.mockReturnValueOnce({
isLoading: false,
data: [],
});
renderML();
expect(screen.queryByText('Upload your first assets...')).toBeInTheDocument();
});
@ -510,15 +482,12 @@ describe('Media library homepage', () => {
results: [],
},
});
useFolders.mockReturnValueOnce({
isLoading: false,
error: null,
data: [],
});
renderML();
expect(
screen.queryByText('There are no elements with the applied filters')
).toBeInTheDocument();

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header renders 1`] = `
.c20 {
.c19 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
@ -310,12 +310,6 @@ exports[`Header renders 1`] = `
line-height: 1.25;
}
.c19 {
color: #666687;
font-size: 1rem;
line-height: 1.5;
}
.c5 {
color: #4945ff;
font-size: 0.75rem;
@ -435,7 +429,7 @@ exports[`Header renders 1`] = `
<h1
class="c8"
>
Media Library - Folder 1
Media Library
</h1>
</div>
<div
@ -500,15 +494,10 @@ exports[`Header renders 1`] = `
</button>
</div>
</div>
<p
class="c19"
>
2 folders - 2 assets
</p>
</div>
</div>
<div
class="c20"
class="c19"
>
<p
aria-live="polite"

View File

@ -3,6 +3,7 @@
"button.next": "Següent",
"checkControl.crop-duplicate": "Duplica i retalla l'imatge",
"checkControl.crop-original": "Retalla l'imatge original",
"content.isLoading": "S'està carregant la llista de fitxers.",
"control-card.add": "Afegeix",
"control-card.cancel": "Cancel·lar",
"control-card.copy-link": "Copia l'enllaç",
@ -35,7 +36,6 @@
"input.url.description": "Separeu els vostres enllaços URL mitjançant un retorn de carro.",
"input.url.label": "URL",
"list.asset.at.finished": "Els fitxers s'han acabat de carregar.",
"list.asset.load": "S'està carregant la llista de fitxers.",
"list.assets-empty.search": "No s'ha trobat cap resultat",
"list.assets-empty.subtitle": "Afegiu-ne un a la llista.",
"list.assets-empty.title": "Encara no hi ha fitxers",

View File

@ -3,6 +3,7 @@
"button.next": "Weiter",
"checkControl.crop-duplicate": "Duplizieren & zuschneiden",
"checkControl.crop-original": "Orginal zuschneiden",
"content.isLoading": "Lade die Datei-Liste.",
"control-card.add": "Hinzufügen",
"control-card.cancel": "Abbrechen",
"control-card.copy-link": "Link kopieren",
@ -34,7 +35,6 @@
"input.url.description": "Eine URL pro Zeile.",
"input.url.label": "URL",
"list.asset.at.finished": "Die Datei-Liste wurde geladen.",
"list.asset.load": "Lade die Datei-Liste.",
"list.assets-empty.search": "Keine Ergebnisse gefunden",
"list.assets-empty.subtitle": "Füge welche der Liste hinzu.",
"list.assets-empty.title": "Keine Dateien vorhanden.",

View File

@ -3,6 +3,7 @@
"button.next": "Næste",
"checkControl.crop-duplicate": "Duplikér og beskær",
"checkControl.crop-original": "Beskær originalen",
"content.isLoading": "Henter medieliste",
"control-card.add": "Tilføj",
"control-card.cancel": "Annuller",
"control-card.copy-link": "Kopier link",
@ -32,7 +33,6 @@
"input.url.description": "Adskil dine URL links med enter.",
"input.url.label": "URL",
"list.asset.at.finished": "Mediet er færdigt med at loade.",
"list.asset.load": "Henter medieliste",
"list.assets-empty.search": "Ingen resultater fundet",
"list.assets-empty.subtitle": "Tilføj en til listen.",
"list.assets-empty.title": "Der er ingen medier endnu",

View File

@ -5,6 +5,7 @@
"button.next": "Next",
"checkControl.crop-duplicate": "Duplicate & crop the asset",
"checkControl.crop-original": "Crop the original asset",
"content.isLoading": "Content is loading.",
"control-card.add": "Add",
"control-card.cancel": "Cancel",
"control-card.copy-link": "Copy link",
@ -39,7 +40,6 @@
"input.url.label": "URL",
"list.assets.title": "Assets",
"list.asset.at.finished": "The assets have finished loading.",
"list.asset.load": "Content is loading.",
"list.assets-empty.search": "No result found",
"list.assets-empty.subtitle": "Add one to the list.",
"list.assets-empty.title": "There are no assets yet",

View File

@ -3,6 +3,7 @@
"button.next": "Siguiente",
"checkControl.crop-duplicate": "Duplicar y cortar el recurso",
"checkControl.crop-original": "Recortar el recurso original",
"content.isLoading": "Cargando la lista de recursos.",
"control-card.add": "Añadir",
"control-card.cancel": "Cancelar",
"control-card.copy-link": "Copiar link",
@ -32,7 +33,6 @@
"input.url.description": "Separe sus enlaces URL con un salto de línea.",
"input.url.label": "URL",
"list.asset.at.finished": "Los recursos han terminado de cargarse.",
"list.asset.load": "Cargando la lista de recursos.",
"list.assets-empty.search": "No se han encontrado resultados",
"list.assets-empty.subtitle": "Agrega uno a la lista.",
"list.assets-empty.title": "Todavía no hay recursos",

View File

@ -3,6 +3,7 @@
"button.next": "다음",
"checkControl.crop-duplicate": "에셋 복제 & 자르기",
"checkControl.crop-original": "원본 에셋 자르기",
"content.isLoading": "에셋을 불러오고 있습니다.",
"control-card.add": "추가",
"control-card.cancel": "취소",
"control-card.copy-link": "링크 복사",
@ -33,7 +34,6 @@
"input.url.description": "URL 링크를 캐리지 리턴(줄바꿈)으로 구분합니다.",
"input.url.label": "URL",
"list.asset.at.finished": "에셋 로딩이 완료되었습니다.",
"list.asset.load": "에셋을 불러오고 있습니다.",
"list.assets-empty.search": "결과를 찾을 수 없습니다.",
"list.assets-empty.subtitle": "Add one to the list.",
"list.assets-empty.title": "에셋이 없습니다.",

View File

@ -3,6 +3,7 @@
"button.next": "Dalej",
"checkControl.crop-duplicate": "Duplikuj i przytnij",
"checkControl.crop-original": "Przytnij",
"content.isLoading": "Ładowanie listy.",
"control-card.add": "Dodaj",
"control-card.cancel": "Anuluj",
"control-card.copy-link": "Kopiuj link",
@ -32,7 +33,6 @@
"input.url.description": "Oddziel swoje linki URL umieszczając je w nowych liniach.",
"input.url.label": "URL",
"list.asset.at.finished": "Załadowano.",
"list.asset.load": "Ładowanie listy.",
"list.assets-empty.search": "Brak wyników wyszukiwania",
"list.assets-empty.subtitle": "Dodaj jeden do listy.",
"list.assets-empty.title": "Nie ma jeszcze żadnych zasobów",

View File

@ -0,0 +1,32 @@
import getTrad from './getTrad';
const getBreadcrumbDataML = folder => {
let data = [
{
id: null,
label: { id: getTrad('plugin.name'), defaultMessage: '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

@ -0,0 +1,35 @@
import getTrad from './getTrad';
import getFolderURL from './getFolderURL';
const getBreadcrumbDataML = (folder, { pathname, query }) => {
let data = [
{
id: null,
label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' },
href: folder ? getFolderURL(pathname, query) : undefined,
},
];
if (folder?.parent?.parent) {
data.push([]);
}
if (folder?.parent) {
data.push({
id: folder.parent.id,
label: folder.parent.name,
href: getFolderURL(pathname, query, folder.parent),
});
}
if (folder) {
data.push({
id: folder.id,
label: folder.name,
});
}
return data;
};
export default getBreadcrumbDataML;

View File

@ -0,0 +1,24 @@
import flattenTree from '../components/SelectTree/utils/flattenTree';
const getFolderParents = (folders, currentFolderId) => {
const parents = [];
const flatFolders = flattenTree(folders);
const currentFolder = flatFolders.find(folder => folder.value === currentFolderId);
if (!currentFolder) {
return [];
}
let { parent } = currentFolder;
while (parent !== undefined) {
// eslint-disable-next-line no-loop-func
let parentToStore = flatFolders.find(({ value }) => value === parent);
parents.push({ id: parentToStore.value, label: parentToStore.label });
parent = parentToStore.parent;
}
return parents.reverse();
};
export default getFolderParents;

View File

@ -0,0 +1,18 @@
import { stringify } from 'qs';
const getFolderURL = (pathname, query, folder) => {
const { _q, ...queryParamsWithoutQ } = query;
const queryParamsString = stringify(
{
...queryParamsWithoutQ,
folder: folder?.id,
},
{ encode: false }
);
// 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
return `${pathname}${queryParamsString ? `?${queryParamsString}` : ''}`;
};
export default getFolderURL;

View File

@ -5,4 +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 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

@ -0,0 +1,86 @@
import { getBreadcrumbDataCM } from '..';
const FIXTURE_FOLDER = {
id: 1,
name: 'first-level',
};
describe('getBreadcrumbDataCM', () => {
test('return one item at the root of the media library', () => {
expect(getBreadcrumbDataCM(null)).toStrictEqual([
{
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
},
]);
});
test('returns two items for the first level of the media library', () => {
expect(getBreadcrumbDataCM(FIXTURE_FOLDER)).toStrictEqual([
{
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
},
{
id: 1,
label: 'first-level',
},
]);
});
test('returns three items for the second level of the media library', () => {
expect(
getBreadcrumbDataCM({ ...FIXTURE_FOLDER, parent: { id: 2, name: 'second-level' } })
).toStrictEqual([
{
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
},
{
id: 2,
label: 'second-level',
},
{
id: 1,
label: 'first-level',
},
]);
});
test('returns four items for the third level of the media library', () => {
expect(
getBreadcrumbDataCM({
...FIXTURE_FOLDER,
parent: { id: 2, name: 'second-level', parent: { id: 3, name: 'third-level' } },
})
).toStrictEqual([
{
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
},
[],
{
id: 2,
label: 'second-level',
},
{
id: 1,
label: 'first-level',
},
]);
});
});

View File

@ -0,0 +1,111 @@
import { getBreadcrumbDataML } from '..';
const FIXTURE_PATHNAME = '/media-library';
const FIXTURE_QUERY = {
_q: 'search',
some: 'thing',
};
const FIXTURE_FOLDER = {
id: 1,
name: 'first-level',
};
describe('getBreadcrumbDataML', () => {
test('return one item at the root of the media library', () => {
expect(
getBreadcrumbDataML(null, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY })
).toStrictEqual([
{
href: undefined,
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
},
]);
});
test('returns two items for the first level of the media library', () => {
expect(
getBreadcrumbDataML(FIXTURE_FOLDER, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY })
).toStrictEqual([
{
href: '/media-library?some=thing',
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
},
{
id: 1,
label: 'first-level',
},
]);
});
test('returns three items for the second level of the media library', () => {
expect(
getBreadcrumbDataML(
{ ...FIXTURE_FOLDER, parent: { id: 2, name: 'second-level' } },
{ pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY }
)
).toStrictEqual([
{
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
href: '/media-library?some=thing',
},
{
id: 2,
label: 'second-level',
href: '/media-library?some=thing&folder=2',
},
{
id: 1,
label: 'first-level',
},
]);
});
test('returns four items for the third level of the media library', () => {
expect(
getBreadcrumbDataML(
{
...FIXTURE_FOLDER,
parent: { id: 2, name: 'second-level', parent: { id: 3, name: 'third-level' } },
},
{ pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY }
)
).toStrictEqual([
{
id: null,
label: {
id: 'upload.plugin.name',
defaultMessage: 'Media Library',
},
href: '/media-library?some=thing',
},
[],
{
id: 2,
label: 'second-level',
href: '/media-library?some=thing&folder=2',
},
{
id: 1,
label: 'first-level',
},
]);
});
});

View File

@ -0,0 +1,69 @@
import getFolderParents from '../getFolderParents';
const FIXTURE_FOLDER_STRUCTURE = [
{
value: null,
label: 'Media Library',
children: [
{
value: 1,
label: 'First folder',
children: [
{
value: 2,
label: 'Second folder',
children: [
{
value: 3,
label: 'Third folder',
children: [
{
value: 5,
label: 'Fourth folder',
children: [],
},
],
},
{
value: 4,
label: 'Second folder sibling',
children: [],
},
],
},
],
},
],
},
];
describe('getFolderParents', () => {
test('should return ascendants', () => {
const result = getFolderParents(FIXTURE_FOLDER_STRUCTURE, 3);
const expected = [
{ id: null, label: 'Media Library' },
{ id: 1, label: 'First folder' },
{ id: 2, label: 'Second folder' },
];
expect(result).toEqual(expected);
});
test('should not return parent siblings', () => {
const result = getFolderParents(FIXTURE_FOLDER_STRUCTURE, 5);
const expected = [
{ id: null, label: 'Media Library' },
{ id: 1, label: 'First folder' },
{ id: 2, label: 'Second folder' },
{ id: 3, label: 'Third folder' },
];
expect(result).toEqual(expected);
});
test('should return array if current folder id not found', () => {
const result = getFolderParents(FIXTURE_FOLDER_STRUCTURE, 8);
expect(result).toEqual([]);
});
});

View File

@ -0,0 +1,31 @@
import { getFolderURL } from '..';
const FIXTURE_PATHNAME = '/media-library';
const FIXTURE_QUERY = {};
const FIXTURE_FOLDER = {
id: 1,
};
describe('getFolderURL', () => {
test('returns a path for the root of the media library', () => {
expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY)).toStrictEqual(FIXTURE_PATHNAME);
});
test('returns a path for a folder', () => {
expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, FIXTURE_FOLDER)).toStrictEqual(
`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER.id}`
);
});
test('removes _q query parameter', () => {
expect(
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, _q: 'search' }, FIXTURE_FOLDER)
).toStrictEqual(`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER.id}`);
});
test('keeps and stringifies query parameter', () => {
expect(
getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, some: 'thing' }, FIXTURE_FOLDER)
).toStrictEqual(`${FIXTURE_PATHNAME}?some=thing&folder=${FIXTURE_FOLDER.id}`);
});
});

View File

@ -62,7 +62,7 @@ module.exports = async ({ strapi }) => {
: ApolloServerPluginLandingPageGraphQLPlayground(),
],
cache: 'bounded'
cache: 'bounded',
};
const serverConfig = merge(defaultServerConfig, config('apolloServer'));
@ -114,7 +114,7 @@ module.exports = async ({ strapi }) => {
// allow graphql playground to load without authentication
if (ctx.request.method === 'GET') return next();
return strapi.auth.authenticate(ctx, next);
},