feat: update list view to be table (#628)

This commit is contained in:
Kristin Aoki
2023-10-16 14:58:28 -04:00
committed by GitHub
parent 7132136a91
commit 5c101b09d4
8 changed files with 371 additions and 62 deletions

View File

@@ -28,7 +28,7 @@ import {
updateAssetOrder,
fetchAssetDownload,
} from './data/thunks';
import { sortFiles } from './data/utils';
import { getFileSizeToClosestByte, sortFiles } from './data/utils';
import messages from './messages';
import FileInfo from './FileInfo';
@@ -36,9 +36,9 @@ import FileInput, { useFileInput } from './FileInput';
import FilesAndUploadsProvider from './FilesAndUploadsProvider';
import {
GalleryCard,
ListCard,
TableActions,
} from './table-components';
import { AccessColumn, MoreInfoColumn, ThumbnailColumn } from './table-components/table-custom-columns';
import ApiStatusToast from './ApiStatusToast';
import { clearErrors } from './data/slice';
import getPageHeadTitle from '../generic/utils';
@@ -141,34 +141,83 @@ const FilesAndUploads = ({
/>
);
const fileCard = ({ className, original }) => {
if (currentView === defaultVal) {
return (
<GalleryCard
{...{
handleLockedAsset,
handleBulkDownload,
handleOpenDeleteConfirmation,
handleOpenAssetInfo,
className,
original,
}}
/>
);
}
return (
<ListCard
{...{
handleLockedAsset,
handleBulkDownload,
handleOpenDeleteConfirmation,
handleOpenAssetInfo,
className,
original,
}}
/>
);
const fileCard = ({ className, original }) => (
<GalleryCard
{...{
handleLockedAsset,
handleBulkDownload,
handleOpenDeleteConfirmation,
handleOpenAssetInfo,
className,
original,
}}
/>
);
const accessColumn = {
id: 'locked',
Header: 'Access',
Cell: ({ row }) => AccessColumn({ row }),
};
const thumbnailColumn = {
id: 'thumbnail',
Header: '',
Cell: ({ row }) => ThumbnailColumn({ row }),
};
const fileSizeColumn = {
id: 'fileSize',
Header: 'File size',
Cell: ({ row }) => {
const { fileSize } = row.original;
return getFileSizeToClosestByte(fileSize);
},
};
const moreInfoColumn = {
id: 'moreInfo',
Header: '',
Cell: ({ row }) => MoreInfoColumn({
row,
handleLock: handleLockedAsset,
handleBulkDownload,
handleOpenAssetInfo,
handleOpenDeleteConfirmation,
}),
};
const tableColumns = [
{ ...thumbnailColumn },
{
Header: 'File name',
accessor: 'displayName',
},
{ ...fileSizeColumn },
{
Header: 'Type',
accessor: 'wrapperType',
Filter: CheckboxFilter,
filter: 'includesValue',
filterChoices: [
{
name: 'Code',
value: 'code',
},
{
name: 'Images',
value: 'image',
},
{
name: 'Documents',
value: 'document',
},
{
name: 'Audio',
value: 'audio',
},
],
},
{ ...accessColumn },
{ ...moreInfoColumn },
];
if (loadingStatus === RequestStatus.DENIED) {
return (
@@ -244,36 +293,7 @@ const FilesAndUploads = ({
}}
tableActions={headerActions}
bulkActions={headerActions}
columns={[
{
Header: 'Name',
accessor: 'displayName',
},
{
Header: 'Type',
accessor: 'wrapperType',
Filter: CheckboxFilter,
filter: 'includesValue',
filterChoices: [
{
name: 'Code',
value: 'code',
},
{
name: 'Images',
value: 'image',
},
{
name: 'Documents',
value: 'document',
},
{
name: 'Audio',
value: 'audio',
},
],
},
]}
columns={tableColumns}
itemCount={totalCount}
pageCount={Math.ceil(totalCount / 50)}
data={assets}
@@ -292,7 +312,7 @@ const FilesAndUploads = ({
<div data-testid="files-data-table" className="mr-4 ml-3">
<DataTable.TableControlBar />
{ currentView === 'card' && <CardView CardComponent={fileCard} columnSizes={columnSizes} selectionPlacement="left" skeletonCardCount={6} /> }
{ currentView === 'list' && <CardView CardComponent={fileCard} columnSizes={{ xs: 12 }} selectionPlacement="left" skeletonCardCount={4} /> }
{ currentView === 'list' && <DataTable.Table /> }
<DataTable.EmptyTable content={intl.formatMessage(messages.noResultsFoundMessage)} />
<DataTable.TableFooter />
<ApiStatusToast

View File

@@ -168,7 +168,7 @@ describe('FilesAndUploads', () => {
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
expect(screen.queryByTestId('list-card-mOckID1')).toBeNull();
expect(screen.queryByRole('table')).toBeNull();
const listButton = screen.getByLabelText('List');
await act(async () => {
@@ -176,7 +176,7 @@ describe('FilesAndUploads', () => {
});
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
expect(screen.getByTestId('list-card-mOckID1')).toBeVisible();
expect(screen.getByRole('table')).toBeVisible();
});
});

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { PropTypes } from 'prop-types';
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import { Locked, LockOpen } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../../messages';
const AccessColumn = ({
row,
// injected
intl,
}) => {
const { locked } = row.original;
return (
<OverlayTrigger
placement="top"
overlay={(
<Tooltip id="access-tooltip-description">
{intl.formatMessage(messages.lockFileTooltipContent)}
</Tooltip>
)}
>
{locked ? (
<Icon src={Locked} size="sm" />
) : (
<Icon src={LockOpen} size="sm" />
)}
</OverlayTrigger>
);
};
AccessColumn.propTypes = {
row: {
original: {
locked: PropTypes.bool.isRequired,
}.isRequired,
}.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(AccessColumn);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { PropTypes } from 'prop-types';
import { Icon } from '@edx/paragon';
import { Check } from '@edx/paragon/icons';
const ActiveColumn = ({ row }) => {
const { usageLocations } = row.original;
const numOfUsageLocations = usageLocations.length;
return numOfUsageLocations > 0 ? <Icon src={Check} /> : null;
};
ActiveColumn.propTypes = {
row: {
original: {
usageLocations: PropTypes.arrayOf(PropTypes.string).isRequired,
}.isRequired,
}.isRequired,
};
export default ActiveColumn;

View File

@@ -0,0 +1,155 @@
import React, { useState } from 'react';
import { PropTypes } from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button,
Icon,
IconButton,
ModalPopup,
Menu,
MenuItem,
useToggle,
} from '@edx/paragon';
import { MoreHoriz } from '@edx/paragon/icons';
import messages from '../../messages';
const MoreInfoColumn = ({
row,
handleLock,
handleBulkDownload,
handleOpenAssetInfo,
handleOpenDeleteConfirmation,
// injected
intl,
}) => {
const [isOpen, , close, toggle] = useToggle();
const [target, setTarget] = useState(null);
const {
externalUrl,
locked,
portableUrl,
id,
wrapperType,
displayName,
} = row.original;
return (
<>
<IconButton src={MoreHoriz} iconAs={Icon} onClick={toggle} ref={setTarget} />
<ModalPopup
placement="left"
positionRef={target}
isOpen={isOpen}
onClose={close}
onEscapeKey={close}
style={{
}}
>
<Menu
className="border border-light-400"
style={{ overflowX: 'hidden', boxShadow: '0px 0px 0px #000', maxHeight: '500px' }}
>
{wrapperType === 'video' ? (
<MenuItem
as={Button}
variant="tertiary"
size="inline"
onClick={() => {
navigator.clipboard.writeText(id);
close();
}}
>
Copy video ID
</MenuItem>
) : (
<>
<MenuItem
as={Button}
variant="tertiary"
size="inline"
onClick={() => {
navigator.clipboard.writeText(portableUrl);
close();
}}
>
{intl.formatMessage(messages.copyStudioUrlTitle)}
</MenuItem>
<MenuItem
as={Button}
variant="tertiary"
size="inline"
onClick={() => {
navigator.clipboard.writeText(externalUrl);
close();
}}
>
{intl.formatMessage(messages.copyWebUrlTitle)}
</MenuItem>
<MenuItem
as={Button}
variant="tertiary"
size="inline"
onClick={() => handleLock(id, !locked)}
>
{locked ? intl.formatMessage(messages.unlockMenuTitle) : intl.formatMessage(messages.lockMenuTitle)}
</MenuItem>
</>
)}
<MenuItem
as={Button}
variant="tertiary"
size="inline"
onClick={() => handleBulkDownload(
[{ original: { id, displayName } }],
)}
>
{intl.formatMessage(messages.downloadTitle)}
</MenuItem>
<MenuItem
as={Button}
variant="tertiary"
size="inline"
onClick={() => handleOpenAssetInfo(row.original)}
>
{intl.formatMessage(messages.infoTitle)}
</MenuItem>
<hr className="m-0" />
<MenuItem
as={Button}
variant="tertiary"
size="inline"
data-testid="open-delete-confirmation-button"
onClick={() => {
handleOpenDeleteConfirmation([{ original: row.original }]);
close();
}}
>
{intl.formatMessage(messages.deleteTitle)}
</MenuItem>
</Menu>
</ModalPopup>
</>
);
};
MoreInfoColumn.propTypes = {
row: {
original: {
externalUrl: PropTypes.string,
locked: PropTypes.bool,
portableUrl: PropTypes.string,
id: PropTypes.string.isRequired,
wrapperType: PropTypes.string,
}.isRequired,
}.isRequired,
handleLock: PropTypes.func.isRequired,
handleBulkDownload: PropTypes.func.isRequired,
handleOpenAssetInfo: PropTypes.func.isRequired,
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(MoreInfoColumn);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { PropTypes } from 'prop-types';
import { Badge } from '@edx/paragon';
const StatusColumn = ({ row }) => {
const { status } = row.original;
return (
<Badge variant="light">
{status}
</Badge>
);
};
StatusColumn.propTypes = {
row: {
original: {
status: PropTypes.string.isRequired,
}.isRequired,
}.isRequired,
};
export default StatusColumn;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { PropTypes } from 'prop-types';
import { Image, Icon } from '@edx/paragon';
import { getSrc } from '../../data/utils';
const ThumbnailColumn = ({ row }) => {
const {
thumbnail,
wrapperType,
externalUrl,
} = row.original;
const src = getSrc({ thumbnail, wrapperType, externalUrl });
return (
<div className="row align-items-center justify-content-center m-0 p-3">
{thumbnail ? (
<Image src={src} style={{ height: '76px', width: '135.71px' }} className="border rounded p-1" />
) : (
<div className="row border justify-content-center align-items-center rounded m-0" style={{ height: '76px', width: '135.71px' }}>
<Icon src={src} style={{ height: '48px', width: '48px' }} />
</div>
)}
</div>
);
};
ThumbnailColumn.propTypes = {
row: {
original: {
thumbnail: PropTypes.string,
wrapperType: PropTypes.string.isRequired,
externalUrl: PropTypes.string,
}.isRequired,
}.isRequired,
};
export default ThumbnailColumn;

View File

@@ -0,0 +1,13 @@
import AccessColumn from './AccessColumn';
import ActiveColumn from './ActiveColumn';
import MoreInfoColumn from './MoreInfoColumn';
import StatusColumn from './StatusColumn';
import ThumbnailColumn from './ThumbnailColumn';
export {
AccessColumn,
ActiveColumn,
MoreInfoColumn,
StatusColumn,
ThumbnailColumn,
};