feat: add file zip on download (#580)
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"classnames": "2.2.6",
|
||||
"core-js": "3.8.1",
|
||||
"email-validator": "2.0.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "2.2.6",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
@@ -9885,6 +9886,11 @@
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "0.6.0",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"classnames": "2.2.6",
|
||||
"core-js": "3.8.1",
|
||||
"email-validator": "2.0.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "2.2.6",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -11,6 +11,7 @@ export const RequestStatus = {
|
||||
FAILED: 'failed',
|
||||
DENIED: 'denied',
|
||||
PENDING: 'pending',
|
||||
CLEAR: 'clear',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const fileInput = ({
|
||||
export const useFileInput = ({
|
||||
onAddFile,
|
||||
setSelectedRows,
|
||||
setAddOpen,
|
||||
}) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
const addFile = (e) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ const FileMenu = ({
|
||||
externalUrl,
|
||||
handleLock,
|
||||
locked,
|
||||
onDownload,
|
||||
openAssetInfo,
|
||||
openDeleteConfirmation,
|
||||
portableUrl,
|
||||
@@ -40,7 +41,7 @@ const FileMenu = ({
|
||||
>
|
||||
{intl.formatMessage(messages.copyWebUrlTitle)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={externalUrl} target="_blank" download>
|
||||
<Dropdown.Item onClick={onDownload}>
|
||||
{intl.formatMessage(messages.downloadTitle)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={handleLock}>
|
||||
@@ -64,6 +65,7 @@ FileMenu.propTypes = {
|
||||
externalUrl: PropTypes.string.isRequired,
|
||||
handleLock: PropTypes.func.isRequired,
|
||||
locked: PropTypes.bool.isRequired,
|
||||
onDownload: PropTypes.func.isRequired,
|
||||
openAssetInfo: PropTypes.func.isRequired,
|
||||
openDeleteConfirmation: PropTypes.func.isRequired,
|
||||
portableUrl: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
@@ -22,15 +22,17 @@ import {
|
||||
addAssetFile,
|
||||
deleteAssetFile,
|
||||
fetchAssets,
|
||||
resetErrors,
|
||||
getUsagePaths,
|
||||
updateAssetLock,
|
||||
updateAssetOrder,
|
||||
fetchAssetDownload,
|
||||
} from './data/thunks';
|
||||
import { sortFiles } from './data/utils';
|
||||
import messages from './messages';
|
||||
|
||||
import FileInfo from './FileInfo';
|
||||
import FileInput, { fileInput } from './FileInput';
|
||||
import FileInput, { useFileInput } from './FileInput';
|
||||
import FilesAndUploadsProvider from './FilesAndUploadsProvider';
|
||||
import {
|
||||
GalleryCard,
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
TableActions,
|
||||
} from './table-components';
|
||||
import ApiStatusToast from './ApiStatusToast';
|
||||
import { clearErrors } from './data/slice';
|
||||
|
||||
const FilesAndUploads = ({
|
||||
courseId,
|
||||
@@ -72,7 +75,7 @@ const FilesAndUploads = ({
|
||||
usageStatus: usagePathStatus,
|
||||
errors: errorMessages,
|
||||
} = useSelector(state => state.assets);
|
||||
const fileInputControl = fileInput({
|
||||
const fileInputControl = useFileInput({
|
||||
onAddFile: (file) => dispatch(addAssetFile(courseId, file, totalCount)),
|
||||
setSelectedRows,
|
||||
setAddOpen,
|
||||
@@ -95,25 +98,18 @@ const FilesAndUploads = ({
|
||||
const handleBulkDelete = () => {
|
||||
closeDeleteConfirmation();
|
||||
setDeleteOpen();
|
||||
dispatch(resetErrors({ errorType: 'delete' }));
|
||||
const assetIdsToDelete = selectedRows.map(row => row.original.id);
|
||||
assetIdsToDelete.forEach(id => dispatch(deleteAssetFile(courseId, id, totalCount)));
|
||||
};
|
||||
|
||||
const handleBulkDownload = (selectedFlatRows) => {
|
||||
selectedFlatRows.forEach(row => {
|
||||
const { externalUrl } = row.original;
|
||||
const link = document.createElement('a');
|
||||
link.target = '_blank';
|
||||
link.download = true;
|
||||
link.href = externalUrl;
|
||||
link.click();
|
||||
});
|
||||
/* ********** TODO ***********
|
||||
* implement a zip file function when there are multiple files
|
||||
*/
|
||||
};
|
||||
const handleBulkDownload = useCallback(async (selectedFlatRows) => {
|
||||
dispatch(resetErrors({ errorType: 'download' }));
|
||||
dispatch(fetchAssetDownload({ selectedRows: selectedFlatRows, courseId }));
|
||||
}, []);
|
||||
|
||||
const handleLockedAsset = (assetId, locked) => {
|
||||
dispatch(clearErrors({ errorType: 'lock' }));
|
||||
dispatch(updateAssetLock({ courseId, assetId, locked }));
|
||||
};
|
||||
|
||||
@@ -123,6 +119,7 @@ const FilesAndUploads = ({
|
||||
};
|
||||
|
||||
const handleOpenAssetInfo = (original) => {
|
||||
dispatch(resetErrors({ errorType: 'usageMetrics' }));
|
||||
setSelectedRows([{ original }]);
|
||||
dispatch(getUsagePaths({ asset: original, courseId, setSelectedRows }));
|
||||
openAssetInfo();
|
||||
@@ -146,6 +143,7 @@ const FilesAndUploads = ({
|
||||
<GalleryCard
|
||||
{...{
|
||||
handleLockedAsset,
|
||||
handleBulkDownload,
|
||||
handleOpenDeleteConfirmation,
|
||||
handleOpenAssetInfo,
|
||||
className,
|
||||
@@ -158,6 +156,7 @@ const FilesAndUploads = ({
|
||||
<ListCard
|
||||
{...{
|
||||
handleLockedAsset,
|
||||
handleBulkDownload,
|
||||
handleOpenDeleteConfirmation,
|
||||
handleOpenAssetInfo,
|
||||
className,
|
||||
@@ -183,8 +182,8 @@ const FilesAndUploads = ({
|
||||
isError={addAssetStatus === RequestStatus.FAILED}
|
||||
>
|
||||
<ul className="p-0">
|
||||
{errorMessages.upload.map(message => (
|
||||
<li style={{ listStyle: 'none' }}>
|
||||
{errorMessages.add.map(message => (
|
||||
<li key={`add-error-${message}`} style={{ listStyle: 'none' }}>
|
||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||
</li>
|
||||
))}
|
||||
@@ -196,7 +195,7 @@ const FilesAndUploads = ({
|
||||
>
|
||||
<ul className="p-0">
|
||||
{errorMessages.delete.map(message => (
|
||||
<li style={{ listStyle: 'none' }}>
|
||||
<li key={`delete-error-${message}`} style={{ listStyle: 'none' }}>
|
||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||
</li>
|
||||
))}
|
||||
@@ -208,12 +207,16 @@ const FilesAndUploads = ({
|
||||
>
|
||||
<ul className="p-0">
|
||||
{errorMessages.lock.map(message => (
|
||||
<li style={{ listStyle: 'none' }}>
|
||||
<li key={`lock-error-${message}`} style={{ listStyle: 'none' }}>
|
||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||
</li>
|
||||
))}
|
||||
{errorMessages.download.map(message => (
|
||||
<li key={`download-error-${message}`} style={{ listStyle: 'none' }}>
|
||||
{intl.formatMessage(messages.errorAlertMessage, { message })}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
</ErrorAlert>
|
||||
<div className="h2">
|
||||
<FormattedMessage {...messages.heading} />
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
@@ -42,6 +43,7 @@ let axiosMock;
|
||||
let store;
|
||||
let file;
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
jest.mock('file-saver');
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
@@ -89,22 +91,27 @@ describe('FilesAndUploads', () => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||
});
|
||||
|
||||
it('should return placeholder component', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.DENIED);
|
||||
expect(screen.getByTestId('under-construction-placeholder')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should have Files and uploads title', async () => {
|
||||
renderComponent();
|
||||
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByText('Files and uploads')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render dropzone', async () => {
|
||||
renderComponent();
|
||||
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('files-dropzone')).toBeVisible();
|
||||
|
||||
expect(screen.queryByTestId('files-data-table')).toBeNull();
|
||||
});
|
||||
|
||||
it('should upload a single file', async () => {
|
||||
renderComponent();
|
||||
await emptyMockStore(RequestStatus.SUCCESSFUL);
|
||||
@@ -119,10 +126,13 @@ describe('FilesAndUploads', () => {
|
||||
});
|
||||
const addStatus = store.getState().assets.addingStatus;
|
||||
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
|
||||
expect(screen.queryByTestId('files-dropzone')).toBeNull();
|
||||
|
||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid assets', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
@@ -137,27 +147,39 @@ describe('FilesAndUploads', () => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
saveAs.mockClear();
|
||||
});
|
||||
|
||||
describe('table view', () => {
|
||||
it('should render table with gallery card', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should switch table to list view', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
expect(screen.queryByTestId('list-card-mOckID1')).toBeNull();
|
||||
|
||||
const listButton = screen.getByLabelText('List');
|
||||
await act(async () => {
|
||||
fireEvent.click(listButton);
|
||||
});
|
||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||
|
||||
expect(screen.getByTestId('list-card-mOckID1')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('table actions', () => {
|
||||
it('should upload a single file', async () => {
|
||||
renderComponent();
|
||||
@@ -171,17 +193,21 @@ describe('FilesAndUploads', () => {
|
||||
const addStatus = store.getState().assets.addingStatus;
|
||||
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
});
|
||||
|
||||
it('should have disabled action buttons', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||
expect(actionsButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(actionsButton);
|
||||
});
|
||||
expect(screen.getByText(messages.downloadTitle.defaultMessage).closest('a')).toHaveClass('disabled');
|
||||
|
||||
expect(screen.getByText(messages.deleteTitle.defaultMessage).closest('a')).toHaveClass('disabled');
|
||||
});
|
||||
|
||||
it('delete button should be enabled and delete selected file', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
@@ -189,59 +215,113 @@ describe('FilesAndUploads', () => {
|
||||
fireEvent.click(selectCardButton);
|
||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||
expect(actionsButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(actionsButton);
|
||||
});
|
||||
const deleteButton = screen.getByText(messages.deleteTitle.defaultMessage).closest('a');
|
||||
expect(deleteButton).not.toHaveClass('disabled');
|
||||
|
||||
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
|
||||
await waitFor(() => {
|
||||
fireEvent.click(deleteButton);
|
||||
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
||||
|
||||
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
||||
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
||||
|
||||
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||
});
|
||||
const deleteStatus = store.getState().assets.deletingStatus;
|
||||
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
|
||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||
});
|
||||
|
||||
it('download button should be enabled and download single selected file', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
const selectCardButton = screen.getAllByTestId('datatable-select-column-checkbox-cell')[0];
|
||||
fireEvent.click(selectCardButton);
|
||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||
expect(actionsButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(actionsButton);
|
||||
});
|
||||
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||
expect(downloadButton).not.toHaveClass('disabled');
|
||||
|
||||
fireEvent.click(downloadButton);
|
||||
expect(saveAs).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('download button should be enabled and download multiple selected files', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
const selectCardButtons = screen.getAllByTestId('datatable-select-column-checkbox-cell');
|
||||
fireEvent.click(selectCardButtons[0]);
|
||||
fireEvent.click(selectCardButtons[1]);
|
||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||
expect(actionsButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(actionsButton);
|
||||
});
|
||||
const mockResponseData = { ok: true, blob: () => 'Data' };
|
||||
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||
expect(downloadButton).not.toHaveClass('disabled');
|
||||
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
fireEvent.click(downloadButton);
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID1/usage`).reply(201, { usageLocations: ['subsection - unit / block'] });
|
||||
await waitFor(() => {
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
@@ -253,16 +333,19 @@ describe('FilesAndUploads', () => {
|
||||
}), store.dispatch);
|
||||
expect(screen.getAllByLabelText('mOckID1')[0]).toBeVisible();
|
||||
});
|
||||
|
||||
const { usageStatus } = store.getState().assets;
|
||||
expect(usageStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByText('subsection - unit / block')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should open asset info and handle lock checkbox', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID1`).reply(201, { locked: false });
|
||||
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID1/usage`).reply(201, { usageLocations: [] });
|
||||
await waitFor(() => {
|
||||
@@ -274,6 +357,7 @@ describe('FilesAndUploads', () => {
|
||||
setSelectedRows: jest.fn(),
|
||||
}), store.dispatch);
|
||||
expect(screen.getAllByLabelText('mOckID1')[0]).toBeVisible();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Checkbox'));
|
||||
executeThunk(updateAssetLock({
|
||||
courseId,
|
||||
@@ -282,15 +366,19 @@ describe('FilesAndUploads', () => {
|
||||
}), store.dispatch);
|
||||
});
|
||||
expect(screen.getByText(messages.usageNotInUseMessage.defaultMessage)).toBeVisible();
|
||||
|
||||
const updateStatus = store.getState().assets.updatingStatus;
|
||||
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
});
|
||||
|
||||
it('should unlock asset', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID1`).reply(201, { locked: false });
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
@@ -304,12 +392,15 @@ describe('FilesAndUploads', () => {
|
||||
const updateStatus = store.getState().assets.updatingStatus;
|
||||
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
});
|
||||
|
||||
it('should lock asset', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID3`).reply(201, { locked: true });
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
@@ -323,26 +414,48 @@ describe('FilesAndUploads', () => {
|
||||
const updateStatus = store.getState().assets.updatingStatus;
|
||||
expect(updateStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
});
|
||||
|
||||
it('download button should download file', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
fireEvent.click(screen.getByText('Download'));
|
||||
});
|
||||
expect(saveAs).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('delete button should delete file', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(204);
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
||||
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
||||
|
||||
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
||||
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
||||
|
||||
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||
});
|
||||
const deleteStatus = store.getState().assets.deletingStatus;
|
||||
expect(deleteStatus).toEqual(RequestStatus.SUCCESSFUL);
|
||||
|
||||
expect(screen.queryByTestId('grid-card-mOckID1')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('api errors', () => {
|
||||
it('invalid file size should show error', async () => {
|
||||
const errorMessage = 'File download.png exceeds maximum size of 20 MB.';
|
||||
@@ -356,8 +469,10 @@ describe('FilesAndUploads', () => {
|
||||
});
|
||||
const addStatus = store.getState().assets.addingStatus;
|
||||
expect(addStatus).toEqual(RequestStatus.FAILED);
|
||||
|
||||
expect(screen.getByText('Error')).toBeVisible();
|
||||
});
|
||||
|
||||
it('404 upload should show error', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
@@ -369,35 +484,45 @@ describe('FilesAndUploads', () => {
|
||||
});
|
||||
const addStatus = store.getState().assets.addingStatus;
|
||||
expect(addStatus).toEqual(RequestStatus.FAILED);
|
||||
|
||||
expect(screen.getByText('Error')).toBeVisible();
|
||||
});
|
||||
|
||||
it('404 delete should show error', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
axiosMock.onDelete(`${getAssetsUrl(courseId)}mOckID1`).reply(404);
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
|
||||
expect(screen.getByText(messages.deleteConfirmationTitle.defaultMessage)).toBeVisible();
|
||||
|
||||
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
|
||||
expect(screen.queryByText(messages.deleteConfirmationTitle.defaultMessage)).toBeNull();
|
||||
|
||||
executeThunk(deleteAssetFile(courseId, 'mOckID1', 5), store.dispatch);
|
||||
});
|
||||
const deleteStatus = store.getState().assets.deletingStatus;
|
||||
expect(deleteStatus).toEqual(RequestStatus.FAILED);
|
||||
|
||||
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
|
||||
|
||||
expect(screen.getByText('Error')).toBeVisible();
|
||||
});
|
||||
|
||||
it('404 usage path fetch should show error', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
axiosMock.onGet(`${getAssetsUrl(courseId)}mOckID3/usage`).reply(404);
|
||||
await waitFor(() => {
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
@@ -411,12 +536,15 @@ describe('FilesAndUploads', () => {
|
||||
const { usageStatus } = store.getState().assets;
|
||||
expect(usageStatus).toEqual(RequestStatus.FAILED);
|
||||
});
|
||||
|
||||
it('404 lock update should show error', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
|
||||
|
||||
const assetMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
|
||||
expect(assetMenuButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
axiosMock.onPut(`${getAssetsUrl(courseId)}mOckID3`).reply(404);
|
||||
fireEvent.click(within(assetMenuButton).getByLabelText('asset-menu-toggle'));
|
||||
@@ -429,6 +557,36 @@ describe('FilesAndUploads', () => {
|
||||
});
|
||||
const updateStatus = store.getState().assets.updatingStatus;
|
||||
expect(updateStatus).toEqual(RequestStatus.FAILED);
|
||||
|
||||
expect(screen.getByText('Error')).toBeVisible();
|
||||
});
|
||||
|
||||
it('multiple asset file fetch failure should show error', async () => {
|
||||
renderComponent();
|
||||
await mockStore(RequestStatus.SUCCESSFUL);
|
||||
const selectCardButtons = screen.getAllByTestId('datatable-select-column-checkbox-cell');
|
||||
fireEvent.click(selectCardButtons[0]);
|
||||
fireEvent.click(selectCardButtons[1]);
|
||||
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
|
||||
expect(actionsButton).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(actionsButton);
|
||||
});
|
||||
const mockResponseData = { ok: false };
|
||||
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||
const downloadButton = screen.getByText(messages.downloadTitle.defaultMessage).closest('a');
|
||||
expect(downloadButton).not.toHaveClass('disabled');
|
||||
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
await waitFor(() => {
|
||||
fireEvent.click(downloadButton);
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
const updateStatus = store.getState().assets.updatingStatus;
|
||||
expect(updateStatus).toEqual(RequestStatus.FAILED);
|
||||
|
||||
expect(screen.getByText('Error')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,11 @@ const UsageMetricsMessage = ({
|
||||
<FormattedMessage {...messages.usageNotInUseMessage} />
|
||||
) : (
|
||||
<ul className="p-0">
|
||||
{usageLocations.map((location) => (<li style={{ listStyle: 'none' }}>{location}</li>))}
|
||||
{usageLocations.map(location => (
|
||||
<li key={`usage-location-${location}`} style={{ listStyle: 'none' }}>
|
||||
{location}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
} else if (usagePathStatus === RequestStatus.FAILED) {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { camelCaseObject, ensureConfig, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import JSZip from 'jszip';
|
||||
import saveAs from 'file-saver';
|
||||
|
||||
ensureConfig([
|
||||
'STUDIO_BASE_URL',
|
||||
], 'Course Apps API service');
|
||||
@@ -20,6 +23,62 @@ export async function getAssets(courseId, totalCount) {
|
||||
.get(`${getAssetsUrl(courseId)}?page_size=${pageCount}`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch asset file.
|
||||
* @param {blockId} courseId Course ID for the course to operate on
|
||||
|
||||
*/
|
||||
export async function getDownload(selectedRows, courseId) {
|
||||
const downloadErrors = [];
|
||||
if (selectedRows?.length > 1) {
|
||||
const zip = new JSZip();
|
||||
const date = new Date().toString();
|
||||
const folder = zip.folder(`${courseId}-assets-${date}`);
|
||||
const assetNames = [];
|
||||
const assetFetcher = await Promise.allSettled(
|
||||
selectedRows.map(async (row) => {
|
||||
const asset = row?.original;
|
||||
try {
|
||||
assetNames.push(asset.displayName);
|
||||
const res = await fetch(`${getApiBaseUrl()}/${asset.id}`);
|
||||
if (!res.ok) {
|
||||
throw new Error();
|
||||
}
|
||||
return res.blob();
|
||||
} catch (error) {
|
||||
downloadErrors.push(`Failed to download ${asset?.displayName}.`);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const definedAssets = assetFetcher.filter(asset => asset.value !== null);
|
||||
if (definedAssets.length > 0) {
|
||||
definedAssets.forEach((assetBlob, index) => {
|
||||
folder.file(assetNames[index], assetBlob.value, { blob: true });
|
||||
});
|
||||
zip.generateAsync({ type: 'blob' }).then(content => {
|
||||
saveAs(content, `${courseId}-assets-${date}.zip`);
|
||||
});
|
||||
}
|
||||
} else if (selectedRows?.length === 1) {
|
||||
const asset = selectedRows[0].original;
|
||||
try {
|
||||
saveAs(`${getApiBaseUrl()}/${asset.id}`, asset.displayName);
|
||||
} catch (error) {
|
||||
downloadErrors.push(`Failed to download ${asset?.displayName}.`);
|
||||
}
|
||||
} else {
|
||||
downloadErrors.push('No files were selected to download');
|
||||
}
|
||||
return downloadErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch where asset is used in a course.
|
||||
* @param {blockId} courseId Course ID for the course to operate on
|
||||
|
||||
*/
|
||||
export async function getAssetUsagePaths({ courseId, assetId }) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getAssetsUrl(courseId)}${assetId}/usage`);
|
||||
@@ -27,7 +86,7 @@ export async function getAssetUsagePaths({ courseId, assetId }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete custom page for provided block.
|
||||
* Delete asset to course.
|
||||
* @param {blockId} courseId Course ID for the course to operate on
|
||||
|
||||
*/
|
||||
@@ -37,7 +96,7 @@ export async function deleteAsset(courseId, assetId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom page for provided block.
|
||||
* Add asset to course.
|
||||
* @param {blockId} courseId Course ID for the course to operate on
|
||||
|
||||
*/
|
||||
|
||||
57
src/files-and-uploads/data/api.test.js
Normal file
57
src/files-and-uploads/data/api.test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { getDownload } from './api';
|
||||
import 'file-saver';
|
||||
|
||||
jest.mock('file-saver');
|
||||
|
||||
describe('api.js', () => {
|
||||
describe('getDownload', () => {
|
||||
describe('selectedRows length is undefined or less than zero', () => {
|
||||
it('should return with no files selected error if selectedRows is empty', async () => {
|
||||
const expected = ['No files were selected to download'];
|
||||
const actual = await getDownload([], 'courseId');
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('should return with no files selected error if selectedRows is null', async () => {
|
||||
const expected = ['No files were selected to download'];
|
||||
const actual = await getDownload(null, 'courseId');
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
describe('selectedRows length is greater than one', () => {
|
||||
it('should not throw error when blob returns null', async () => {
|
||||
const mockResponseData = { ok: true, blob: () => null };
|
||||
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
const expected = [];
|
||||
const actual = await getDownload([
|
||||
{ original: { displayName: 'test1' } },
|
||||
{ original: { displayName: 'test2', id: '2' } },
|
||||
]);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
it('should return error if row does not contain .original ancestor', async () => {
|
||||
const mockResponseData = { ok: true, blob: () => 'data' };
|
||||
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
const expected = ['Failed to download undefined.'];
|
||||
const actual = await getDownload([
|
||||
{ asset: { displayName: 'test1', id: '1' } },
|
||||
{ original: { displayName: 'test2', id: '2' } },
|
||||
]);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
describe('selectedRows length equals one', () => {
|
||||
it('should return error if row does not contain .original ancestor', async () => {
|
||||
const mockResponseData = { ok: true, blob: () => 'data' };
|
||||
const mockFetchResponse = Promise.resolve(mockResponseData);
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
const expected = ['Failed to download undefined.'];
|
||||
const actual = await getDownload([
|
||||
{ asset: { displayName: 'test1', id: '1' } },
|
||||
]);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,9 +13,10 @@ const slice = createSlice({
|
||||
deletingStatus: '',
|
||||
usageStatus: '',
|
||||
errors: {
|
||||
upload: [],
|
||||
add: [],
|
||||
delete: [],
|
||||
lock: [],
|
||||
download: [],
|
||||
usageMetrics: [],
|
||||
},
|
||||
totalCount: 0,
|
||||
@@ -30,14 +31,27 @@ const slice = createSlice({
|
||||
updateLoadingStatus: (state, { payload }) => {
|
||||
state.loadingStatus = payload.status;
|
||||
},
|
||||
updateUpdatingStatus: (state, { payload }) => {
|
||||
state.updatingStatus = payload.status;
|
||||
},
|
||||
updateAddingStatus: (state, { payload }) => {
|
||||
state.addingStatus = payload.status;
|
||||
},
|
||||
updateDeletingStatus: (state, { payload }) => {
|
||||
state.deletingStatus = payload.status;
|
||||
updateEditStatus: (state, { payload }) => {
|
||||
const { editType, status } = payload;
|
||||
switch (editType) {
|
||||
case 'delete':
|
||||
state.deletingStatus = status;
|
||||
break;
|
||||
case 'add':
|
||||
state.addingStatus = status;
|
||||
break;
|
||||
case 'lock':
|
||||
state.updatingStatus = status;
|
||||
break;
|
||||
case 'download':
|
||||
state.updatingStatus = status;
|
||||
break;
|
||||
case 'usageMetrics':
|
||||
state.usageStatus = status;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
deleteAssetSuccess: (state, { payload }) => {
|
||||
state.assetIds = state.assetIds.filter(id => id !== payload.assetId);
|
||||
@@ -45,14 +59,15 @@ const slice = createSlice({
|
||||
addAssetSuccess: (state, { payload }) => {
|
||||
state.assetIds = [payload.assetId, ...state.assetIds];
|
||||
},
|
||||
updateUsageStatus: (state, { payload }) => {
|
||||
state.usageStatus = payload.status;
|
||||
},
|
||||
updateErrors: (state, { payload }) => {
|
||||
const { error, message } = payload;
|
||||
const currentErrorState = state.errors[error];
|
||||
state.errors[error] = [...currentErrorState, message];
|
||||
},
|
||||
clearErrors: (state, { payload }) => {
|
||||
const { error } = payload;
|
||||
state.errors[error] = [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -60,13 +75,11 @@ export const {
|
||||
setAssetIds,
|
||||
setTotalCount,
|
||||
updateLoadingStatus,
|
||||
updateUpdatingStatus,
|
||||
deleteAssetSuccess,
|
||||
updateDeletingStatus,
|
||||
addAssetSuccess,
|
||||
updateAddingStatus,
|
||||
updateUsageStatus,
|
||||
updateErrors,
|
||||
clearErrors,
|
||||
updateEditStatus,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import {
|
||||
addModel,
|
||||
@@ -11,18 +12,17 @@ import {
|
||||
addAsset,
|
||||
deleteAsset,
|
||||
updateLockStatus,
|
||||
getDownload,
|
||||
} from './api';
|
||||
import {
|
||||
setAssetIds,
|
||||
setTotalCount,
|
||||
updateLoadingStatus,
|
||||
updateUpdatingStatus,
|
||||
deleteAssetSuccess,
|
||||
updateDeletingStatus,
|
||||
addAssetSuccess,
|
||||
updateAddingStatus,
|
||||
updateErrors,
|
||||
updateUsageStatus,
|
||||
clearErrors,
|
||||
updateEditStatus,
|
||||
} from './slice';
|
||||
|
||||
import { updateFileValues } from './utils';
|
||||
@@ -61,24 +61,24 @@ export function updateAssetOrder(courseId, assetIds) {
|
||||
|
||||
export function deleteAssetFile(courseId, id, totalCount) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateDeletingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
await deleteAsset(courseId, id);
|
||||
dispatch(deleteAssetSuccess({ assetId: id }));
|
||||
dispatch(removeModel({ modelType: 'assets', id }));
|
||||
dispatch(setTotalCount({ totalCount: totalCount - 1 }));
|
||||
dispatch(updateDeletingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateErrors({ error: 'delete', message: `Failed to delete file id ${id}.` }));
|
||||
dispatch(updateDeletingStatus({ status: RequestStatus.FAILED }));
|
||||
dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function addAssetFile(courseId, file, totalCount) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateAddingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const { asset } = await addAsset(courseId, file);
|
||||
@@ -91,22 +91,22 @@ export function addAssetFile(courseId, file, totalCount) {
|
||||
assetId: asset.id,
|
||||
}));
|
||||
dispatch(setTotalCount({ totalCount: totalCount + 1 }));
|
||||
dispatch(updateAddingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 413) {
|
||||
const message = error.response.data.error;
|
||||
dispatch(updateErrors({ error: 'upload', message }));
|
||||
dispatch(updateErrors({ error: 'add', message }));
|
||||
} else {
|
||||
dispatch(updateErrors({ error: 'upload', message: `Failed to add ${file.name}.` }));
|
||||
dispatch(updateErrors({ error: 'add', message: `Failed to add ${file.name}.` }));
|
||||
}
|
||||
dispatch(updateAddingStatus({ status: RequestStatus.FAILED }));
|
||||
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAssetLock({ assetId, courseId, locked }) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateUpdatingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
await updateLockStatus({ assetId, courseId, locked });
|
||||
@@ -117,26 +117,45 @@ export function updateAssetLock({ assetId, courseId, locked }) {
|
||||
locked,
|
||||
},
|
||||
}));
|
||||
dispatch(updateUpdatingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
const lockStatus = locked ? 'lock' : 'unlock';
|
||||
dispatch(updateErrors({ error: 'lock', message: `Failed to ${lockStatus} file id ${assetId}.` }));
|
||||
dispatch(updateUpdatingStatus({ status: RequestStatus.FAILED }));
|
||||
dispatch(updateEditStatus({ editType: 'lock', status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resetErrors({ errorType }) {
|
||||
return (dispatch) => { dispatch(clearErrors({ error: errorType })); };
|
||||
}
|
||||
|
||||
export function getUsagePaths({ asset, courseId, setSelectedRows }) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateUsageStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const { usageLocations } = await getAssetUsagePaths({ assetId: asset.id, courseId });
|
||||
setSelectedRows([{ original: { ...asset, usageLocations } }]);
|
||||
dispatch(updateUsageStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateErrors({ error: 'usageMetrics', message: `Failed to get usage metrics for ${asset.displayName}.` }));
|
||||
dispatch(updateUsageStatus({ status: RequestStatus.FAILED }));
|
||||
dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchAssetDownload({ selectedRows, courseId }) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.IN_PROGRESS }));
|
||||
const errors = await getDownload(selectedRows, courseId);
|
||||
if (isEmpty(errors)) {
|
||||
dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.SUCCESSFUL }));
|
||||
} else {
|
||||
errors.forEach(error => {
|
||||
dispatch(updateErrors({ error: 'download', message: error }));
|
||||
});
|
||||
dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ export const initialState = {
|
||||
addingStatus: '',
|
||||
usageStatus: '',
|
||||
errors: {
|
||||
upload: [],
|
||||
add: [],
|
||||
delete: [],
|
||||
lock: [],
|
||||
download: [],
|
||||
usageMetrics: [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getSrc } from '../data/utils';
|
||||
const GalleryCard = ({
|
||||
className,
|
||||
original,
|
||||
handleBulkDownload,
|
||||
handleLockedAsset,
|
||||
handleOpenDeleteConfirmation,
|
||||
handleOpenAssetInfo,
|
||||
@@ -43,6 +44,9 @@ const GalleryCard = ({
|
||||
portableUrl={original.portableUrl}
|
||||
iconSrc={MoreVert}
|
||||
id={original.id}
|
||||
onDownload={() => handleBulkDownload(
|
||||
[{ original: { id: original.id, displayName: original.displayName } }],
|
||||
)}
|
||||
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
||||
/>
|
||||
</ActionRow>
|
||||
@@ -87,6 +91,7 @@ GalleryCard.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
portableUrl: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
handleBulkDownload: PropTypes.func.isRequired,
|
||||
handleLockedAsset: PropTypes.func.isRequired,
|
||||
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
||||
handleOpenAssetInfo: PropTypes.func.isRequired,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getSrc } from '../data/utils';
|
||||
const ListCard = ({
|
||||
className,
|
||||
original,
|
||||
handleBulkDownload,
|
||||
handleLockedAsset,
|
||||
handleOpenDeleteConfirmation,
|
||||
handleOpenAssetInfo,
|
||||
@@ -67,6 +68,9 @@ const ListCard = ({
|
||||
portableUrl={original.portableUrl}
|
||||
iconSrc={MoreVert}
|
||||
id={original.id}
|
||||
onDownload={() => handleBulkDownload(
|
||||
[{ original: { id: original.id, displayName: original.displayName } }],
|
||||
)}
|
||||
openDeleteConfirmation={() => handleOpenDeleteConfirmation([{ original }])}
|
||||
/>
|
||||
</ActionRow>
|
||||
@@ -89,6 +93,7 @@ ListCard.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
portableUrl: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
handleBulkDownload: PropTypes.func.isRequired,
|
||||
handleLockedAsset: PropTypes.func.isRequired,
|
||||
handleOpenDeleteConfirmation: PropTypes.func.isRequired,
|
||||
handleOpenAssetInfo: PropTypes.func.isRequired,
|
||||
|
||||
Reference in New Issue
Block a user