Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/files-and-videos/files-page/CourseFilesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export const CourseFilesTable = () => {
const {
assetIds,
loadingStatus,
addingStatus,
deletingStatus,
uploadProgress,
usageStatus: usagePathStatus,
errors: errorMessages,
} = useSelector((state: DeprecatedReduxState) => state.assets);
Expand Down Expand Up @@ -71,6 +74,9 @@ export const CourseFilesTable = () => {
const data = {
fileIds: assetIds,
loadingStatus,
addingStatus,
deletingStatus,
uploadProgress,
usagePathStatus,
usageErrorMessages: errorMessages.usageMetrics,
fileType: 'file',
Expand Down
4 changes: 2 additions & 2 deletions src/files-and-videos/files-page/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ export async function deleteAsset(courseId, assetId) {
* @param {blockId} courseId Course ID for the course to operate on

*/
export async function addAsset(courseId, file) {
export async function addAsset(courseId, file, onUploadProgress) {
const formData = new FormData();
formData.append('file', file);
const { data } = await getAuthenticatedHttpClient()
.post(getAssetsUrl(courseId), formData);
.post(getAssetsUrl(courseId), formData, { onUploadProgress });
return camelCaseObject(data);
}

Expand Down
5 changes: 5 additions & 0 deletions src/files-and-videos/files-page/data/slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const slice = createSlice({
duplicateFiles: [],
updatingStatus: '',
addingStatus: '',
uploadProgress: 0,
deletingStatus: '',
usageStatus: '',
errors: {
Expand Down Expand Up @@ -84,6 +85,9 @@ const slice = createSlice({
const { error } = payload;
state.errors[error] = [];
},
updateUploadProgress: (state, { payload }) => {
state.uploadProgress = payload.progress;
},
},
});

Expand All @@ -98,6 +102,7 @@ export const {
updateEditStatus,
updateDuplicateFiles,
clearAssetIds,
updateUploadProgress,
} = slice.actions;

export const {
Expand Down
11 changes: 10 additions & 1 deletion src/files-and-videos/files-page/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
updateEditStatus,
updateDuplicateFiles,
clearAssetIds,
updateUploadProgress,
} from './slice';

import { getUploadConflicts, updateFileValues } from './utils';
Expand Down Expand Up @@ -111,9 +112,15 @@ export function deleteAssetFile(courseId, id) {
export function addAssetFile(courseId, file, isOverwrite) {
return async (dispatch) => {
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.IN_PROGRESS }));
dispatch(updateUploadProgress({ progress: 0 }));

const onUploadProgress = ({ loaded, total }) => {
const percent = Math.round((loaded / total) * 100);
dispatch(updateUploadProgress({ progress: percent }));
};

try {
const { asset } = await addAsset(courseId, file);
const { asset } = await addAsset(courseId, file, onUploadProgress);
const [parsedAssets] = updateFileValues([asset]);
dispatch(addModel({
modelType: 'assets',
Expand All @@ -125,6 +132,7 @@ export function addAssetFile(courseId, file, isOverwrite) {
}));
}
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.SUCCESSFUL }));
dispatch(updateUploadProgress({ progress: 0 }));
} catch (error) {
if (error.response && error.response.status === 413) {
const message = error.response.data.error;
Expand All @@ -133,6 +141,7 @@ export function addAssetFile(courseId, file, isOverwrite) {
dispatch(updateErrors({ error: 'add', message: `Failed to add ${file.name}.` }));
}
dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }));
dispatch(updateUploadProgress({ progress: 0 }));
}
};
}
Expand Down
15 changes: 13 additions & 2 deletions src/files-and-videos/generic/ApiStatusToast.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Toast } from '@openedx/paragon';
import { Icon, Toast } from '@openedx/paragon';
import messages from './messages';

