mirror of
https://github.com/strapi/strapi.git
synced 2025-12-02 10:04:09 +00:00
breadcrumbs in ML modal - CM
This commit is contained in:
parent
110317b6b1
commit
bdc0454cd7
@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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),
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
30
packages/core/upload/admin/src/utils/getBreadcrumbDataCM.js
Normal file
30
packages/core/upload/admin/src/utils/getBreadcrumbDataCM.js
Normal 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;
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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' } },
|
||||
Loading…
x
Reference in New Issue
Block a user