diff --git a/.env.development b/.env.development
index 279bbaedae..de0150810e 100644
--- a/.env.development
+++ b/.env.development
@@ -35,7 +35,8 @@ ENABLE_TEAM_TYPE_SETTING=false
ENABLE_NEW_EDITOR_PAGES=true
ENABLE_UNIT_PAGE=false
ENABLE_ASSETS_PAGE=false
-ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
+ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
+ENABLE_NEW_VIDEO_UPLOAD_PAGE=true
ENABLE_TAGGING_TAXONOMY_PAGES=true
BBB_LEARN_MORE_URL=''
HOTJAR_APP_ID=''
diff --git a/src/files-and-videos/generic/messages.js b/src/files-and-videos/generic/messages.js
index 7e452f5a35..d6f6d03e87 100644
--- a/src/files-and-videos/generic/messages.js
+++ b/src/files-and-videos/generic/messages.js
@@ -199,6 +199,10 @@ const messages = defineMessages({
defaultMessage: 'Apply',
description: 'Label for apply sort button in sort and filter modal',
},
+ failedLabel: {
+ id: 'course-authoring.files-and-uploads.filter.failed.label',
+ defaultMessage: 'Failed',
+ },
});
export default messages;
diff --git a/src/files-and-videos/generic/table-components/table-custom-columns/StatusColumn.jsx b/src/files-and-videos/generic/table-components/table-custom-columns/StatusColumn.jsx
index c88bb65875..4d7195301c 100644
--- a/src/files-and-videos/generic/table-components/table-custom-columns/StatusColumn.jsx
+++ b/src/files-and-videos/generic/table-components/table-custom-columns/StatusColumn.jsx
@@ -1,10 +1,16 @@
import React from 'react';
import { PropTypes } from 'prop-types';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { Badge } from '@openedx/paragon';
+import { VIDEO_FAILURE_STATUSES } from '../../../videos-page/data/constants';
+import messages from '../../messages';
const StatusColumn = ({ row }) => {
const { status } = row.original;
const isUploaded = status === 'Success';
+ const isFailed = VIDEO_FAILURE_STATUSES.includes(status);
+ const intl = useIntl();
+ const failedText = intl.formatMessage(messages.failedLabel);
if (isUploaded) {
return null;
@@ -12,7 +18,7 @@ const StatusColumn = ({ row }) => {
return (
- {status}
+ {isFailed ? failedText : status}
);
};
diff --git a/src/files-and-videos/index.scss b/src/files-and-videos/index.scss
index 2e419559af..0af5102913 100644
--- a/src/files-and-videos/index.scss
+++ b/src/files-and-videos/index.scss
@@ -71,3 +71,12 @@
gap: 24px 16px;
grid-template-columns: repeat(3, 33%);
}
+
+.video-upload-spinner {
+ width: 1.3rem;
+ height: 1.3rem;
+}
+
+.video-upload-warning-text {
+ font-size: 18px;
+}
diff --git a/src/files-and-videos/videos-page/VideoThumbnail.jsx b/src/files-and-videos/videos-page/VideoThumbnail.jsx
index d1c65f3374..60543991dc 100644
--- a/src/files-and-videos/videos-page/VideoThumbnail.jsx
+++ b/src/files-and-videos/videos-page/VideoThumbnail.jsx
@@ -10,7 +10,7 @@ import {
} from '@openedx/paragon';
import { FileInput, useFileInput } from '../generic';
import messages from './messages';
-import { VIDEO_SUCCESS_STATUSES } from './data/constants';
+import { VIDEO_SUCCESS_STATUSES, VIDEO_FAILURE_STATUSES } from './data/constants';
import { RequestStatus } from '../../data/constants';
const VideoThumbnail = ({
@@ -45,6 +45,8 @@ const VideoThumbnail = ({
const supportedFiles = videoImageSettings?.supportedFileFormats
? Object.values(videoImageSettings.supportedFileFormats) : null;
const isUploaded = VIDEO_SUCCESS_STATUSES.includes(status);
+ const isFailed = VIDEO_FAILURE_STATUSES.includes(status);
+ const failedMessage = intl.formatMessage(messages.failedCheckboxLabel);
const showThumbnail = allowThumbnailUpload && thumbnail && isUploaded;
@@ -84,7 +86,7 @@ const VideoThumbnail = ({
diff --git a/src/files-and-videos/videos-page/VideosPage.test.jsx b/src/files-and-videos/videos-page/VideosPage.test.jsx
index 33fe2be9b7..cc1b1f52af 100644
--- a/src/files-and-videos/videos-page/VideosPage.test.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.test.jsx
@@ -36,10 +36,12 @@ import {
addVideoThumbnail,
fetchVideoDownload,
} from './data/thunks';
-import { getVideosUrl, getCourseVideosApiUrl, getApiBaseUrl } from './data/api';
+import * as api from './data/api';
import videoMessages from './messages';
import messages from '../generic/messages';
+const { getVideosUrl, getCourseVideosApiUrl, getApiBaseUrl } = api;
+
let axiosMock;
let store;
let file;
@@ -136,7 +138,7 @@ describe('Videos page', () => {
value: [file],
});
fireEvent.drop(dropzone);
- await executeThunk(addVideoFile(courseId, file, []), store.dispatch);
+ await executeThunk(addVideoFile(courseId, file, [], { current: [] }), store.dispatch);
});
const addStatus = store.getState().videos.addingStatus;
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
@@ -157,7 +159,7 @@ describe('Videos page', () => {
roles: [],
},
});
- store = initializeStore(initialState);
+ store = initializeStore({ ...initialState });
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
});
@@ -224,6 +226,13 @@ describe('Videos page', () => {
expect(addThumbnailButton).toBeNull();
});
+ describe('with videos with backend status in_progress', () => {
+ it('should render video with in progress status', async () => {
+ await mockStore(RequestStatus.IN_PROGRESS);
+ expect(screen.getByText('Failed')).toBeVisible();
+ expect(screen.queryByText('In Progress')).not.toBeInTheDocument();
+ });
+ });
});
describe('table actions', () => {
@@ -240,12 +249,46 @@ describe('Videos page', () => {
const { videoIds } = store.getState().videos;
await act(async () => {
userEvent.upload(addFilesButton, file);
- await executeThunk(addVideoFile(courseId, file, videoIds), store.dispatch);
+ await executeThunk(addVideoFile(courseId, file, videoIds, { current: [] }), store.dispatch);
});
const addStatus = store.getState().videos.addingStatus;
expect(addStatus).toEqual(RequestStatus.SUCCESSFUL);
});
+ it('when uploads are in progress, should show alert and set them to failed on page leave', async () => {
+ await mockStore(RequestStatus.SUCCESSFUL);
+
+ const mockResponseData = { status: '200', ok: true, blob: () => 'Data' };
+ const mockFetchResponse = Promise.resolve(mockResponseData);
+ global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
+
+ axiosMock.onPost(getCourseVideosApiUrl(courseId)).reply(204, generateNewVideoApiResponse());
+ axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse());
+
+ const uploadSpy = jest.spyOn(api, 'uploadVideo');
+ const setFailedSpy = jest.spyOn(api, 'sendVideoUploadStatus').mockImplementation(() => {});
+ uploadSpy.mockResolvedValue(new Promise(() => {}));
+
+ const addFilesButton = screen.getAllByLabelText('file-input')[3];
+ act(async () => {
+ userEvent.upload(addFilesButton, file);
+ });
+ await waitFor(() => {
+ const addStatus = store.getState().videos.addingStatus;
+ expect(addStatus).toEqual(RequestStatus.IN_PROGRESS);
+ expect(uploadSpy).toHaveBeenCalled();
+ expect(screen.getByText(videoMessages.videoUploadAlertLabel.defaultMessage)).toBeVisible();
+ });
+ act(() => {
+ window.dispatchEvent(new Event('beforeunload'));
+ });
+ await waitFor(() => {
+ expect(setFailedSpy).toHaveBeenCalledWith(courseId, expect.any(String), expect.any(String), 'upload_failed');
+ });
+ uploadSpy.mockRestore();
+ setFailedSpy.mockRestore();
+ });
+
it('should have disabled action buttons', async () => {
await mockStore(RequestStatus.SUCCESSFUL);
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
@@ -573,7 +616,7 @@ describe('Videos page', () => {
const addFilesButton = screen.getAllByLabelText('file-input')[3];
await act(async () => {
userEvent.upload(addFilesButton, file);
- await executeThunk(addVideoFile(courseId, file), store.dispatch);
+ await executeThunk(addVideoFile(courseId, file, undefined, { current: [] }), store.dispatch);
});
const addStatus = store.getState().videos.addingStatus;
expect(addStatus).toEqual(RequestStatus.FAILED);
@@ -588,7 +631,7 @@ describe('Videos page', () => {
const addFilesButton = screen.getAllByLabelText('file-input')[3];
await act(async () => {
userEvent.upload(addFilesButton, file);
- await executeThunk(addVideoFile(courseId, file), store.dispatch);
+ await executeThunk(addVideoFile(courseId, file, undefined, { current: [] }), store.dispatch);
});
const addStatus = store.getState().videos.addingStatus;
expect(addStatus).toEqual(RequestStatus.FAILED);
@@ -623,7 +666,7 @@ describe('Videos page', () => {
const addFilesButton = screen.getAllByLabelText('file-input')[3];
await act(async () => {
userEvent.upload(addFilesButton, file);
- await executeThunk(addVideoFile(courseId, file), store.dispatch);
+ await executeThunk(addVideoFile(courseId, file, undefined, { current: [] }), store.dispatch);
});
const addStatus = store.getState().videos.addingStatus;
expect(addStatus).toEqual(RequestStatus.FAILED);
@@ -636,7 +679,7 @@ describe('Videos page', () => {
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- await waitFor(() => {
+ await waitFor(async () => {
axiosMock.onDelete(`${getCourseVideosApiUrl(courseId)}/mOckID1`).reply(404);
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
fireEvent.click(screen.getByTestId('open-delete-confirmation-button'));
@@ -644,11 +687,12 @@ describe('Videos page', () => {
fireEvent.click(screen.getByText(messages.deleteFileButtonLabel.defaultMessage));
expect(screen.queryByText('Delete mOckID1.mp4')).toBeNull();
-
- executeThunk(deleteVideoFile(courseId, 'mOckID1', 5), store.dispatch);
});
- const deleteStatus = store.getState().videos.deletingStatus;
- expect(deleteStatus).toEqual(RequestStatus.FAILED);
+ executeThunk(deleteVideoFile(courseId, 'mOckID1', 5), store.dispatch);
+ await waitFor(() => {
+ const deleteStatus = store.getState().videos.deletingStatus;
+ expect(deleteStatus).toEqual(RequestStatus.FAILED);
+ });
expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
@@ -669,8 +713,10 @@ describe('Videos page', () => {
video: { id: 'mOckID3', displayName: 'mOckID3' },
}), store.dispatch);
});
- const { usageStatus } = store.getState().videos;
- expect(usageStatus).toEqual(RequestStatus.FAILED);
+ await waitFor(() => {
+ const { usageStatus } = store.getState().videos;
+ expect(usageStatus).toEqual(RequestStatus.FAILED);
+ });
});
it('multiple video files fetch failure should show error', async () => {
diff --git a/src/files-and-videos/videos-page/data/constants.js b/src/files-and-videos/videos-page/data/constants.js
index ab666b37aa..5b2ff7e4ee 100644
--- a/src/files-and-videos/videos-page/data/constants.js
+++ b/src/files-and-videos/videos-page/data/constants.js
@@ -7,5 +7,6 @@ export const MIN_HEIGHT = 360;
export const ASPECT_RATIO = 16 / 9;
export const ASPECT_RATIO_ERROR_MARGIN = 0.1;
export const TRANSCRIPT_FAILURE_STATUSES = ['Transcript Failed', 'Partial Failure'];
-export const VIDEO_PROCESSING_STATUSES = ['Uploading', 'In Progress', 'Uploaded'];
+export const VIDEO_PROCESSING_STATUSES = ['In Progress', 'Uploaded']; // Don't add "Uploading" here. Otherwise interrupted uploads will be considered as processing.
export const VIDEO_SUCCESS_STATUSES = ['Ready', 'Imported'];
+export const VIDEO_FAILURE_STATUSES = ['Failed', 'Partial Failure', 'Uploading']; // 'Uploading' is added here to handle interrupted uploads.
diff --git a/src/files-and-videos/videos-page/data/thunks.js b/src/files-and-videos/videos-page/data/thunks.js
index 4d1e4cabec..426fdfbd79 100644
--- a/src/files-and-videos/videos-page/data/thunks.js
+++ b/src/files-and-videos/videos-page/data/thunks.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-param-reassign */
import { camelCase, isEmpty } from 'lodash';
import { getConfig, camelCaseObject } from '@edx/frontend-platform';
import { RequestStatus } from '../../../data/constants';
@@ -43,7 +44,9 @@ import { updateFileValues } from './utils';
export function fetchVideos(courseId) {
return async (dispatch) => {
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }),
+ );
try {
const { previousUploads, ...data } = await getVideos(courseId);
dispatch(setPageSettings({ ...data }));
@@ -51,110 +54,228 @@ export function fetchVideos(courseId) {
// If previous uploads are empty there is no need to add an empty model
// or loop through and empty list so automatically set loading to successful
if (isEmpty(previousUploads)) {
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }),
+ );
} else {
const parsedVideos = updateFileValues(previousUploads);
- const videoIds = parsedVideos.map(video => video.id);
+ const videoIds = parsedVideos.map((video) => video.id);
dispatch(addModels({ modelType: 'videos', models: parsedVideos }));
dispatch(setVideoIds({ videoIds }));
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.PARTIAL }));
- const allUsageLocations = await getAllUsagePaths({ courseId, videoIds });
- dispatch(updateModels({ modelType: 'videos', models: allUsageLocations }));
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.PARTIAL }),
+ );
+ const allUsageLocations = await getAllUsagePaths({
+ courseId,
+ videoIds,
+ });
+ dispatch(
+ updateModels({ modelType: 'videos', models: allUsageLocations }),
+ );
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }),
+ );
}
} catch (error) {
if (error.response && error.response.status === 403) {
dispatch(updateLoadingStatus({ status: RequestStatus.DENIED }));
} else {
- dispatch(updateErrors({ error: 'loading', message: 'Failed to load videos' }));
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({ error: 'loading', message: 'Failed to load videos' }),
+ );
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.FAILED }),
+ );
}
}
};
}
export function resetErrors({ errorType }) {
- return (dispatch) => { dispatch(clearErrors({ error: errorType })); };
+ return (dispatch) => {
+ dispatch(clearErrors({ error: errorType }));
+ };
}
export function updateVideoOrder(courseId, videoIds) {
return async (dispatch) => {
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }),
+ );
dispatch(setVideoIds({ videoIds }));
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }),
+ );
};
}
export function deleteVideoFile(courseId, id) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'delete',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
await deleteVideo(courseId, id);
dispatch(deleteVideoSuccess({ videoId: id }));
dispatch(removeModel({ modelType: 'videos', id }));
- dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'delete',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
- dispatch(updateErrors({ error: 'delete', message: `Failed to delete file id ${id}.` }));
- dispatch(updateEditStatus({ editType: 'delete', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'delete',
+ message: `Failed to delete file id ${id}.`,
+ }),
+ );
+ dispatch(
+ updateEditStatus({ editType: 'delete', status: RequestStatus.FAILED }),
+ );
}
};
}
-export function addVideoFile(courseId, file, videoIds) {
+export function markVideoUploadsInProgressAsFailed({ uploadingIdsRef, courseId }) {
+ return (dispatch) => {
+ uploadingIdsRef.current.forEach((edxVideoId) => {
+ try {
+ sendVideoUploadStatus(
+ courseId,
+ edxVideoId || '',
+ 'Upload failed',
+ 'upload_failed',
+ );
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(`Failed to send "Failed" upload status for ${edxVideoId} onbeforeunload`);
+ }
+ dispatch(
+ updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }),
+ );
+ });
+ // eslint-disable-next-line no-param-reassign
+ uploadingIdsRef.current = [];
+ };
+}
+
+export function addVideoFile(
+ courseId,
+ file,
+ videoIds,
+ uploadingIdsRef,
+) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.IN_PROGRESS }));
- let edxVideoId; let uploadUrl;
+ dispatch(
+ updateEditStatus({ editType: 'add', status: RequestStatus.IN_PROGRESS }),
+ );
+
+ let edxVideoId;
+ let uploadUrl;
try {
const createUrlResponse = await addVideo(courseId, file);
// eslint-disable-next-line
- console.log(`Post Response: ${createUrlResponse}`);
+ console.log(`Post Response: ${JSON.stringify(createUrlResponse)}`);
if (createUrlResponse.status < 200 || createUrlResponse.status >= 300) {
dispatch(failAddVideo({ fileName: file.name }));
}
// eslint-disable-next-line prefer-destructuring
- [{ edxVideoId, uploadUrl }] = camelCaseObject(createUrlResponse.data).files;
+ [{ edxVideoId, uploadUrl }] = camelCaseObject(
+ createUrlResponse.data,
+ ).files;
} catch (error) {
dispatch(failAddVideo({ fileName: file.name }));
+ updateEditStatus({ editType: 'add', status: RequestStatus.FAILED });
return;
}
try {
+ uploadingIdsRef.current = [...uploadingIdsRef.current, edxVideoId];
+
const putToServerResponse = await uploadVideo(uploadUrl, file);
- if (putToServerResponse.status < 200 || putToServerResponse.status >= 300) {
- throw new ServerError('Server responded with an error status', putToServerResponse.status);
+ if (
+ putToServerResponse.status < 200
+ || putToServerResponse.status >= 300
+ ) {
+ throw new ServerError(
+ 'Server responded with an error status',
+ putToServerResponse.status,
+ );
} else {
- await sendVideoUploadStatus(courseId, edxVideoId, 'Upload completed', 'upload_completed');
+ await sendVideoUploadStatus(
+ courseId,
+ edxVideoId,
+ 'Upload completed',
+ 'upload_completed',
+ );
}
+ uploadingIdsRef.current = uploadingIdsRef.current.filter(
+ (id) => id !== edxVideoId,
+ );
} catch (error) {
if (error.response && error.response.status === 413) {
const message = error.response.data.error;
dispatch(updateErrors({ error: 'add', message }));
} else {
- dispatch(updateErrors({ error: 'add', message: `Failed to upload ${file.name}.` }));
+ dispatch(
+ updateErrors({
+ error: 'add',
+ message: `Failed to upload ${file.name}.`,
+ }),
+ );
}
- await sendVideoUploadStatus(courseId, edxVideoId || '', 'Upload failed', 'upload_failed');
- dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }));
+ await sendVideoUploadStatus(
+ courseId,
+ edxVideoId || '',
+ 'Upload failed',
+ 'upload_failed',
+ );
+ dispatch(
+ updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }),
+ );
+ uploadingIdsRef.current = uploadingIdsRef.current.filter(
+ (id) => id !== edxVideoId,
+ );
+ // return;
}
try {
const { videos } = await fetchVideoList(courseId);
- const newVideos = videos.filter(video => !videoIds.includes(video.edxVideoId));
- const newVideoIds = newVideos.map(video => video.edxVideoId);
+ const newVideos = videos.filter(
+ (video) => !videoIds.includes(video.edxVideoId),
+ );
+ const newVideoIds = newVideos.map((video) => video.edxVideoId);
const parsedVideos = updateFileValues(newVideos, true);
dispatch(addModels({ modelType: 'videos', models: parsedVideos }));
dispatch(setVideoIds({ videoIds: newVideoIds.concat(videoIds) }));
} catch (error) {
- dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }));
+ dispatch(
+ updateEditStatus({ editType: 'add', status: RequestStatus.FAILED }),
+ );
// eslint-disable-next-line
console.error(`fetchVideoList failed with message: ${error.message}`);
- dispatch(updateErrors({ error: 'add', message: 'Failed to load videos' }));
+ dispatch(
+ updateErrors({ error: 'add', message: 'Failed to load videos' }),
+ );
return;
}
- dispatch(updateEditStatus({ editType: 'add', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({ editType: 'add', status: RequestStatus.SUCCESSFUL }),
+ );
};
}
export function addVideoThumbnail({ file, videoId, courseId }) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'thumbnail', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'thumbnail',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
dispatch(resetErrors({ errorType: 'thumbnail' }));
try {
const { imageUrl } = await addThumbnail({ courseId, videoId, file });
@@ -162,22 +283,39 @@ export function addVideoThumbnail({ file, videoId, courseId }) {
if (thumbnail.startsWith('/')) {
thumbnail = `${getConfig().STUDIO_BASE_URL}${imageUrl}`;
}
- dispatch(updateModel({
- modelType: 'videos',
- model: {
- id: videoId,
- thumbnail,
- },
- }));
- dispatch(updateEditStatus({ editType: 'thumbnail', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateModel({
+ modelType: 'videos',
+ model: {
+ id: videoId,
+ thumbnail,
+ },
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'thumbnail',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
if (error.response?.data?.error) {
const message = error.response.data.error;
dispatch(updateErrors({ error: 'thumbnail', message }));
} else {
- dispatch(updateErrors({ error: 'thumbnail', message: `Failed to add thumbnail for video id ${videoId}.` }));
+ dispatch(
+ updateErrors({
+ error: 'thumbnail',
+ message: `Failed to add thumbnail for video id ${videoId}.`,
+ }),
+ );
}
- dispatch(updateEditStatus({ editType: 'thumbnail', status: RequestStatus.FAILED }));
+ dispatch(
+ updateEditStatus({
+ editType: 'thumbnail',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
@@ -189,7 +327,12 @@ export function deleteVideoTranscript({
apiUrl,
}) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
await deleteTranscript({
@@ -197,22 +340,41 @@ export function deleteVideoTranscript({
language,
apiUrl,
});
- const updatedTranscripts = transcripts.filter(transcript => transcript !== language);
+ const updatedTranscripts = transcripts.filter(
+ (transcript) => transcript !== language,
+ );
const transcriptStatus = updatedTranscripts?.length > 0 ? 'transcribed' : 'notTranscribed';
- dispatch(updateModel({
- modelType: 'videos',
- model: {
- id: videoId,
- transcripts: updatedTranscripts,
- transcriptStatus,
- },
- }));
+ dispatch(
+ updateModel({
+ modelType: 'videos',
+ model: {
+ id: videoId,
+ transcripts: updatedTranscripts,
+ transcriptStatus,
+ },
+ }),
+ );
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
- dispatch(updateErrors({ error: 'transcript', message: `Failed to delete ${language} transcript.` }));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'transcript',
+ message: `Failed to delete ${language} transcript.`,
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
@@ -224,7 +386,12 @@ export function downloadVideoTranscript({
apiUrl,
}) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
await downloadTranscript({
@@ -233,10 +400,25 @@ export function downloadVideoTranscript({
apiUrl,
filename,
});
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
- dispatch(updateErrors({ error: 'transcript', message: `Failed to download ${filename}.` }));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'transcript',
+ message: `Failed to download ${filename}.`,
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
@@ -250,7 +432,12 @@ export function uploadVideoTranscript({
transcripts,
}) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
const isReplacement = !isEmpty(language);
try {
@@ -263,7 +450,9 @@ export function uploadVideoTranscript({
});
let updatedTranscripts = transcripts;
if (isReplacement) {
- const removeTranscript = transcripts.filter(transcript => transcript !== language);
+ const removeTranscript = transcripts.filter(
+ (transcript) => transcript !== language,
+ );
updatedTranscripts = [...removeTranscript, newLanguage];
} else {
updatedTranscripts = [...transcripts, newLanguage];
@@ -271,118 +460,246 @@ export function uploadVideoTranscript({
const transcriptStatus = updatedTranscripts?.length > 0 ? 'transcribed' : 'notTranscribed';
- dispatch(updateModel({
- modelType: 'videos',
- model: {
- id: videoId,
- transcripts: updatedTranscripts,
- transcriptStatus,
- },
- }));
+ dispatch(
+ updateModel({
+ modelType: 'videos',
+ model: {
+ id: videoId,
+ transcripts: updatedTranscripts,
+ transcriptStatus,
+ },
+ }),
+ );
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
if (error.response?.data?.error) {
const message = error.response.data.error;
dispatch(updateErrors({ error: 'transcript', message }));
} else {
- const message = isReplacement ? `Failed to replace ${language} with ${newLanguage}.` : `Failed to add ${newLanguage}.`;
+ const message = isReplacement
+ ? `Failed to replace ${language} with ${newLanguage}.`
+ : `Failed to add ${newLanguage}.`;
dispatch(updateErrors({ error: 'transcript', message }));
}
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.FAILED }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
export function getUsagePaths({ video, courseId }) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'usageMetrics',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
- const { usageLocations } = await getVideoUsagePaths({ videoId: video.id, courseId });
+ const { usageLocations } = await getVideoUsagePaths({
+ videoId: video.id,
+ courseId,
+ });
const activeStatus = usageLocations?.length > 0 ? 'active' : 'inactive';
- dispatch(updateModel({
- modelType: 'videos',
- model: {
- id: video.id,
- usageLocations,
- activeStatus,
- },
- }));
- dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateModel({
+ modelType: 'videos',
+ model: {
+ id: video.id,
+ usageLocations,
+ activeStatus,
+ },
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'usageMetrics',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
- dispatch(updateErrors({ error: 'usageMetrics', message: `Failed to get usage metrics for ${video.displayName}.` }));
- dispatch(updateEditStatus({ editType: 'usageMetrics', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'usageMetrics',
+ message: `Failed to get usage metrics for ${video.displayName}.`,
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'usageMetrics',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
export function fetchVideoDownload({ selectedRows, courseId }) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'download',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
const errors = await getDownload(selectedRows, courseId);
- dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'download',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
if (!isEmpty(errors)) {
- errors.forEach(error => {
+ errors.forEach((error) => {
dispatch(updateErrors({ error: 'download', message: error }));
});
- dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.FAILED }));
+ dispatch(
+ updateEditStatus({
+ editType: 'download',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
} catch (error) {
- dispatch(updateErrors({ error: 'download', message: 'Failed to download zip file of videos.' }));
- dispatch(updateEditStatus({ editType: 'download', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'download',
+ message: 'Failed to download zip file of videos.',
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'download',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
export function clearAutomatedTranscript({ courseId }) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
await deleteTranscriptPreferences(courseId);
dispatch(updateTranscriptPreferenceSuccess({ modified: new Date() }));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
- dispatch(updateErrors({ error: 'transcript', message: 'Failed to update order transcripts settings.' }));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'transcript',
+ message: 'Failed to update order transcripts settings.',
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
export function updateTranscriptCredentials({ courseId, data }) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
await setTranscriptCredentials(courseId, data);
- dispatch(updateTranscriptCredentialsSuccess({ provider: camelCase(data.provider) }));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateTranscriptCredentialsSuccess({
+ provider: camelCase(data.provider),
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
- dispatch(updateErrors({ error: 'transcript', message: `Failed to update ${data.provider} credentials.` }));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.FAILED }));
+ dispatch(
+ updateErrors({
+ error: 'transcript',
+ message: `Failed to update ${data.provider} credentials.`,
+ }),
+ );
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
export function updateTranscriptPreference({ courseId, data }) {
return async (dispatch) => {
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.IN_PROGRESS }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.IN_PROGRESS,
+ }),
+ );
try {
const preferences = await setTranscriptPreferences(courseId, data);
dispatch(updateTranscriptPreferenceSuccess(preferences));
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.SUCCESSFUL }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.SUCCESSFUL,
+ }),
+ );
} catch (error) {
if (error.response?.data?.error) {
const message = error.response.data.error;
dispatch(updateErrors({ error: 'transcript', message }));
} else {
- dispatch(updateErrors({ error: 'transcript', message: `Failed to update ${data.provider} transcripts settings.` }));
+ dispatch(
+ updateErrors({
+ error: 'transcript',
+ message: `Failed to update ${data.provider} transcripts settings.`,
+ }),
+ );
}
- dispatch(updateEditStatus({ editType: 'transcript', status: RequestStatus.FAILED }));
+ dispatch(
+ updateEditStatus({
+ editType: 'transcript',
+ status: RequestStatus.FAILED,
+ }),
+ );
}
};
}
diff --git a/src/files-and-videos/videos-page/data/thunks.test.js b/src/files-and-videos/videos-page/data/thunks.test.js
index 20c61d6ae1..7d0b3c7acc 100644
--- a/src/files-and-videos/videos-page/data/thunks.test.js
+++ b/src/files-and-videos/videos-page/data/thunks.test.js
@@ -18,7 +18,7 @@ describe('addVideoFile', () => {
status: 404,
});
- await addVideoFile(courseId, mockFile)(dispatch, getState);
+ await addVideoFile(courseId, mockFile, undefined, { current: [] })(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
payload: {
@@ -43,7 +43,7 @@ describe('addVideoFile', () => {
jest.spyOn(api, 'uploadVideo').mockResolvedValue({
status: 404,
});
- await addVideoFile(courseId, mockFile)(dispatch, getState);
+ await addVideoFile(courseId, mockFile, undefined, { current: [] })(dispatch, getState);
expect(videoStatusMock).toHaveBeenCalledWith(courseId, mockEdxVideoId, 'Upload failed', 'upload_failed');
expect(dispatch).toHaveBeenCalledWith({
payload: {
@@ -70,7 +70,7 @@ describe('addVideoFile', () => {
jest.spyOn(api, 'uploadVideo').mockResolvedValue({
status: 200,
});
- await addVideoFile(courseId, mockFile)(dispatch, getState);
+ await addVideoFile(courseId, mockFile, undefined, { current: [] })(dispatch, getState);
expect(videoStatusMock).toHaveBeenCalledWith(courseId, mockEdxVideoId, 'Upload completed', 'upload_completed');
});
});
diff --git a/src/files-and-videos/videos-page/messages.js b/src/files-and-videos/videos-page/messages.js
index 774dfe2f0b..6dd3f16b8c 100644
--- a/src/files-and-videos/videos-page/messages.js
+++ b/src/files-and-videos/videos-page/messages.js
@@ -41,6 +41,10 @@ const messages = defineMessages({
id: 'course-authoring.files-and-videos.add-video-progress-bar.progress-bar.label',
defaultMessage: 'Video upload progress:',
},
+ videoUploadAlertLabel: {
+ id: 'course-authoring.files-and-videos.video-upload-alert',
+ defaultMessage: 'Upload in progress. Please wait for the upload to complete before navigating away from this page.',
+ },
});
export default messages;