feat: add sort function and modal (#577)

* feat: add sort modal and function

* fix: dateAdded typo

* chore: update mock api data
This commit is contained in:
Kristin Aoki
2023-08-25 10:08:15 -04:00
committed by GitHub
parent 1d95af5a31
commit 181f9c7a5f
8 changed files with 320 additions and 74 deletions

View File

@@ -17,7 +17,6 @@ import {
CheckboxControl,
} from '@edx/paragon';
import { ContentCopy, InfoOutline } from '@edx/paragon/icons';
import { getUtcDateTime } from './data/utils';
import AssetThumbnail from './FileThumbnail';
import messages from './messages';
@@ -36,8 +35,6 @@ const FileInfo = ({
setLockedState(locked);
handleLockedAsset(asset.id, locked);
};
const dateAdded = getUtcDateTime(asset.dateAdded);
return (
<ModalDialog
title={asset.displayName}
@@ -71,7 +68,7 @@ const FileInfo = ({
<FormattedMessage {...messages.dateAddedTitle} />
</div>
<FormattedDate
value={dateAdded}
value={asset.dateAdded}
year="numeric"
month="short"
day="2-digit"

View File

@@ -23,7 +23,9 @@ import {
deleteAssetFile,
fetchAssets,
updateAssetLock,
updateAssetOrder,
} from './data/thunks';
import { sortFiles } from './data/utils';
import messages from './messages';
import FileInput, { fileInput } from './FileInput';
@@ -72,7 +74,6 @@ const FilesAndUploads = ({
setAddOpen,
});
const assets = useModels('assets', assetIds);
const handleDropzoneAsset = ({ fileData, handleError }) => {
try {
const file = fileData.get('file');
@@ -82,6 +83,11 @@ const FilesAndUploads = ({
}
};
const handleSort = (sortType) => {
const newAssetIdOrder = sortFiles(assets, sortType);
dispatch(updateAssetOrder(courseId, newAssetIdOrder, sortType));
};
const handleBulkDelete = () => {
closeDeleteConfirmation();
setDeleteOpen();
@@ -117,6 +123,7 @@ const FilesAndUploads = ({
{...{
selectedFlatRows,
fileInputControl,
handleSort,
handleBulkDownload,
handleOpenDeleteConfirmation,
}}
@@ -229,6 +236,22 @@ const FilesAndUploads = ({
},
],
},
{
Header: 'Locked',
accessor: 'locked',
// Filter: CheckboxFilter,
// filter: 'exactText',
// filterChoices: [
// {
// name: 'Locked',
// value: true,
// },
// {
// name: 'Unlocked',
// value: false,
// },
// ],
},
]}
itemCount={totalCount}
pageCount={Math.ceil(totalCount / 50)}

View File

@@ -205,6 +205,34 @@ describe('FilesAndUploads', () => {
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
});
it('sort button should be enabled and sort files by name', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
expect(sortsButton).toBeVisible();
await waitFor(() => {
fireEvent.click(sortsButton);
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
});
const sortNameAscendingButton = screen.getByText(messages.sortByNameAscending.defaultMessage);
fireEvent.click(sortNameAscendingButton);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
});
it('sort button should be enabled and sort files by file size', async () => {
renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const sortsButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
expect(sortsButton).toBeVisible();
await waitFor(() => {
fireEvent.click(sortsButton);
expect(screen.getByText(messages.sortModalTitleLabel.defaultMessage)).toBeVisible();
});
const sortBySizeDescendingButton = screen.getByText(messages.sortBySizeDescending.defaultMessage);
fireEvent.click(sortBySizeDescendingButton);
fireEvent.click(screen.getByText(messages.applySortButton.defaultMessage));
expect(screen.queryByText(messages.sortModalTitleLabel.defaultMessage)).toBeNull();
});
});
describe('card menu actions', () => {
it('should open asset info', async () => {

View File

@@ -23,7 +23,7 @@ import {
updateErrors,
} from './slice';
import { getWrapperType } from './utils';
import { updateFileValues } from './utils';
export function fetchAssets(courseId) {
return async (dispatch) => {
@@ -32,8 +32,8 @@ export function fetchAssets(courseId) {
try {
const { totalCount } = await getAssets(courseId);
const { assets } = await getAssets(courseId, totalCount);
const assetsWithWraperType = getWrapperType(assets);
dispatch(addModels({ modelType: 'assets', models: assetsWithWraperType }));
const parsedAssests = updateFileValues(assets);
dispatch(addModels({ modelType: 'assets', models: parsedAssests }));
dispatch(setAssetIds({
assetIds: assets.map(asset => asset.id),
}));
@@ -49,6 +49,14 @@ export function fetchAssets(courseId) {
};
}
export function updateAssetOrder(courseId, assetIds) {
return async (dispatch) => {
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
dispatch(setAssetIds({ assetIds }));
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
};
}
export function deleteAssetFile(courseId, id, totalCount) {
return async (dispatch) => {
dispatch(updateDeletingStatus({ status: RequestStatus.IN_PROGRESS }));
@@ -72,10 +80,10 @@ export function addAssetFile(courseId, file, totalCount) {
try {
const { asset } = await addAsset(courseId, file);
const [assetsWithWraperType] = getWrapperType([asset]);
const [parsedAssest] = updateFileValues([asset]);
dispatch(addModel({
modelType: 'assets',
model: { ...assetsWithWraperType },
model: { ...parsedAssest },
}));
dispatch(addAssetSuccess({
assetId: asset.id,

View File

@@ -6,22 +6,28 @@ ensureConfig([
'STUDIO_BASE_URL',
], 'Course Apps API service');
export const getWrapperType = (assets) => {
const assetsWithWraperType = [];
assets.forEach(asset => {
if (FILES_AND_UPLOAD_TYPE_FILTERS.images.includes(asset.contentType)) {
assetsWithWraperType.push({ wrapperType: 'image', ...asset });
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.documents.includes(asset.contentType)) {
assetsWithWraperType.push({ wrapperType: 'document', ...asset });
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.code.includes(asset.contentType)) {
assetsWithWraperType.push({ wrapperType: 'code', ...asset });
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.audio.includes(asset.contentType)) {
assetsWithWraperType.push({ wrapperType: 'audio', ...asset });
} else {
assetsWithWraperType.push({ wrapperType: 'other', ...asset });
export const updateFileValues = (files) => {
const updatedFiles = [];
files.forEach(file => {
let wrapperType = 'other';
if (FILES_AND_UPLOAD_TYPE_FILTERS.images.includes(file.contentType)) {
wrapperType = 'image';
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.documents.includes(file.contentType)) {
wrapperType = 'document';
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.code.includes(file.contentType)) {
wrapperType = 'code';
} else if (FILES_AND_UPLOAD_TYPE_FILTERS.audio.includes(file.contentType)) {
wrapperType = 'audio';
}
const { dateAdded } = file;
const utcDateString = dateAdded.replace(/\bat\b/g, '');
const utcDateTime = new Date(utcDateString).toString();
updatedFiles.push({ ...file, wrapperType, dateAdded: utcDateTime });
});
return assetsWithWraperType;
return updatedFiles;
};
export const getSrc = ({ thumbnail, wrapperType, externalUrl }) => {
@@ -40,8 +46,35 @@ export const getSrc = ({ thumbnail, wrapperType, externalUrl }) => {
}
};
export const getUtcDateTime = (date) => {
const utcDateString = date.replace(/\bat\b/g, '');
const utcDateTime = new Date(utcDateString);
return utcDateTime;
export const sortFiles = (files, sortType) => {
const [sort, direction] = sortType.split(',');
let sortedFiles;
if (sort === 'displayName') {
sortedFiles = files.sort((f1, f2) => {
const lowerCaseF1 = f1[sort].toLowerCase();
const lowerCaseF2 = f2[sort].toLowerCase();
if (lowerCaseF1 < lowerCaseF2) {
return 1;
}
if (lowerCaseF1 > lowerCaseF2) {
return -1;
}
return 0;
});
} else {
sortedFiles = files.sort((f1, f2) => {
if (f1[sort] < f2[sort]) {
return 1;
}
if (f1[sort] > f2[sort]) {
return -1;
}
return 0;
});
}
const sortedIds = sortedFiles.map(file => file.id);
if (direction === 'asc') {
return sortedIds.reverse();
}
return sortedIds;
};

View File

@@ -48,6 +48,16 @@ export const generateFetchAssetApiResponse = () => ({
dateAdded: '',
thumbnail: '/asset',
},
{
id: 'mOckID5',
displayName: 'mOckID5',
locked: false,
externalUrl: 'static_tab_1',
portableUrl: '',
contentType: 'application/json',
dateAdded: 'Aug 13, 2023 at 22:08 UTC',
thumbnail: null,
},
{
id: 'mOckID3',
displayName: 'mOckID3',
@@ -55,7 +65,7 @@ export const generateFetchAssetApiResponse = () => ({
externalUrl: 'static_tab_1',
portableUrl: '',
contentType: 'application/pdf',
dateAdded: '',
dateAdded: 'Aug 17, 2023 at 22:08 UTC',
thumbnail: null,
},
{
@@ -68,16 +78,6 @@ export const generateFetchAssetApiResponse = () => ({
dateAdded: '',
thumbnail: null,
},
{
id: 'mOckID5',
displayName: 'mOckID5',
locked: false,
externalUrl: 'static_tab_1',
portableUrl: '',
contentType: 'application/json',
dateAdded: '',
thumbnail: null,
},
{
id: 'mOckID6',
displayName: 'mOckID6',
@@ -88,6 +88,16 @@ export const generateFetchAssetApiResponse = () => ({
dateAdded: '',
thumbnail: null,
},
{
id: 'mOckID6-2',
displayName: 'mOckID6',
locked: false,
externalUrl: 'static_tab_1',
portableUrl: 'May 17, 2023 at 22:08 UTC',
contentType: 'application/octet-stream',
dateAdded: '',
thumbnail: null,
},
],
totalCount: 50,
});

View File

@@ -114,9 +114,45 @@ const messages = defineMessages({
defaultMessage: 'Delete',
},
cancelButtonLabel: {
id: 'course-authoring.files-and-uploads.deleteConfirmation.cancelButton.label',
id: 'course-authoring.files-and-uploads.cancelButton.label',
defaultMessage: 'Cancel',
},
sortButtonLabel: {
id: 'course-authoring.files-and-uploads.sortButton.label',
defaultMessage: 'Sort',
},
sortModalTitleLabel: {
id: 'course-authoring.files-and-uploads.sortModal.title',
defaultMessage: 'Sort by',
},
sortByNameAscending: {
id: 'course-authoring.files-and-uploads.sortByNameAscendingButton.label',
defaultMessage: 'Name (A-Z)',
},
sortByNewest: {
id: 'course-authoring.files-and-uploads.sortByNewestButton.label',
defaultMessage: 'Newest',
},
sortBySizeDescending: {
id: 'course-authoring.files-and-uploads.sortBySizeDescendingButton.label',
defaultMessage: 'File size (High to low)',
},
sortByNameDescending: {
id: 'course-authoring.files-and-uploads.sortByNameDescendingButton.label',
defaultMessage: 'Name (Z-A)',
},
sortByOldest: {
id: 'course-authoring.files-and-uploads.sortByOldestButton.label',
defaultMessage: 'Oldest',
},
sortBySizeAscending: {
id: 'course-authoring.files-and-uploads.sortBySizeAscendingButton.label',
defaultMessage: 'File size(Low to high)',
},
applySortButton: {
id: 'course-authoring.files-and-uploads.applyySortButton.label',
defaultMessage: 'Apply',
},
});
export default messages;

View File

@@ -1,48 +1,156 @@
import React from 'react';
import React, { useState } from 'react';
import _ from 'lodash';
import { PropTypes } from 'prop-types';
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, Dropdown } from '@edx/paragon';
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
Dropdown,
ModalDialog,
SelectableBox,
useToggle,
} from '@edx/paragon';
import { Add } from '@edx/paragon/icons';
import messages from '../messages';
const TableActions = ({
selectedFlatRows,
fileInputControl,
handleSort,
handleBulkDownload,
handleOpenDeleteConfirmation,
}) => (
<>
<Dropdown>
<Dropdown.Toggle
id="actions-menu-toggle"
alt="actions-menu-toggle"
variant="outline-primary"
// injected
intl,
}) => {
const [isSortOpen, openSort, closeSort] = useToggle(false);
const [sortBy, setSortBy] = useState('dateAdded,desc');
const handleChange = (e) => {
setSortBy(e.target.value);
};
return (
<>
<Button variant="outline-primary" onClick={openSort}>
<FormattedMessage {...messages.sortButtonLabel} />
</Button>
<Dropdown className="mx-2">
<Dropdown.Toggle
id="actions-menu-toggle"
alt="actions-menu-toggle"
variant="outline-primary"
>
<FormattedMessage {...messages.actionsButtonLabel} />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
onClick={() => handleBulkDownload(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
>
<FormattedMessage {...messages.downloadTitle} />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
data-testid="open-delete-confirmation-button"
onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
>
<FormattedMessage {...messages.deleteTitle} />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Button iconBefore={Add} onClick={fileInputControl.click}>
<FormattedMessage {...messages.addFilesButtonLabel} />
</Button>
<ModalDialog
title={intl.formatMessage(messages.sortModalTitleLabel)}
isOpen={isSortOpen}
onClose={closeSort}
size="lg"
hasCloseButton
>
<FormattedMessage {...messages.actionsButtonLabel} />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item
onClick={() => handleBulkDownload(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
>
<FormattedMessage {...messages.downloadTitle} />
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
data-testid="open-delete-confirmation-button"
onClick={() => handleOpenDeleteConfirmation(selectedFlatRows)}
disabled={_.isEmpty(selectedFlatRows)}
>
<FormattedMessage {...messages.deleteTitle} />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
<Button iconBefore={Add} onClick={fileInputControl.click} className="ml-2">
<FormattedMessage {...messages.addFilesButtonLabel} />
</Button>
</>
);
<ModalDialog.Header>
<ModalDialog.Title>
<FormattedMessage {...messages.sortModalTitleLabel} />
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<SelectableBox.Set
type="radio"
value={sortBy}
onChange={handleChange}
name="sort options"
columns={3}
ariaLabel="sort by selection"
>
<SelectableBox
className="text-center"
value="displayName,asc"
type="radio"
aria-label="name descending radio"
>
<FormattedMessage {...messages.sortByNameAscending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="dateAdded,desc"
type="radio"
aria-label="date added descending radio"
>
<FormattedMessage {...messages.sortByNewest} />
</SelectableBox>
<SelectableBox
className="text-center"
value="fileSize,desc"
type="radio"
aria-label="date added descending radio"
>
<FormattedMessage {...messages.sortBySizeDescending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="displayName,desc"
type="radio"
aria-label="name ascending radio"
>
<FormattedMessage {...messages.sortByNameDescending} />
</SelectableBox>
<SelectableBox
className="text-center"
value="dateAdded,asc"
type="radio"
aria-label="date added ascending radio"
>
<FormattedMessage {...messages.sortByOldest} />
</SelectableBox>
<SelectableBox
className="text-center"
value="fileSize,asc"
type="radio"
aria-label="date added ascending radio"
>
<FormattedMessage {...messages.sortBySizeAscending} />
</SelectableBox>
</SelectableBox.Set>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
<FormattedMessage {...messages.cancelButtonLabel} />
</ModalDialog.CloseButton>
<Button
variant="primary"
onClick={() => {
closeSort();
handleSort(sortBy);
}}
>
<FormattedMessage {...messages.applySortButton} />
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</>
);
};
TableActions.defaultProps = {
selectedFlatRows: null,
@@ -66,6 +174,9 @@ TableActions.propTypes = {
}).isRequired,
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
handleBulkDownload: PropTypes.func.isRequired,
handleSort: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
};
export default injectIntl(TableActions);