Fix video upload failures (#952)
This commit is contained in:
@@ -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=''
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<Badge variant="light">
|
||||
{status}
|
||||
{isFailed ? failedText : status}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = ({
|
||||
<div className="status-badge">
|
||||
{!isUploaded && (
|
||||
<Badge variant="light">
|
||||
{status}
|
||||
{!isFailed ? status : failedMessage}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
Button,
|
||||
CheckboxFilter,
|
||||
Container,
|
||||
Alert,
|
||||
Spinner,
|
||||
} from '@openedx/paragon';
|
||||
import Placeholder from '@edx/frontend-lib-content-components';
|
||||
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
fetchVideoDownload,
|
||||
fetchVideos,
|
||||
getUsagePaths,
|
||||
markVideoUploadsInProgressAsFailed,
|
||||
resetErrors,
|
||||
updateVideoOrder,
|
||||
} from './data/thunks';
|
||||
@@ -50,9 +53,16 @@ const VideosPage = ({
|
||||
intl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isTranscriptSettingsOpen, openTranscriptSettings, closeTranscriptSettings] = useToggle(false);
|
||||
const [
|
||||
isTranscriptSettingsOpen,
|
||||
openTranscriptSettings,
|
||||
closeTranscriptSettings,
|
||||
] = useToggle(false);
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||
document.title = getPageHeadTitle(
|
||||
courseDetails?.name,
|
||||
intl.formatMessage(messages.heading),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchVideos(courseId));
|
||||
@@ -68,7 +78,16 @@ const VideosPage = ({
|
||||
usageStatus: usagePathStatus,
|
||||
errors: errorMessages,
|
||||
pageSettings,
|
||||
} = useSelector(state => state.videos);
|
||||
} = useSelector((state) => state.videos);
|
||||
|
||||
const uploadingIdsRef = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
window.onbeforeunload = () => {
|
||||
dispatch(markVideoUploadsInProgressAsFailed({ uploadingIdsRef, courseId }));
|
||||
return undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isVideoTranscriptEnabled,
|
||||
@@ -78,12 +97,14 @@ const VideosPage = ({
|
||||
videoImageSettings,
|
||||
} = pageSettings;
|
||||
|
||||
const supportedFileFormats = { 'video/*': videoSupportedFileFormats || FILES_AND_UPLOAD_TYPE_FILTERS.video };
|
||||
const supportedFileFormats = {
|
||||
'video/*': videoSupportedFileFormats || FILES_AND_UPLOAD_TYPE_FILTERS.video,
|
||||
};
|
||||
|
||||
const handleErrorReset = (error) => dispatch(resetErrors(error));
|
||||
const handleAddFile = (files) => {
|
||||
handleErrorReset({ errorType: 'add' });
|
||||
files.forEach(file => dispatch(addVideoFile(courseId, file, videoIds)));
|
||||
files.forEach((file) => dispatch(addVideoFile(courseId, file, videoIds, uploadingIdsRef)));
|
||||
};
|
||||
const handleDeleteFile = (id) => dispatch(deleteVideoFile(courseId, id));
|
||||
const handleDownloadFile = (selectedRows) => dispatch(fetchVideoDownload({ selectedRows, courseId }));
|
||||
@@ -128,8 +149,14 @@ const VideosPage = ({
|
||||
Filter: CheckboxFilter,
|
||||
filter: 'exactTextCase',
|
||||
filterChoices: [
|
||||
{ name: intl.formatMessage(messages.transcribedCheckboxLabel), value: 'transcribed' },
|
||||
{ name: intl.formatMessage(messages.notTranscribedCheckboxLabel), value: 'notTranscribed' },
|
||||
{
|
||||
name: intl.formatMessage(messages.transcribedCheckboxLabel),
|
||||
value: 'transcribed',
|
||||
},
|
||||
{
|
||||
name: intl.formatMessage(messages.notTranscribedCheckboxLabel),
|
||||
value: 'notTranscribed',
|
||||
},
|
||||
],
|
||||
};
|
||||
const activeColumn = {
|
||||
@@ -201,6 +228,11 @@ const VideosPage = ({
|
||||
updateFileStatus={updateVideoStatus}
|
||||
loadingStatus={loadingStatus}
|
||||
/>
|
||||
<Alert variant="warning" show={addVideoStatus === RequestStatus.IN_PROGRESS}>
|
||||
<div className="video-upload-warning-text"><Spinner animation="border" variant="warning" className="video-upload-spinner mr-3" screenReaderText="loading" />
|
||||
<p className="d-inline"><FormattedMessage {...messages.videoUploadAlertLabel} /></p>
|
||||
</div>
|
||||
</Alert>
|
||||
<ActionRow>
|
||||
<div className="h2">
|
||||
<FormattedMessage {...messages.heading} />
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user