Merge remote-tracking branch 'origin/features/MediaLibrary-listview' into MediaLibrary-listview/view-switch

This commit is contained in:
Jamie Howard 2022-11-22 11:24:50 +00:00
commit 530cb8b701
18 changed files with 1505 additions and 56 deletions

View File

@ -23,7 +23,7 @@ import { FolderDefinition, AssetDefinition, viewOptions } from '../../../constan
import getTrad from '../../../utils/getTrad';
import { getBreadcrumbDataCM } from '../../../utils';
import getAllowedFiles from '../../../utils/getAllowedFiles';
import { AssetList } from '../../AssetList';
import { AssetGridList } from '../../AssetGridList';
import { FolderList } from '../../FolderList';
import { EmptyAssets } from '../../EmptyAssets';
import { Breadcrumbs } from '../../Breadcrumbs';
@ -304,7 +304,7 @@ export const BrowseStep = ({
{assetCount > 0 && (
<Box paddingTop={6}>
<AssetList
<AssetGridList
allowedTypes={allowedTypes}
size="S"
assets={assets}

View File

@ -3,7 +3,7 @@ import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Stack } from '@strapi/design-system/Stack';
import { Typography } from '@strapi/design-system/Typography';
import { AssetList } from '../../AssetList';
import { AssetGridList } from '../../AssetGridList';
import getTrad from '../../../utils/getTrad';
export const SelectedStep = ({ selectedAssets, onSelectAsset, onReorderAsset }) => {
@ -30,7 +30,7 @@ export const SelectedStep = ({ selectedAssets, onSelectAsset, onReorderAsset })
</Typography>
</Stack>
<AssetList
<AssetGridList
size="S"
assets={selectedAssets}
onSelectAsset={onSelectAsset}

View File

@ -8,7 +8,7 @@ import { Typography } from '@strapi/design-system/Typography';
import { AssetCard } from '../AssetCard/AssetCard';
import { Draggable } from './Draggable';
export const AssetList = ({
export const AssetGridList = ({
allowedTypes,
assets,
onEditAsset,
@ -41,7 +41,7 @@ export const AssetList = ({
asset={asset}
isSelected={isSelected}
onEdit={onEditAsset ? () => onEditAsset(asset) : undefined}
onSelect={() => onSelectAsset({ ...asset, type: 'asset' })}
onSelect={() => onSelectAsset(asset)}
size={size}
/>
</Draggable>
@ -57,7 +57,7 @@ export const AssetList = ({
asset={asset}
isSelected={isSelected}
onEdit={onEditAsset ? () => onEditAsset(asset) : undefined}
onSelect={() => onSelectAsset({ ...asset, type: 'asset' })}
onSelect={() => onSelectAsset(asset)}
size={size}
/>
</GridItem>
@ -68,7 +68,7 @@ export const AssetList = ({
);
};
AssetList.defaultProps = {
AssetGridList.defaultProps = {
allowedTypes: ['images', 'files', 'videos', 'audios'],
onEditAsset: undefined,
size: 'M',
@ -76,7 +76,7 @@ AssetList.defaultProps = {
title: null,
};
AssetList.propTypes = {
AssetGridList.propTypes = {
allowedTypes: PropTypes.arrayOf(PropTypes.string),
assets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onEditAsset: PropTypes.func,

View File

@ -2,7 +2,7 @@ import React from 'react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { render as renderTL } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { AssetList } from '..';
import { AssetGridList } from '..';
import en from '../../../translations/en.json';
jest.mock('../../../utils', () => ({
@ -123,7 +123,7 @@ const setup = (props = { assets: data, selectedAssets: [], onSelectAsset: jest.f
renderTL(
<MemoryRouter>
<ThemeProvider theme={lightTheme}>
<AssetList {...props} />
<AssetGridList {...props} />
</ThemeProvider>
</MemoryRouter>
);

View File

@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { getFileExtension } from '@strapi/helper-plugin';
import { Typography } from '@strapi/design-system/Typography';
import { PreviewCell } from './PreviewCell';
import { formatBytes } from '../../utils';
export const CellContent = ({
alternativeText,
content,
cellType,
elementType,
mime,
fileExtension,
thumbnailURL,
url,
}) => {
const { formatDate } = useIntl();
switch (cellType) {
case 'image':
return (
<PreviewCell
alternativeText={alternativeText}
fileExtension={fileExtension}
mime={mime}
type={elementType}
thumbnailURL={thumbnailURL}
url={url}
/>
);
case 'date':
return <Typography>{formatDate(content)}</Typography>;
case 'size':
if (elementType === 'folder') return <Typography>-</Typography>;
return <Typography>{formatBytes(content)}</Typography>;
case 'ext':
if (elementType === 'folder') return <Typography>-</Typography>;
return <Typography>{getFileExtension(content).toUpperCase()}</Typography>;
case 'text':
return <Typography>{content}</Typography>;
default:
return <Typography>-</Typography>;
}
};
CellContent.defaultProps = {
alternativeText: null,
content: '',
fileExtension: '',
mime: '',
thumbnailURL: null,
url: null,
};
CellContent.propTypes = {
alternativeText: PropTypes.string,
content: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
fileExtension: PropTypes.string,
mime: PropTypes.string,
thumbnailURL: PropTypes.string,
cellType: PropTypes.string.isRequired,
elementType: PropTypes.string.isRequired,
url: PropTypes.string,
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { prefixFileUrlWithBackendUrl, pxToRem } from '@strapi/helper-plugin';
import { Avatar } from '@strapi/design-system/Avatar';
import { Flex } from '@strapi/design-system/Flex';
import { Typography } from '@strapi/design-system/Typography';
import { Icon } from '@strapi/design-system/Icon';
import Folder from '@strapi/icons/Folder';
const GenericAssetWrapper = styled(Flex)`
span {
/* The smallest fontSize in the DS is not small enough in this case */
font-size: ${pxToRem(10)};
}
`;
export const PreviewCell = ({ alternativeText, fileExtension, mime, thumbnailURL, type, url }) => {
if (type === 'folder') {
return (
<Flex
background="secondary100"
height={pxToRem(26)}
justifyContent="center"
width={pxToRem(26)}
borderRadius="50%"
>
<Icon color="secondary500" as={Folder} />
</Flex>
);
}
if (mime.includes('image')) {
const mediaURL = prefixFileUrlWithBackendUrl(thumbnailURL) ?? prefixFileUrlWithBackendUrl(url);
return <Avatar src={mediaURL} alt={alternativeText} preview />;
}
return (
<GenericAssetWrapper
background="secondary100"
height={pxToRem(26)}
justifyContent="center"
width={pxToRem(26)}
borderRadius="50%"
>
<Typography variant="sigma" textColor="secondary600">
{fileExtension}
</Typography>
</GenericAssetWrapper>
);
};
PreviewCell.defaultProps = {
alternativeText: null,
fileExtension: '',
mime: '',
thumbnailURL: null,
url: null,
};
PreviewCell.propTypes = {
alternativeText: PropTypes.string,
fileExtension: PropTypes.string,
mime: PropTypes.string,
thumbnailURL: PropTypes.string,
type: PropTypes.string.isRequired,
url: PropTypes.string,
};

View File

@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { getFileExtension } from '@strapi/helper-plugin';
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
import { IconButton } from '@strapi/design-system/IconButton';
import { Tbody, Td, Tr } from '@strapi/design-system/Table';
import Pencil from '@strapi/icons/Pencil';
import { CellContent } from './CellContent';
import { AssetDefinition, FolderDefinition, tableHeaders as cells } from '../../constants';
import { getTrad } from '../../utils';
export const TableRows = ({ onEditAsset, onEditFolder, onSelectOne, rows, selected }) => {
const { formatMessage } = useIntl();
return (
<Tbody>
{rows.map((element) => {
const { alternativeText, id, name, ext, url, mime, formats, type: elementType } = element;
const isSelected = !!selected.find((currentRow) => currentRow.id === id);
return (
<Tr key={id}>
<Td>
<BaseCheckbox
aria-label={formatMessage(
{
id: elementType === 'asset' ? 'list-assets-select' : 'list.folder.select',
defaultMessage:
elementType === 'asset' ? 'Select {name} asset' : 'Select {name} folder',
},
{ name }
)}
onValueChange={() => onSelectOne({ ...element, elementType })}
checked={isSelected}
/>
</Td>
{cells.map(({ name, type: cellType }) => {
return (
<Td key={name}>
<CellContent
alternativeText={alternativeText}
content={element[name]}
fileExtension={getFileExtension(ext)}
mime={mime}
cellType={cellType}
elementType={elementType}
thumbnailURL={formats?.thumbnail?.url}
url={url}
/>
</Td>
);
})}
{((elementType === 'asset' && onEditAsset) ||
(elementType === 'folder' && onEditFolder)) && (
<Td>
<IconButton
label={formatMessage({
id: getTrad('control-card.edit'),
defaultMessage: 'Edit',
})}
onClick={() =>
elementType === 'asset' ? onEditAsset(element) : onEditFolder(element)
}
noBorder
>
<Pencil />
</IconButton>
</Td>
)}
</Tr>
);
})}
</Tbody>
);
};
TableRows.defaultProps = {
onEditAsset: null,
onEditFolder: null,
rows: [],
selected: [],
};
TableRows.propTypes = {
rows: PropTypes.arrayOf(AssetDefinition, FolderDefinition),
onEditAsset: PropTypes.func,
onEditFolder: PropTypes.func,
onSelectOne: PropTypes.func.isRequired,
selected: PropTypes.arrayOf(AssetDefinition, FolderDefinition),
};

View File

@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
import { Table, Th, Thead, Tr } from '@strapi/design-system/Table';
import { Typography } from '@strapi/design-system/Typography';
import { VisuallyHidden } from '@strapi/design-system/VisuallyHidden';
import { getTrad } from '../../utils';
import { AssetDefinition, tableHeaders, FolderDefinition } from '../../constants';
import { TableRows } from './TableRows';
export const TableList = ({
assetCount,
folderCount,
indeterminate,
onEditAsset,
onEditFolder,
onSelectAll,
onSelectOne,
rows,
selected,
}) => {
const { formatMessage } = useIntl();
return (
<Table colCount={tableHeaders.length + 2} rowCount={assetCount + folderCount + 1}>
<Thead>
<Tr>
<Th>
<BaseCheckbox
aria-label={formatMessage({
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all folders & assets',
})}
indeterminate={indeterminate}
onChange={(e) => onSelectAll(e, rows)}
value={
(assetCount > 0 || folderCount > 0) && selected.length === assetCount + folderCount
}
/>
</Th>
{tableHeaders.map(({ metadatas, key }) => {
return (
<Th key={key}>
<Typography textColor="neutral600" variant="sigma">
{formatMessage(metadatas.label)}
</Typography>
</Th>
);
})}
<Th>
<VisuallyHidden>
{formatMessage({
id: getTrad('list-table-header-actions'),
defaultMessage: 'actions',
})}
</VisuallyHidden>
</Th>
</Tr>
</Thead>
<TableRows
onEditAsset={onEditAsset}
onEditFolder={onEditFolder}
rows={rows}
onSelectOne={onSelectOne}
selected={selected}
/>
</Table>
);
};
TableList.defaultProps = {
assetCount: 0,
folderCount: 0,
indeterminate: false,
onEditAsset: null,
onEditFolder: null,
rows: [],
selected: [],
};
TableList.propTypes = {
assetCount: PropTypes.number,
folderCount: PropTypes.number,
indeterminate: PropTypes.bool,
onEditAsset: PropTypes.func,
onEditFolder: PropTypes.func,
onSelectAll: PropTypes.func.isRequired,
onSelectOne: PropTypes.func.isRequired,
rows: PropTypes.arrayOf(AssetDefinition, FolderDefinition),
selected: PropTypes.arrayOf(AssetDefinition, FolderDefinition),
};

View File

@ -0,0 +1,109 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import { render } from '@testing-library/react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { CellContent } from '../CellContent';
const PROPS_FIXTURE = {
alternativeText: 'alternative alt',
cellType: 'image',
elementType: 'asset',
content: 'michka-picture-url-default.jpeg',
fileExtension: '.jpeg',
mime: 'image/jpeg',
thumbnailURL: 'michka-picture-url-thumbnail.jpeg',
url: 'michka-picture-url-default.jpeg',
};
const ComponentFixture = (props) => {
const customProps = {
...PROPS_FIXTURE,
...props,
};
return (
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<CellContent {...PROPS_FIXTURE} {...customProps} />
</ThemeProvider>
</IntlProvider>
);
};
const setup = (props) => render(<ComponentFixture {...props} />);
describe('TableList | CellContent', () => {
it('should render image cell type when element type is asset and mime includes image', () => {
const { container, getByRole } = setup();
expect(getByRole('img', { name: 'alternative alt' })).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render image cell type when element type is asset and mime does not include image', () => {
const { container, getByText } = setup({ mime: 'application/pdf', fileExtension: 'pdf' });
expect(getByText('pdf')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render image cell type when element type is folder', () => {
const { container } = setup({ elementType: 'folder' });
expect(container.querySelector('path')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render text cell type', () => {
const { container, getByText } = setup({ cellType: 'text', content: 'some text' });
expect(getByText('some text')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render extension cell type when element type is asset', () => {
const { container, getByText } = setup({ cellType: 'ext', content: '.pdf' });
expect(getByText('PDF')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render extension cell type with "-" when element type is folder', () => {
const { container, getByText } = setup({ cellType: 'ext', elementType: 'folder' });
expect(getByText('-')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render size cell type when element type is asset', () => {
const { container, getByText } = setup({ cellType: 'size', content: '20.5435' });
expect(getByText('21KB')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render size cell type with "-" when element type is folder', () => {
const { container, getByText } = setup({ cellType: 'size', elementType: 'folder' });
expect(getByText('-')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render date cell type', () => {
const { container, getByText } = setup({
cellType: 'date',
content: '2022-11-18T12:08:02.202Z',
});
expect(getByText('11/18/2022')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
it('should render "-" by default when no recognized cell type is passed', () => {
const { container, getByText } = setup({ cellType: 'not recognized type' });
expect(getByText('-')).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1,75 @@
import React from 'react';
import { render } from '@testing-library/react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { PreviewCell } from '../PreviewCell';
const PROPS_FIXTURE = {
alternativeText: 'alternative alt',
fileExtension: 'jpeg',
mime: 'image/jpeg',
name: 'michka',
thumbnailURL: 'michka-picture-url-thumbnail.jpeg',
url: 'michka-picture-url-default.jpeg',
type: 'asset',
};
const ComponentFixture = (props) => {
const customProps = {
...PROPS_FIXTURE,
...props,
};
return (
<ThemeProvider theme={lightTheme}>
<PreviewCell {...PROPS_FIXTURE} {...customProps} />
</ThemeProvider>
);
};
const setup = (props) => render(<ComponentFixture {...props} />);
describe('TableList | PreviewCell', () => {
describe('rendering images', () => {
it('should render an image with thumbnail if available', () => {
const { getByRole } = setup();
expect(getByRole('img', { name: 'alternative alt' })).toHaveAttribute(
'src',
'michka-picture-url-thumbnail.jpeg'
);
});
it('should render an image with default url if thumbnail is not available', () => {
const { getByRole } = setup({ thumbnailURL: undefined });
expect(getByRole('img', { name: 'alternative alt' })).toHaveAttribute(
'src',
'michka-picture-url-default.jpeg'
);
});
it('should render alternative text if available', () => {
const { getByRole, queryByRole } = setup();
expect(getByRole('img', { name: 'alternative alt' })).toBeInTheDocument();
expect(queryByRole('img', { name: 'michka' })).not.toBeInTheDocument();
});
});
describe('rendering files', () => {
it('should render a file with fileExtension', () => {
const { getByText } = setup({ mime: 'application/pdf', fileExtension: 'pdf' });
expect(getByText('pdf')).toBeInTheDocument();
});
});
describe('rendering folder', () => {
it('should render a folder', () => {
const { container } = setup({ type: 'folder' });
expect(container.querySelector('path')).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,96 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import { render } from '@testing-library/react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { TableList } from '..';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useTracking: jest.fn(() => ({ trackUsage: jest.fn() })),
}));
const PROPS_FIXTURE = {
rows: [
{
alternativeText: 'alternative text',
createdAt: '2021-10-18T08:04:56.326Z',
ext: '.jpeg',
formats: {
thumbnail: {
url: '/uploads/thumbnail_3874873_b5818bb250.jpg',
},
},
id: 1,
mime: 'image/jpeg',
name: 'michka',
size: 11.79,
updatedAt: '2021-10-18T08:04:56.326Z',
url: '/uploads/michka.jpg',
type: 'asset',
},
],
onEditAsset: jest.fn(),
onSelectOne: jest.fn(),
onSelectAll: jest.fn(),
selected: [],
};
const ComponentFixture = (props) => {
const customProps = {
...PROPS_FIXTURE,
...props,
};
return (
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<TableList {...customProps} />
</ThemeProvider>
</IntlProvider>
);
};
const setup = (props) => render(<ComponentFixture {...props} />);
describe('TableList', () => {
it('should render table headers labels', () => {
const { getByText } = setup();
expect(getByText('preview')).toBeInTheDocument();
expect(getByText('name')).toBeInTheDocument();
expect(getByText('extension')).toBeInTheDocument();
expect(getByText('size')).toBeInTheDocument();
expect(getByText('created')).toBeInTheDocument();
expect(getByText('last update')).toBeInTheDocument();
});
it('should render a visually hidden edit table headers label', () => {
const { getByRole } = setup();
expect(getByRole('columnheader', { name: 'actions' })).toBeInTheDocument();
});
it('should render assets', () => {
const { getByText } = setup();
expect(getByText('michka')).toBeInTheDocument();
expect(getByText('JPEG')).toBeInTheDocument();
});
it('should render folders', () => {
const { getByText } = setup({
rows: [
{
createdAt: '2022-11-17T10:40:06.022Z',
id: 2,
name: 'folder 1',
type: 'folder',
updatedAt: '2022-11-17T10:40:06.022Z',
},
],
});
expect(getByText('folder 1')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,145 @@
import React from 'react';
import { IntlProvider } from 'react-intl';
import { render, fireEvent } from '@testing-library/react';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import { TableRows } from '../TableRows';
const PROPS_FIXTURE = {
rows: [
{
alternativeText: 'alternative text',
createdAt: '2021-10-01T08:04:56.326Z',
ext: '.jpeg',
formats: {
thumbnail: {
url: '/uploads/thumbnail_3874873_b5818bb250.jpg',
},
},
id: 1,
mime: 'image/jpeg',
name: 'michka',
size: 11.79,
updatedAt: '2021-10-18T08:04:56.326Z',
url: '/uploads/michka.jpg',
type: 'asset',
},
],
onEditAsset: jest.fn(),
onEditFolder: jest.fn(),
onSelectOne: jest.fn(),
selected: [],
};
const FOLDER_FIXTURE = {
createdAt: '2022-11-17T10:40:06.022Z',
id: 2,
name: 'folder 1',
type: 'folder',
updatedAt: '2022-11-17T10:40:06.022Z',
};
const ComponentFixture = (props) => {
const customProps = {
...PROPS_FIXTURE,
...props,
};
return (
<IntlProvider locale="en" messages={{}}>
<ThemeProvider theme={lightTheme}>
<table>
<TableRows {...customProps} />
</table>
</ThemeProvider>
</IntlProvider>
);
};
const setup = (props) => render(<ComponentFixture {...props} />);
describe('TableList | TableRows', () => {
describe('rendering assets', () => {
it('should properly render every asset attribute', () => {
const { getByRole, getByText } = setup();
expect(getByRole('img', { name: 'alternative text' })).toBeInTheDocument();
expect(getByText('michka')).toBeInTheDocument();
expect(getByText('JPEG')).toBeInTheDocument();
expect(getByText('12KB')).toBeInTheDocument();
expect(getByText('10/1/2021')).toBeInTheDocument();
expect(getByText('10/18/2021')).toBeInTheDocument();
expect(getByText('10/18/2021')).toBeInTheDocument();
});
it('should call onSelectAsset callback', () => {
const onSelectOneSpy = jest.fn();
const { getByRole } = setup({ onSelectOne: onSelectOneSpy });
fireEvent.click(getByRole('checkbox', { name: 'Select michka asset' }));
expect(onSelectOneSpy).toHaveBeenCalledTimes(1);
});
it('should reflect non selected assets state', () => {
const { getByRole } = setup();
expect(getByRole('checkbox', { name: 'Select michka asset' })).not.toBeChecked();
});
it('should reflect selected assets state', () => {
const { getByRole } = setup({ selected: [{ id: 1 }] });
expect(getByRole('checkbox', { name: 'Select michka asset' })).toBeChecked();
});
it('should call onEditAsset callback', () => {
const onEditAssetSpy = jest.fn();
const { getByRole } = setup({ onEditAsset: onEditAssetSpy });
fireEvent.click(getByRole('button', { name: 'Edit' }));
expect(onEditAssetSpy).toHaveBeenCalledTimes(1);
});
});
describe('rendering folders', () => {
it('should render folder', () => {
const { getByText } = setup({
rows: [FOLDER_FIXTURE],
});
expect(getByText('folder 1')).toBeInTheDocument();
});
it('should call onEditFolder callback', () => {
const onEditFolderSpy = jest.fn();
const { getByRole } = setup({
rows: [FOLDER_FIXTURE],
onEditFolder: onEditFolderSpy,
});
fireEvent.click(getByRole('button', { name: 'Edit' }));
expect(onEditFolderSpy).toHaveBeenCalledTimes(1);
});
it('should reflect non selected folder state', () => {
const { getByRole } = setup({ rows: [FOLDER_FIXTURE] });
expect(getByRole('checkbox', { name: 'Select folder 1 folder' })).not.toBeChecked();
});
it('should reflect selected folder state', () => {
const { getByRole } = setup({ rows: [FOLDER_FIXTURE], selected: [{ id: 2 }] });
expect(getByRole('checkbox', { name: 'Select folder 1 folder' })).toBeChecked();
});
it('should not display size and ext', () => {
const { getAllByText } = setup({ rows: [FOLDER_FIXTURE] });
expect(getAllByText('-').length).toEqual(2);
});
});
});

View File

@ -0,0 +1,594 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TableList | CellContent should render "-" by default when no recognized cell type is passed 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
-
</span>
<div
class="c1"
>
<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>
`;
exports[`TableList | CellContent should render date cell type 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
11/18/2022
</span>
<div
class="c1"
>
<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>
`;
exports[`TableList | CellContent should render extension cell type when element type is asset 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
PDF
</span>
<div
class="c1"
>
<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>
`;
exports[`TableList | CellContent should render extension cell type with "-" when element type is folder 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
-
</span>
<div
class="c1"
>
<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>
`;
exports[`TableList | CellContent should render image cell type when element type is asset and mime does not include image 1`] = `
.c4 {
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;
}
.c0 {
background: #eaf5ff;
border-radius: 50%;
width: 1.625rem;
height: 1.625rem;
}
.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;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.c3 {
font-weight: 600;
font-size: 0.6875rem;
line-height: 1.45;
text-transform: uppercase;
color: #0c75af;
}
.c2 span {
font-size: 0.625rem;
}
<div>
<div
class="c0 c1 c2"
height="1.625rem"
width="1.625rem"
>
<span
class="c3"
>
pdf
</span>
</div>
<div
class="c4"
>
<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>
`;
exports[`TableList | CellContent should render image cell type when element type is asset and mime includes image 1`] = `
.c2 {
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;
}
.c1 {
border-radius: 50%;
object-fit: cover;
display: block;
position: relative;
}
.c0 {
position: relative;
width: 1.625rem;
height: 1.625rem;
}
<div>
<span>
<div
class="c0"
>
<img
alt="alternative alt"
class="c1"
height="26px"
src="michka-picture-url-thumbnail.jpeg"
width="26px"
/>
</div>
</span>
<div
class="c2"
>
<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>
`;
exports[`TableList | CellContent should render image cell type when element type is folder 1`] = `
.c4 {
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;
}
.c0 {
background: #eaf5ff;
border-radius: 50%;
width: 1.625rem;
height: 1.625rem;
}
.c2 {
color: #66b7f1;
}
.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;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.c3 path {
fill: #66b7f1;
}
<div>
<div
class="c0 c1"
height="1.625rem"
width="1.625rem"
>
<svg
class="c2 c3"
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.414 5H21a1 1 0 011 1v14a1 1 0 01-1 1H3a1 1 0 01-1-1V4a1 1 0 011-1h7.414l2 2z"
fill="#212134"
/>
</svg>
</div>
<div
class="c4"
>
<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>
`;
exports[`TableList | CellContent should render size cell type when element type is asset 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
21KB
</span>
<div
class="c1"
>
<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>
`;
exports[`TableList | CellContent should render size cell type with "-" when element type is folder 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
-
</span>
<div
class="c1"
>
<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>
`;
exports[`TableList | CellContent should render text cell type 1`] = `
.c1 {
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;
}
.c0 {
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
<div>
<span
class="c0"
>
some text
</span>
<div
class="c1"
>
<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

@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
import { getTrad } from './utils';
export const AssetType = {
Video: 'video',
@ -100,3 +101,70 @@ export const viewOptions = {
GRID: 0,
LIST: 1,
};
export const tableHeaders = [
{
name: 'preview',
key: 'preview',
metadatas: {
label: { id: getTrad('list-table-header-preview'), defaultMessage: 'preview' },
sortable: false,
},
type: 'image',
},
{
name: 'name',
key: 'name',
metadatas: {
label: { id: getTrad('list-table-header-name'), defaultMessage: 'name' },
sortable: true,
},
type: 'text',
},
{
name: 'ext',
key: 'extension',
metadatas: {
label: { id: getTrad('list-table-header-ext'), defaultMessage: 'extension' },
sortable: false,
},
type: 'ext',
},
{
name: 'size',
key: 'size',
metadatas: {
label: { id: getTrad('list-table-header-size'), defaultMessage: 'size' },
sortable: false,
},
type: 'size',
},
{
name: 'createdAt',
key: 'createdAt',
metadatas: {
label: { id: getTrad('list-table-header-createdAt'), defaultMessage: 'created' },
sortable: true,
},
type: 'date',
},
{
name: 'updatedAt',
key: 'updatedAt',
metadatas: {
label: { id: getTrad('list-table-header-updatedAt'), defaultMessage: 'last update' },
sortable: true,
},
type: 'date',
},
];
export const pageSizes = [10, 20, 50, 100];
export const sortOptions = [
{ key: 'sort.created_at_desc', value: 'createdAt:DESC' },
{ key: 'sort.created_at_asc', value: 'createdAt:ASC' },
{ key: 'sort.name_asc', value: 'name:ASC' },
{ key: 'sort.name_desc', value: 'name:DESC' },
{ key: 'sort.updated_at_desc', value: 'updatedAt:DESC' },
{ key: 'sort.updated_at_asc', value: 'updatedAt:ASC' },
];

View File

@ -31,7 +31,8 @@ import Grid from '@strapi/icons/Grid';
import { UploadAssetDialog } from '../../components/UploadAssetDialog/UploadAssetDialog';
import { EditFolderDialog } from '../../components/EditFolderDialog';
import { EditAssetDialog } from '../../components/EditAssetDialog';
import { AssetList } from '../../components/AssetList';
import { AssetGridList } from '../../components/AssetGridList';
import { TableList } from '../../components/TableList';
import { FolderList } from '../../components/FolderList';
import SortPicker from '../../components/SortPicker';
import { useAssets } from '../../hooks/useAssets';
@ -102,7 +103,7 @@ export const MediaLibrary = () => {
});
const {
data: folders,
data: foldersData,
isLoading: foldersLoading,
errors: foldersError,
} = useFolders({
@ -123,15 +124,19 @@ export const MediaLibrary = () => {
push(pathname);
}
const folders = foldersData?.map((folder) => ({ ...folder, type: 'folder' })) ?? [];
const folderCount = folders?.length || 0;
const assets = assetsData?.results;
const assets = assetsData?.results?.map((asset) => ({ ...asset, type: 'asset' })) || [];
const assetCount = assets?.length ?? 0;
const isLoading = isCurrentFolderLoading || foldersLoading || permissionsLoading || assetsLoading;
const [showUploadAssetDialog, setShowUploadAssetDialog] = useState(false);
const [showEditFolderDialog, setShowEditFolderDialog] = useState(false);
const [assetToEdit, setAssetToEdit] = useState(undefined);
const [folderToEdit, setFolderToEdit] = useState(undefined);
const [selected, { selectOne, selectAll }] = useSelectionState(['type', 'id'], []);
const indeterminateBulkSelect =
selected?.length > 0 && selected?.length !== assetCount + folderCount;
const toggleUploadAssetDialog = () => setShowUploadAssetDialog((prev) => !prev);
const toggleEditFolderDialog = ({ created = false } = {}) => {
// folders are only displayed on the first page, therefore
@ -147,6 +152,14 @@ export const MediaLibrary = () => {
setShowEditFolderDialog((prev) => !prev);
};
const handleBulkSelect = (event, elements) => {
if (event.target.checked) {
trackUsage('didSelectAllMediaLibraryElements');
}
selectAll(elements);
};
const handleChangeSort = (value) => {
trackUsage('didSortMediaLibraryElements', {
location: 'upload',
@ -186,7 +199,7 @@ export const MediaLibrary = () => {
<ActionLayout
startActions={
<>
{canUpdate && (assetCount > 0 || folderCount > 0) && (
{canUpdate && isGridView && (assetCount > 0 || folderCount > 0) && (
<BoxWithHeight
paddingLeft={2}
paddingRight={2}
@ -199,26 +212,16 @@ export const MediaLibrary = () => {
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all folders & assets',
})}
indeterminate={
selected?.length > 0 && selected?.length !== assetCount + folderCount
}
indeterminate={indeterminateBulkSelect}
value={
(assetCount > 0 || folderCount > 0) &&
selected.length === assetCount + folderCount
}
onChange={(e) => {
if (e.target.checked) {
trackUsage('didSelectAllMediaLibraryElements');
}
selectAll([
...assets.map((asset) => ({ ...asset, type: 'asset' })),
...folders.map((folder) => ({ ...folder, type: 'folder' })),
]);
}}
onChange={(e) => handleBulkSelect(e, [...assets, ...folders])}
/>
</BoxWithHeight>
)}
{canRead && <SortPicker onChangeSort={handleChangeSort} />}
{canRead && isGridView && <SortPicker onChangeSort={handleChangeSort} />}
{canRead && <Filters />}
</>
}
@ -272,11 +275,34 @@ export const MediaLibrary = () => {
/>
)}
{canRead && (
{/* TODO: fix AssetListTable should handle no assets views (loading) */}
{canRead && !isGridView && (assetCount > 0 || folderCount > 0) && (
<TableList
assetCount={assetCount}
folderCount={folderCount}
indeterminate={indeterminateBulkSelect}
onEditAsset={setAssetToEdit}
onEditFolder={handleEditFolder}
onSelectOne={selectOne}
onSelectAll={handleBulkSelect}
rows={
// TODO: remove when fixed on DS side
// when number of rows in Table changes, the keyboard tab from a row to another
// is not working for 1st and last column
!assetsLoading && !foldersLoading ? [...folders, ...assets] : []
}
selected={selected}
/>
)}
{canRead && isGridView && (
<>
{folderCount > 0 && (
<FolderList
title={
// Folders title should only appear if:
// user is filtering and there are assets to display, to divide both type of elements
// user is not filtering
(((isFiltering && assetCount > 0) || !isFiltering) &&
formatMessage(
{
@ -312,7 +338,7 @@ export const MediaLibrary = () => {
<FolderCardCheckbox
data-testid={`folder-checkbox-${folder.id}`}
value={isSelected}
onChange={() => selectOne({ ...folder, type: 'folder' })}
onChange={() => selectOne(folder)}
/>
)
}
@ -370,33 +396,33 @@ export const MediaLibrary = () => {
)}
{assetCount > 0 && (
<>
<AssetList
assets={assets}
onEditAsset={setAssetToEdit}
onSelectAsset={selectOne}
selectedAssets={selected.filter(({ type }) => type === 'asset')}
title={
((!isFiltering || (isFiltering && folderCount > 0)) &&
assetsData?.pagination?.page === 1 &&
formatMessage(
{
id: getTrad('list.assets.title'),
defaultMessage: 'Assets ({count})',
},
{ count: assetCount }
)) ||
''
}
/>
{assetsData?.pagination && (
<PaginationFooter pagination={assetsData.pagination} />
)}
</>
<AssetGridList
assets={assets}
onEditAsset={setAssetToEdit}
onSelectAsset={selectOne}
selectedAssets={selected.filter(({ type }) => type === 'asset')}
title={
// Assets title should only appear if:
// - user is not filtering
// - user is filtering and there are folders to display, to separate them
// - user is on page 1 since folders won't appear on any other page than the first one (no need to visually separate them)
((!isFiltering || (isFiltering && folderCount > 0)) &&
assetsData?.pagination?.page === 1 &&
formatMessage(
{
id: getTrad('list.assets.title'),
defaultMessage: 'Assets ({count})',
},
{ count: assetCount }
)) ||
''
}
/>
)}
</>
)}
{assetsData?.pagination && <PaginationFooter pagination={assetsData.pagination} />}
</ContentLayout>
</Main>

View File

@ -38,7 +38,7 @@
"input.placeholder.icon": "Drop the asset in this zone",
"input.url.description": "Separate your URL links by a carriage return.",
"input.url.label": "URL",
"list.assets.title": "Assets",
"list.assets.title": "Assets ({count})",
"list.asset.at.finished": "The assets have finished loading.",
"list.assets-empty.search": "No result found",
"list.assets-empty.subtitle": "Add one to the list.",
@ -51,11 +51,13 @@
"list.assets.not-supported-content": "No preview available",
"list.assets.preview-asset": "Preview for the video at path {path}",
"list.assets.selected": "{numberFolders, plural, one {1 folder} other {# folders}} - {numberAssets, plural, one {1 asset} other {# assets}}",
"list-assets-select": "Select {name} asset",
"list.assets.type-not-allowed": "This type of file is not allowed.",
"list.assets.to-upload": "{number, plural, =0 {No asset} one {1 asset} other {# assets}} ready to upload",
"list.folder.edit": "Edit folder",
"list.folder.select": "Select {name} folder",
"list.folder.subtitle": "{folderCount, plural, =0 {# folder} one {# folder} other {# folders}}, {filesCount, plural, =0 {# asset} one {# asset} other {# assets}}",
"list.folders.title": "Folders",
"list.folders.title": "Folders ({count})",
"mediaLibraryInput.actions.nextSlide": "Next slide",
"mediaLibraryInput.actions.previousSlide": "Previous slide",
"mediaLibraryInput.placeholder": "Click to add an asset or drag and drop one in this area",
@ -112,6 +114,13 @@
"sort.name_desc": "Reverse alphabetical order (Z to A)",
"sort.updated_at_asc": "Oldest updates",
"sort.updated_at_desc": "Most recent updates",
"list-table-header-actions": "actions",
"list-table-header-preview": "preview",
"list-table-header-name": "name",
"list-table-header-ext": "extension",
"list-table-header-size": "size",
"list-table-header-createdAt": "Created",
"list-table-header-updatedAt": "Last update",
"tabs.title": "How do you want to upload your assets?",
"window.confirm.close-modal.file": "Are you sure? Your changes will be lost.",
"window.confirm.close-modal.files": "Are you sure? You have some files that have not been uploaded yet.",