const ApiStatusToast = ({
Expand All @@ -11,6 +11,7 @@ const ApiStatusToast = ({
setClose,
setSelectedRows,
fileType,
icon,
}) => {
const intl = useIntl();
const handleClose = () => {
Expand All @@ -23,7 +24,12 @@ const ApiStatusToast = ({
show={isOpen}
onClose={handleClose}
>
{intl.formatMessage(messages.apiStatusToastMessage, { actionType, selectedRowCount, fileType })}
<div className="d-flex align-items-center">
{icon && <Icon src={icon} className="mr-2" style={{ fontSize: '1.25rem' }} />}
<span>
{intl.formatMessage(messages.apiStatusToastMessage, { actionType, selectedRowCount, fileType })}
</span>
</div>
</Toast>
);
};
Expand All @@ -35,6 +41,11 @@ ApiStatusToast.propTypes = {
setClose: PropTypes.func.isRequired,
setSelectedRows: PropTypes.func.isRequired,
fileType: PropTypes.string.isRequired,
icon: PropTypes.elementType,
};

ApiStatusToast.defaultProps = {
icon: null,
};

export default ApiStatusToast;
69 changes: 67 additions & 2 deletions src/files-and-videos/generic/FileTable.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import {
useCallback, useEffect, useRef, useState,
} from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import isEmpty from 'lodash/isEmpty';
Expand All @@ -7,9 +9,13 @@ import {
CardView,
DataTable,
Dropzone,
Icon,
ProgressBar,
TextFilter,
Toast,
useToggle,
} from '@openedx/paragon';
import { Check, DeleteOutline, FileUpload } from '@openedx/paragon/icons';

import { RequestStatus } from '../../data/constants';
import { sortFiles } from './utils';
Expand Down Expand Up @@ -59,6 +65,8 @@ const FileTable = ({
const [isAddOpen, setAddOpen, setAddClose] = useToggle(false);
const [selectedRows, setSelectedRows] = useState([]);
const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false);
const [isUploadSuccessOpen, setUploadSuccessOpen, setUploadSuccessClose] = useToggle(false);
const prevAddingStatusRef = useRef(null);
const [initialState, setInitialState] = useState({
filters: [],
hiddenColumns: [],
Expand All @@ -70,12 +78,40 @@ const FileTable = ({

const {
loadingStatus,
addingStatus,
deletingStatus,
uploadProgress,
usagePathStatus,
usageErrorMessages,
encodingsDownloadUrl,
supportedFileFormats,
fileType,
} = data;

useEffect(() => {
if (prevAddingStatusRef.current === RequestStatus.IN_PROGRESS
&& addingStatus === RequestStatus.SUCCESSFUL) {
setUploadSuccessOpen();
}
prevAddingStatusRef.current = addingStatus;
}, [addingStatus]);

useEffect(() => {
if (isUploadSuccessOpen) {
const timer = setTimeout(() => setUploadSuccessClose(), 3000);
return () => clearTimeout(timer);
}
return undefined;
}, [isUploadSuccessOpen]);

useEffect(() => {
if (deletingStatus === RequestStatus.SUCCESSFUL && isDeleteOpen) {
const timer = setTimeout(() => setDeleteClose(), 3000);
return () => clearTimeout(timer);
}
return undefined;
}, [deletingStatus, isDeleteOpen]);

const defaultCurrentView = (fileType === 'video' && localStorage.getItem('videosCurrentView')) || (fileType === 'file' && localStorage.getItem('filesCurrentView')) || defaultView;
const [currentView, setCurrentView] = useState(defaultCurrentView);

Expand Down Expand Up @@ -190,6 +226,18 @@ const FileTable = ({

return (
<div className="files-table">
{addingStatus === RequestStatus.IN_PROGRESS && uploadProgress > 0 && (
<div className="d-flex align-items-center bg-white border rounded p-3 mb-3 shadow-sm">
<Icon src={FileUpload} className="text-info mr-3" style={{ fontSize: '1.75rem' }} />
<div className="flex-grow-1">
<div className="d-flex justify-content-between align-items-center mb-1">
<span className="small font-weight-bold">Uploading...</span>
<span className="small text-muted">{uploadProgress}%</span>
</div>
<ProgressBar now={uploadProgress} variant="info" />
</div>
</div>
)}
<DataTable
isFilterable
isLoading={loadingStatus === RequestStatus.IN_PROGRESS}
Expand Down Expand Up @@ -245,12 +293,15 @@ const FileTable = ({
)}

<ApiStatusToast
actionType={intl.formatMessage(messages.apiStatusDeletingAction)}
actionType={deletingStatus === RequestStatus.SUCCESSFUL
? intl.formatMessage(messages.apiStatusDeletedAction)
: intl.formatMessage(messages.apiStatusDeletingAction)}
selectedRowCount={selectedRows.length}
isOpen={isDeleteOpen}
setClose={setDeleteClose}
setSelectedRows={setSelectedRows}
fileType={fileType}
icon={DeleteOutline}
/>

{fileType === 'file' && (
Expand All @@ -261,9 +312,20 @@ const FileTable = ({
setClose={setAddClose}
setSelectedRows={setSelectedRows}
fileType={fileType}
icon={FileUpload}
/>
)}

<Toast
show={isUploadSuccessOpen}
onClose={setUploadSuccessClose}
>
<div className="d-flex align-items-center">
<Icon src={Check} className="mr-2 text-success" style={{ fontSize: '1.25rem' }} />
<span>{intl.formatMessage(messages.uploadSuccessToastMessage)}</span>
</div>
</Toast>

<ApiStatusToast
actionType={intl.formatMessage(messages.apiStatusDownloadingAction)}
selectedRowCount={selectedRows.length}
Expand Down Expand Up @@ -306,6 +368,9 @@ FileTable.propTypes = {
data: PropTypes.shape({
fileIds: PropTypes.arrayOf(PropTypes.string).isRequired,
loadingStatus: PropTypes.string.isRequired,
addingStatus: PropTypes.string,
deletingStatus: PropTypes.string,
uploadProgress: PropTypes.number,
usagePathStatus: PropTypes.string.isRequired,
usageErrorMessages: PropTypes.arrayOf(PropTypes.string).isRequired,
encodingsDownloadUrl: PropTypes.string,
Expand Down
10 changes: 10 additions & 0 deletions src/files-and-videos/generic/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ const messages = defineMessages({
defaultMessage: 'Deleting',
description: 'This message is used in the toast when files are deleted',
},
apiStatusDeletedAction: {
id: 'course-authoring.files-and-upload.apiStatus.deletedAction.message',
defaultMessage: 'Deleted',
description: 'This message is used in the toast after files are deleted',
},
apiStatusDownloadingAction: {
id: 'course-authoring.files-and-upload.apiStatus.downloadingAction.message',
defaultMessage: 'Downloading',
Expand Down Expand Up @@ -214,6 +219,11 @@ const messages = defineMessages({
defaultMessage: 'Upload a file',
description: 'Accessible (screen reader) label for file input',
},
uploadSuccessToastMessage: {
id: 'course-authoring.files-and-uploads.upload.success.toast',
defaultMessage: 'File uploaded successfully',
description: 'Toast message shown when a file upload completes successfully',
},
});

export default messages;