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,
-}) => (
- <>
-
-
-
- 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);