From 181f9c7a5f4522f5a437d2c642d964cdb0039de6 Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Fri, 25 Aug 2023 10:08:15 -0400 Subject: [PATCH] feat: add sort function and modal (#577) * feat: add sort modal and function * fix: dateAdded typo * chore: update mock api data --- src/files-and-uploads/FileInfo.jsx | 5 +- src/files-and-uploads/FilesAndUploads.jsx | 25 ++- .../FilesAndUploads.test.jsx | 28 +++ src/files-and-uploads/data/thunks.js | 18 +- src/files-and-uploads/data/utils.js | 69 +++++-- .../factories/mockApiResponses.jsx | 32 ++-- src/files-and-uploads/messages.js | 38 +++- .../table-components/TableActions.jsx | 179 ++++++++++++++---- 8 files changed, 320 insertions(+), 74 deletions(-) diff --git a/src/files-and-uploads/FileInfo.jsx b/src/files-and-uploads/FileInfo.jsx index a07c40851..171958cbb 100644 --- a/src/files-and-uploads/FileInfo.jsx +++ b/src/files-and-uploads/FileInfo.jsx @@ -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 ( { 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)} diff --git a/src/files-and-uploads/FilesAndUploads.test.jsx b/src/files-and-uploads/FilesAndUploads.test.jsx index dbd76a5f7..4e52e6b68 100644 --- a/src/files-and-uploads/FilesAndUploads.test.jsx +++ b/src/files-and-uploads/FilesAndUploads.test.jsx @@ -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 () => { diff --git a/src/files-and-uploads/data/thunks.js b/src/files-and-uploads/data/thunks.js index 21622e6a5..6ef78b32e 100644 --- a/src/files-and-uploads/data/thunks.js +++ b/src/files-and-uploads/data/thunks.js @@ -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, diff --git a/src/files-and-uploads/data/utils.js b/src/files-and-uploads/data/utils.js index 1416fdfdf..823a81f6c 100644 --- a/src/files-and-uploads/data/utils.js +++ b/src/files-and-uploads/data/utils.js @@ -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; }; diff --git a/src/files-and-uploads/factories/mockApiResponses.jsx b/src/files-and-uploads/factories/mockApiResponses.jsx index a71b60eeb..d3ceb2720 100644 --- a/src/files-and-uploads/factories/mockApiResponses.jsx +++ b/src/files-and-uploads/factories/mockApiResponses.jsx @@ -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, }); diff --git a/src/files-and-uploads/messages.js b/src/files-and-uploads/messages.js index 21cd6196e..4af9b26f2 100644 --- a/src/files-and-uploads/messages.js +++ b/src/files-and-uploads/messages.js @@ -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; diff --git a/src/files-and-uploads/table-components/TableActions.jsx b/src/files-and-uploads/table-components/TableActions.jsx index 76d6ae418..e395ca301 100644 --- a/src/files-and-uploads/table-components/TableActions.jsx +++ b/src/files-and-uploads/table-components/TableActions.jsx @@ -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, -}) => ( - <> - - { + const [isSortOpen, openSort, closeSort] = useToggle(false); + const [sortBy, setSortBy] = useState('dateAdded,desc'); + const handleChange = (e) => { + setSortBy(e.target.value); + }; + return ( + <> + + + + + + + handleBulkDownload(selectedFlatRows)} + disabled={_.isEmpty(selectedFlatRows)} + > + + + + handleOpenDeleteConfirmation(selectedFlatRows)} + disabled={_.isEmpty(selectedFlatRows)} + > + + + + + + - - - - handleBulkDownload(selectedFlatRows)} - disabled={_.isEmpty(selectedFlatRows)} - > - - - - handleOpenDeleteConfirmation(selectedFlatRows)} - disabled={_.isEmpty(selectedFlatRows)} - > - - - - - - -); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; 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);