diff --git a/src/course-outline/page-alerts/PageAlerts.jsx b/src/course-outline/page-alerts/PageAlerts.jsx
index 3ce5f0567c..03b9a6a79b 100644
--- a/src/course-outline/page-alerts/PageAlerts.jsx
+++ b/src/course-outline/page-alerts/PageAlerts.jsx
@@ -1,3 +1,4 @@
+import CourseOutlinePageAlertsSlot from 'CourseAuthoring/plugin-slots/CourseOutlinePageAlertsSlot';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
@@ -413,6 +414,7 @@ const PageAlerts = ({
{errorFilesPasteAlert()}
{conflictingFilesPasteAlert()}
{newFilesPasteAlert()}
+
>
);
};
diff --git a/src/course-outline/page-alerts/PageAlerts.test.jsx b/src/course-outline/page-alerts/PageAlerts.test.jsx
index 21d2f74916..a56a405422 100644
--- a/src/course-outline/page-alerts/PageAlerts.test.jsx
+++ b/src/course-outline/page-alerts/PageAlerts.test.jsx
@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
-import { act, render, fireEvent } from '@testing-library/react';
+import { act, render, fireEvent, waitFor } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
@@ -103,9 +103,11 @@ describe('', () => {
const discussionAlertDismissKey = `discussionAlertDismissed-${pageAlertsData.courseId}`;
expect(localStorage.getItem(discussionAlertDismissKey)).toBe('true');
- const feedbackLink = queryByText(messages.discussionNotificationFeedback.defaultMessage);
- expect(feedbackLink).toBeInTheDocument();
- expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
+ await waitFor(() => {
+ const feedbackLink = queryByText(messages.discussionNotificationFeedback.defaultMessage);
+ expect(feedbackLink).toBeInTheDocument();
+ expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
+ });
});
it('renders deprecation warning alerts', async () => {
diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.jsx
index 6c3ef01b43..50ab4024b3 100644
--- a/src/files-and-videos/files-page/FilesPage.jsx
+++ b/src/files-and-videos/files-page/FilesPage.jsx
@@ -1,173 +1,39 @@
-import React, { useEffect } from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Container } from '@openedx/paragon';
+import CourseFilesSlot from 'CourseAuthoring/plugin-slots/CourseFilesSlot';
import PropTypes from 'prop-types';
+import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
-import { CheckboxFilter, Container } from '@openedx/paragon';
-import Placeholder from '@edx/frontend-lib-content-components';
+import Placeholder from '@edx/frontend-lib-content-components';
import { RequestStatus } from '../../data/constants';
-import { useModels, useModel } from '../../generic/model-store';
-import {
- addAssetFile,
- deleteAssetFile,
- fetchAssets,
- updateAssetLock,
- fetchAssetDownload,
- getUsagePaths,
- resetErrors,
- updateAssetOrder,
- validateAssetFiles,
-} from './data/thunks';
-import messages from './messages';
-import FilesPageProvider from './FilesPageProvider';
+import { useModel } from '../../generic/model-store';
import getPageHeadTitle from '../../generic/utils';
-import {
- AccessColumn,
- ActiveColumn,
- EditFileErrors,
- FileTable,
- ThumbnailColumn,
-} from '../generic';
-import { getFileSizeToClosestByte } from '../../utils';
-import FileThumbnail from './FileThumbnail';
-import FileInfoModalSidebar from './FileInfoModalSidebar';
-import FileValidationModal from './FileValidationModal';
+import { EditFileErrors } from '../generic';
+import { fetchAssets, resetErrors } from './data/thunks';
+import FilesPageProvider from './FilesPageProvider';
+import messages from './messages';
const FilesPage = ({
courseId,
- // injected
- intl,
}) => {
const dispatch = useDispatch();
+ const intl = useIntl();
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
-
- useEffect(() => {
- dispatch(fetchAssets(courseId));
- }, [courseId]);
-
const {
- assetIds,
loadingStatus,
addingStatus: addAssetStatus,
deletingStatus: deleteAssetStatus,
updatingStatus: updateAssetStatus,
- usageStatus: usagePathStatus,
errors: errorMessages,
} = useSelector(state => state.assets);
- const handleErrorReset = (error) => dispatch(resetErrors(error));
- const handleDeleteFile = (id) => dispatch(deleteAssetFile(courseId, id));
- const handleDownloadFile = (selectedRows) => dispatch(fetchAssetDownload({ selectedRows, courseId }));
- const handleAddFile = (files) => {
- handleErrorReset({ errorType: 'add' });
- dispatch(validateAssetFiles(courseId, files));
- };
- const handleFileOverwrite = (close, files) => {
- Object.values(files).forEach(file => dispatch(addAssetFile(courseId, file, true)));
- close();
- };
- const handleLockFile = (fileId, locked) => {
- handleErrorReset({ errorType: 'lock' });
- dispatch(updateAssetLock({ courseId, assetId: fileId, locked }));
- };
- const handleUsagePaths = (asset) => dispatch(getUsagePaths({ asset, courseId }));
- const handleFileOrder = ({ newFileIdOrder, sortType }) => {
- dispatch(updateAssetOrder(courseId, newFileIdOrder, sortType));
- };
-
- const thumbnailPreview = (props) => FileThumbnail(props);
- const infoModalSidebar = (asset) => FileInfoModalSidebar({
- asset,
- handleLockedAsset: handleLockFile,
- });
-
- const assets = useModels('assets', assetIds);
- const data = {
- fileIds: assetIds,
- loadingStatus,
- usagePathStatus,
- usageErrorMessages: errorMessages.usageMetrics,
- fileType: 'file',
- };
- const maxFileSize = 20 * 1048576;
-
- const activeColumn = {
- id: 'activeStatus',
- Header: 'Active',
- accessor: 'activeStatus',
- Cell: ({ row }) => ActiveColumn({ row, pageLoadStatus: loadingStatus }),
- Filter: CheckboxFilter,
- filter: 'exactTextCase',
- filterChoices: [
- { name: intl.formatMessage(messages.activeCheckboxLabel), value: 'active' },
- { name: intl.formatMessage(messages.inactiveCheckboxLabel), value: 'inactive' },
- ],
- };
- const accessColumn = {
- id: 'lockStatus',
- Header: 'Access',
- accessor: 'lockStatus',
- Cell: ({ row }) => AccessColumn({ row }),
- Filter: CheckboxFilter,
- filterChoices: [
- { name: intl.formatMessage(messages.lockedCheckboxLabel), value: 'locked' },
- { name: intl.formatMessage(messages.publicCheckboxLabel), value: 'public' },
- ],
- };
- const thumbnailColumn = {
- id: 'thumbnail',
- Header: '',
- Cell: ({ row }) => ThumbnailColumn({ row, thumbnailPreview }),
- };
- const fileSizeColumn = {
- id: 'fileSize',
- Header: 'File size',
- accessor: 'fileSize',
- Cell: ({ row }) => {
- const { fileSize } = row.original;
- return getFileSizeToClosestByte(fileSize);
- },
- };
+ useEffect(() => {
+ dispatch(fetchAssets(courseId));
+ }, [courseId]);
- const tableColumns = [
- { ...thumbnailColumn },
- {
- Header: 'File name',
- accessor: 'displayName',
- },
- { ...fileSizeColumn },
- {
- Header: 'Type',
- accessor: 'wrapperType',
- Filter: CheckboxFilter,
- filter: 'includesValue',
- filterChoices: [
- {
- name: intl.formatMessage(messages.codeCheckboxLabel),
- value: 'code',
- },
- {
- name: intl.formatMessage(messages.imageCheckboxLabel),
- value: 'image',
- },
- {
- name: intl.formatMessage(messages.documentCheckboxLabel),
- value: 'document',
- },
- {
- name: intl.formatMessage(messages.audioCheckboxLabel),
- value: 'audio',
- },
- {
- name: intl.formatMessage(messages.otherCheckboxLabel),
- value: 'other',
- },
- ],
- },
- { ...activeColumn },
- { ...accessColumn },
- ];
+ const handleErrorReset = (error) => dispatch(resetErrors(error));
if (loadingStatus === RequestStatus.DENIED) {
return (
@@ -189,30 +55,10 @@ const FilesPage = ({
loadingStatus={loadingStatus}
/>
-
+ {intl.formatMessage(messages.heading)}
{loadingStatus !== RequestStatus.FAILED && (
- <>
-
-
- >
+
)}
@@ -221,8 +67,6 @@ const FilesPage = ({
FilesPage.propTypes = {
courseId: PropTypes.string.isRequired,
- // injected
- intl: intlShape.isRequired,
};
-export default injectIntl(FilesPage);
+export default FilesPage;
diff --git a/src/files-and-videos/generic/EditFileErrors.jsx b/src/files-and-videos/generic/EditFileErrors.jsx
index 4a2e1e9098..928ade8e6e 100644
--- a/src/files-and-videos/generic/EditFileErrors.jsx
+++ b/src/files-and-videos/generic/EditFileErrors.jsx
@@ -1,6 +1,7 @@
+import EditFileErrorAlertsSlot from 'CourseAuthoring/plugin-slots/EditFileErrorAlertsSlot';
import React from 'react';
import PropTypes from 'prop-types';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { ErrorAlert } from '@edx/frontend-lib-content-components';
import { RequestStatus } from '../../data/constants';
import messages from './messages';
@@ -12,68 +13,70 @@ const EditFileErrors = ({
deleteFileStatus,
updateFileStatus,
loadingStatus,
- // injected
- intl,
-}) => (
- <>
- resetErrors({ errorType: 'loading' })}
- isError={loadingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.PARTIAL_FAILURE}
- >
- {intl.formatMessage(messages.errorAlertMessage, { message: errorMessages.loading })}
-
- resetErrors({ errorType: 'add' })}
- isError={addFileStatus === RequestStatus.FAILED}
- >
-
- {errorMessages.add.map(message => (
- -
- {intl.formatMessage(messages.errorAlertMessage, { message })}
-
- ))}
-
-
- resetErrors({ errorType: 'delete' })}
- isError={deleteFileStatus === RequestStatus.FAILED}
- >
-
- {errorMessages.delete.map(message => (
- -
- {intl.formatMessage(messages.errorAlertMessage, { message })}
-
- ))}
-
-
- resetErrors({ errorType: 'update' })}
- isError={updateFileStatus === RequestStatus.FAILED}
- >
-
- {errorMessages.lock?.map(message => (
- -
- {intl.formatMessage(messages.errorAlertMessage, { message })}
-
- ))}
- {errorMessages.download.map(message => (
- -
- {intl.formatMessage(messages.errorAlertMessage, { message })}
-
- ))}
- {errorMessages.thumbnail?.map(message => (
- -
- {intl.formatMessage(messages.errorAlertMessage, { message })}
-
- ))}
-
-
- >
-);
+}) => {
+ const intl = useIntl();
+ return (
+ <>
+ resetErrors({ errorType: 'loading' })}
+ isError={loadingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.PARTIAL_FAILURE}
+ >
+ {intl.formatMessage(messages.errorAlertMessage, { message: errorMessages.loading })}
+
+ resetErrors({ errorType: 'add' })}
+ isError={addFileStatus === RequestStatus.FAILED}
+ >
+
+ {errorMessages.add.map(message => (
+ -
+ {intl.formatMessage(messages.errorAlertMessage, { message })}
+
+ ))}
+
+
+ resetErrors({ errorType: 'delete' })}
+ isError={deleteFileStatus === RequestStatus.FAILED}
+ >
+
+ {errorMessages.delete.map(message => (
+ -
+ {intl.formatMessage(messages.errorAlertMessage, { message })}
+
+ ))}
+
+
+ resetErrors({ errorType: 'update' })}
+ isError={updateFileStatus === RequestStatus.FAILED}
+ >
+
+ {errorMessages.lock?.map(message => (
+ -
+ {intl.formatMessage(messages.errorAlertMessage, { message })}
+
+ ))}
+ {errorMessages.download.map(message => (
+ -
+ {intl.formatMessage(messages.errorAlertMessage, { message })}
+
+ ))}
+ {errorMessages.thumbnail?.map(message => (
+ -
+ {intl.formatMessage(messages.errorAlertMessage, { message })}
+
+ ))}
+
+
+
+ >
+ );
+};
EditFileErrors.propTypes = {
resetErrors: PropTypes.func.isRequired,
@@ -89,8 +92,6 @@ EditFileErrors.propTypes = {
deleteFileStatus: PropTypes.string.isRequired,
updateFileStatus: PropTypes.string.isRequired,
loadingStatus: PropTypes.string.isRequired,
- // injected
- intl: intlShape.isRequired,
};
-export default injectIntl(EditFileErrors);
+export default EditFileErrors;
diff --git a/src/files-and-videos/generic/FileTable.jsx b/src/files-and-videos/generic/FileTable.jsx
index 9e82ab0502..f80e846ba7 100644
--- a/src/files-and-videos/generic/FileTable.jsx
+++ b/src/files-and-videos/generic/FileTable.jsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import isEmpty from 'lodash/isEmpty';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import {
CardView,
DataTable,
@@ -41,9 +41,8 @@ const FileTable = ({
maxFileSize,
thumbnailPreview,
infoModalSidebar,
- // injected
- intl,
}) => {
+ const intl = useIntl();
const defaultVal = 'card';
const pageCount = Math.ceil(files.length / 50);
const columnSizes = {
@@ -312,8 +311,6 @@ FileTable.propTypes = {
maxFileSize: PropTypes.number.isRequired,
thumbnailPreview: PropTypes.func.isRequired,
infoModalSidebar: PropTypes.func.isRequired,
- // injected
- intl: intlShape.isRequired,
};
FileTable.defaultProps = {
@@ -321,4 +318,4 @@ FileTable.defaultProps = {
handleLockFile: () => {},
};
-export default injectIntl(FileTable);
+export default FileTable;
diff --git a/src/files-and-videos/videos-page/VideosPage.jsx b/src/files-and-videos/videos-page/VideosPage.jsx
index 18e79d40b6..8dc08668fc 100644
--- a/src/files-and-videos/videos-page/VideosPage.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.jsx
@@ -1,217 +1,30 @@
-import React, { useEffect, useRef } from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Alert, Container, Spinner } from '@openedx/paragon';
+import CourseVideosSlot from 'CourseAuthoring/plugin-slots/CourseVideosSlot';
import PropTypes from 'prop-types';
+import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import {
- injectIntl,
- FormattedMessage,
- intlShape,
-} from '@edx/frontend-platform/i18n';
-import {
- useToggle,
- ActionRow,
- Button,
- CheckboxFilter,
- Container,
- Alert,
- Spinner,
-} from '@openedx/paragon';
import Placeholder from '@edx/frontend-lib-content-components';
-
import { RequestStatus } from '../../data/constants';
-import { useModels, useModel } from '../../generic/model-store';
-import {
- addVideoFile,
- addVideoThumbnail,
- deleteVideoFile,
- fetchVideoDownload,
- fetchVideos,
- getUsagePaths,
- markVideoUploadsInProgressAsFailed,
- resetErrors,
- updateVideoOrder,
-} from './data/thunks';
+
+import { EditFileErrors } from '../generic';
+import { resetErrors } from './data/thunks';
import messages from './messages';
import VideosPageProvider from './VideosPageProvider';
-import getPageHeadTitle from '../../generic/utils';
-import {
- ActiveColumn,
- EditFileErrors,
- FileTable,
- StatusColumn,
- ThumbnailColumn,
- TranscriptColumn,
-} from '../generic';
-import TranscriptSettings from './transcript-settings';
-import VideoThumbnail from './VideoThumbnail';
-import { getFormattedDuration, resampleFile } from './data/utils';
-import FILES_AND_UPLOAD_TYPE_FILTERS from '../generic/constants';
-import VideoInfoModalSidebar from './info-sidebar';
const VideosPage = ({
courseId,
- // injected
- intl,
}) => {
+ const intl = useIntl();
const dispatch = useDispatch();
- const [
- isTranscriptSettingsOpen,
- openTranscriptSettings,
- closeTranscriptSettings,
- ] = useToggle(false);
- const courseDetails = useModel('courseDetails', courseId);
- document.title = getPageHeadTitle(
- courseDetails?.name,
- intl.formatMessage(messages.heading),
- );
-
- useEffect(() => {
- dispatch(fetchVideos(courseId));
- }, [courseId]);
-
const {
- videoIds,
loadingStatus,
- transcriptStatus,
addingStatus: addVideoStatus,
deletingStatus: deleteVideoStatus,
updatingStatus: updateVideoStatus,
- usageStatus: usagePathStatus,
errors: errorMessages,
- pageSettings,
} = useSelector((state) => state.videos);
-
- const uploadingIdsRef = useRef([]);
-
- useEffect(() => {
- window.onbeforeunload = () => {
- dispatch(markVideoUploadsInProgressAsFailed({ uploadingIdsRef, courseId }));
- if (addVideoStatus === RequestStatus.IN_PROGRESS) {
- return '';
- }
- return undefined;
- };
- }, [addVideoStatus]);
-
- const {
- isVideoTranscriptEnabled,
- encodingsDownloadUrl,
- videoUploadMaxFileSize,
- videoSupportedFileFormats,
- videoImageSettings,
- } = pageSettings;
-
- 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, uploadingIdsRef)));
- };
- const handleDeleteFile = (id) => dispatch(deleteVideoFile(courseId, id));
- const handleDownloadFile = (selectedRows) => dispatch(fetchVideoDownload({ selectedRows, courseId }));
- const handleUsagePaths = (video) => dispatch(getUsagePaths({ video, courseId }));
- const handleFileOrder = ({ newFileIdOrder, sortType }) => {
- dispatch(updateVideoOrder(courseId, newFileIdOrder, sortType));
- };
- const handleAddThumbnail = (file, videoId) => resampleFile({
- file,
- dispatch,
- courseId,
- videoId,
- addVideoThumbnail,
- });
-
- const videos = useModels('videos', videoIds);
-
- const data = {
- supportedFileFormats,
- encodingsDownloadUrl,
- fileIds: videoIds,
- loadingStatus,
- usagePathStatus,
- usageErrorMessages: errorMessages.usageMetrics,
- fileType: 'video',
- };
- const thumbnailPreview = (props) => VideoThumbnail({
- ...props,
- pageLoadStatus: loadingStatus,
- handleAddThumbnail,
- videoImageSettings,
- });
- const infoModalSidebar = (video, activeTab, setActiveTab) => (
- VideoInfoModalSidebar({ video, activeTab, setActiveTab })
- );
- const maxFileSize = videoUploadMaxFileSize * 1073741824;
- const transcriptColumn = {
- id: 'transcriptStatus',
- Header: 'Transcript',
- accessor: 'transcriptStatus',
- Cell: ({ row }) => TranscriptColumn({ row }),
- Filter: CheckboxFilter,
- filter: 'exactTextCase',
- filterChoices: [
- {
- name: intl.formatMessage(messages.transcribedCheckboxLabel),
- value: 'transcribed',
- },
- {
- name: intl.formatMessage(messages.notTranscribedCheckboxLabel),
- value: 'notTranscribed',
- },
- ],
- };
- const activeColumn = {
- id: 'activeStatus',
- Header: 'Active',
- accessor: 'activeStatus',
- Cell: ({ row }) => ActiveColumn({ row, pageLoadStatus: loadingStatus }),
- Filter: CheckboxFilter,
- filter: 'exactTextCase',
- filterChoices: [
- { name: intl.formatMessage(messages.activeCheckboxLabel), value: 'active' },
- { name: intl.formatMessage(messages.inactiveCheckboxLabel), value: 'inactive' },
- ],
- };
- const durationColumn = {
- id: 'duration',
- Header: 'Video length',
- accessor: 'duration',
- Cell: ({ row }) => {
- const { duration } = row.original;
- return getFormattedDuration(duration);
- },
- };
- const processingStatusColumn = {
- id: 'status',
- Header: 'Status',
- accessor: 'status',
- Cell: ({ row }) => StatusColumn({ row }),
- Filter: CheckboxFilter,
- filterChoices: [
- { name: intl.formatMessage(messages.processingCheckboxLabel), value: 'Processing' },
-
- { name: intl.formatMessage(messages.failedCheckboxLabel), value: 'Failed' },
- ],
- };
- const videoThumbnailColumn = {
- id: 'courseVideoImageUrl',
- Header: '',
- Cell: ({ row }) => ThumbnailColumn({ row, thumbnailPreview }),
- };
- const tableColumns = [
- { ...videoThumbnailColumn },
- {
- Header: 'File name',
- accessor: 'clientVideoId',
- },
- { ...durationColumn },
- { ...transcriptColumn },
- { ...activeColumn },
- { ...processingStatusColumn },
- ];
-
if (loadingStatus === RequestStatus.DENIED) {
return (
@@ -233,60 +46,11 @@ const VideosPage = ({
/>
-
+
{intl.formatMessage(messages.videoUploadAlertLabel)}
-
-
-
-
-
- {isVideoTranscriptEnabled ? (
-
- ) : null}
-
- {loadingStatus !== RequestStatus.FAILED && (
- <>
- {isVideoTranscriptEnabled && (
-
- )}
-
- >
- )}
+
{intl.formatMessage(messages.heading)}
+
);
@@ -294,8 +58,6 @@ const VideosPage = ({
VideosPage.propTypes = {
courseId: PropTypes.string.isRequired,
- // injected
- intl: intlShape.isRequired,
};
-export default injectIntl(VideosPage);
+export default VideosPage;
diff --git a/src/plugin-slots/CourseFilesSlot/index.jsx b/src/plugin-slots/CourseFilesSlot/index.jsx
new file mode 100644
index 0000000000..5600354df7
--- /dev/null
+++ b/src/plugin-slots/CourseFilesSlot/index.jsx
@@ -0,0 +1,177 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import { CheckboxFilter } from '@openedx/paragon';
+import {
+ addAssetFile,
+ deleteAssetFile,
+ fetchAssetDownload,
+ getUsagePaths,
+ resetErrors,
+ updateAssetLock,
+ updateAssetOrder,
+ validateAssetFiles,
+} from 'CourseAuthoring/files-and-videos/files-page/data/thunks';
+import FileInfoModalSidebar from 'CourseAuthoring/files-and-videos/files-page/FileInfoModalSidebar';
+import FileThumbnail from 'CourseAuthoring/files-and-videos/files-page/FileThumbnail';
+import FileValidationModal from 'CourseAuthoring/files-and-videos/files-page/FileValidationModal';
+import messages from 'CourseAuthoring/files-and-videos/files-page/messages';
+import {
+ AccessColumn, ActiveColumn, FileTable, ThumbnailColumn,
+} from 'CourseAuthoring/files-and-videos/generic';
+import { useModels } from 'CourseAuthoring/generic/model-store';
+import { getFileSizeToClosestByte } from 'CourseAuthoring/utils';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+const CourseFilesSlot = ({ courseId }) => {
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const {
+ assetIds,
+ loadingStatus,
+ usageStatus: usagePathStatus,
+ errors: errorMessages,
+ } = useSelector(state => state.assets);
+ const data = {
+ fileIds: assetIds,
+ loadingStatus,
+ usagePathStatus,
+ usageErrorMessages: errorMessages.usageMetrics,
+ fileType: 'file',
+ };
+ const handleErrorReset = (error) => dispatch(resetErrors(error));
+ const handleDeleteFile = (id) => dispatch(deleteAssetFile(courseId, id));
+ const handleDownloadFile = (selectedRows) => dispatch(fetchAssetDownload({ selectedRows, courseId }));
+ const handleAddFile = (files) => {
+ handleErrorReset({ errorType: 'add' });
+ dispatch(validateAssetFiles(courseId, files));
+ };
+ const handleLockFile = (fileId, locked) => {
+ handleErrorReset({ errorType: 'lock' });
+ dispatch(updateAssetLock({ courseId, assetId: fileId, locked }));
+ };
+ const handleUsagePaths = (asset) => dispatch(getUsagePaths({ asset, courseId }));
+ const handleFileOrder = ({ newFileIdOrder, sortType }) => {
+ dispatch(updateAssetOrder(courseId, newFileIdOrder, sortType));
+ };
+
+ const handleFileOverwrite = (close, files) => {
+ Object.values(files).forEach(file => dispatch(addAssetFile(courseId, file, true)));
+ close();
+ };
+
+ const thumbnailPreview = (props) => FileThumbnail(props);
+ const infoModalSidebar = (asset) => FileInfoModalSidebar({
+ asset,
+ handleLockedAsset: handleLockFile,
+ });
+ const assets = useModels('assets', assetIds);
+ const maxFileSize = 20 * 1048576;
+
+ const activeColumn = {
+ id: 'activeStatus',
+ Header: 'Active',
+ accessor: 'activeStatus',
+ Cell: ({ row }) => ActiveColumn({ row, pageLoadStatus: loadingStatus }),
+ Filter: CheckboxFilter,
+ filter: 'exactTextCase',
+ filterChoices: [
+ { name: intl.formatMessage(messages.activeCheckboxLabel), value: 'active' },
+ { name: intl.formatMessage(messages.inactiveCheckboxLabel), value: 'inactive' },
+ ],
+ };
+ const accessColumn = {
+ id: 'lockStatus',
+ Header: 'Access',
+ accessor: 'lockStatus',
+ Cell: ({ row }) => AccessColumn({ row }),
+ Filter: CheckboxFilter,
+ filterChoices: [
+ { name: intl.formatMessage(messages.lockedCheckboxLabel), value: 'locked' },
+ { name: intl.formatMessage(messages.publicCheckboxLabel), value: 'public' },
+ ],
+ };
+ const thumbnailColumn = {
+ id: 'thumbnail',
+ Header: '',
+ Cell: ({ row }) => ThumbnailColumn({ row, thumbnailPreview }),
+ };
+ const fileSizeColumn = {
+ id: 'fileSize',
+ Header: 'File size',
+ accessor: 'fileSize',
+ Cell: ({ row }) => {
+ const { fileSize } = row.original;
+ return getFileSizeToClosestByte(fileSize);
+ },
+ };
+
+ const tableColumns = [
+ { ...thumbnailColumn },
+ {
+ Header: 'File name',
+ accessor: 'displayName',
+ },
+ { ...fileSizeColumn },
+ {
+ Header: 'Type',
+ accessor: 'wrapperType',
+ Filter: CheckboxFilter,
+ filter: 'includesValue',
+ filterChoices: [
+ {
+ name: intl.formatMessage(messages.codeCheckboxLabel),
+ value: 'code',
+ },
+ {
+ name: intl.formatMessage(messages.imageCheckboxLabel),
+ value: 'image',
+ },
+ {
+ name: intl.formatMessage(messages.documentCheckboxLabel),
+ value: 'document',
+ },
+ {
+ name: intl.formatMessage(messages.audioCheckboxLabel),
+ value: 'audio',
+ },
+ {
+ name: intl.formatMessage(messages.otherCheckboxLabel),
+ value: 'other',
+ },
+ ],
+ },
+ { ...activeColumn },
+ { ...accessColumn },
+ ];
+ return (
+
+
+
+
+ );
+};
+
+CourseFilesSlot.propTypes = {
+ courseId: PropTypes.string.isRequired,
+};
+
+export default CourseFilesSlot;
diff --git a/src/plugin-slots/CourseOutlinePageAlertsSlot/index.jsx b/src/plugin-slots/CourseOutlinePageAlertsSlot/index.jsx
new file mode 100644
index 0000000000..06c11017a9
--- /dev/null
+++ b/src/plugin-slots/CourseOutlinePageAlertsSlot/index.jsx
@@ -0,0 +1,5 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import React from 'react';
+
+const CourseOutlinePageAlertsSlot = () =>
;
+export default CourseOutlinePageAlertsSlot;
diff --git a/src/plugin-slots/CourseVideosSlot/index.jsx b/src/plugin-slots/CourseVideosSlot/index.jsx
new file mode 100644
index 0000000000..b52914d831
--- /dev/null
+++ b/src/plugin-slots/CourseVideosSlot/index.jsx
@@ -0,0 +1,246 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import {
+ ActionRow, Button, CheckboxFilter, useToggle,
+} from '@openedx/paragon';
+import { RequestStatus } from 'CourseAuthoring/data/constants';
+import {
+ ActiveColumn,
+ FileTable,
+ StatusColumn,
+ ThumbnailColumn,
+ TranscriptColumn,
+} from 'CourseAuthoring/files-and-videos/generic';
+import FILES_AND_UPLOAD_TYPE_FILTERS from 'CourseAuthoring/files-and-videos/generic/constants';
+import {
+ addVideoFile,
+ addVideoThumbnail,
+ deleteVideoFile,
+ fetchVideoDownload, fetchVideos,
+ getUsagePaths, markVideoUploadsInProgressAsFailed, resetErrors,
+ updateVideoOrder,
+} from 'CourseAuthoring/files-and-videos/videos-page/data/thunks';
+import { getFormattedDuration, resampleFile } from 'CourseAuthoring/files-and-videos/videos-page/data/utils';
+import VideoInfoModalSidebar from 'CourseAuthoring/files-and-videos/videos-page/info-sidebar';
+import messages from 'CourseAuthoring/files-and-videos/videos-page/messages';
+import TranscriptSettings from 'CourseAuthoring/files-and-videos/videos-page/transcript-settings';
+import VideoThumbnail from 'CourseAuthoring/files-and-videos/videos-page/VideoThumbnail';
+import { useModels } from 'CourseAuthoring/generic/model-store';
+import PropTypes from 'prop-types';
+import React, { useEffect, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+const CourseVideosSlot = ({ courseId }) => {
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const [
+ isTranscriptSettingsOpen,
+ openTranscriptSettings,
+ closeTranscriptSettings,
+ ] = useToggle(false);
+ const {
+ videoIds,
+ loadingStatus,
+ transcriptStatus,
+ addingStatus: addVideoStatus,
+ usageStatus: usagePathStatus,
+ errors: errorMessages,
+ pageSettings,
+ } = useSelector((state) => state.videos);
+
+ const uploadingIdsRef = useRef([]);
+
+ const {
+ isVideoTranscriptEnabled,
+ encodingsDownloadUrl,
+ videoUploadMaxFileSize,
+ videoSupportedFileFormats,
+ videoImageSettings,
+ } = pageSettings;
+ 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, uploadingIdsRef)));
+ };
+ const handleDeleteFile = (id) => dispatch(deleteVideoFile(courseId, id));
+ const handleDownloadFile = (selectedRows) => dispatch(fetchVideoDownload({ selectedRows, courseId }));
+ const handleUsagePaths = (video) => dispatch(getUsagePaths({ video, courseId }));
+ const handleFileOrder = ({ newFileIdOrder, sortType }) => {
+ dispatch(updateVideoOrder(courseId, newFileIdOrder, sortType));
+ };
+ const handleAddThumbnail = (file, videoId) => resampleFile({
+ file,
+ dispatch,
+ courseId,
+ videoId,
+ addVideoThumbnail,
+ });
+ const videos = useModels('videos', videoIds);
+
+ useEffect(() => {
+ dispatch(fetchVideos(courseId));
+ }, [courseId]);
+
+ useEffect(() => {
+ window.onbeforeunload = () => {
+ dispatch(markVideoUploadsInProgressAsFailed({ uploadingIdsRef, courseId }));
+ if (addVideoStatus === RequestStatus.IN_PROGRESS) {
+ return '';
+ }
+ return undefined;
+ };
+ }, [addVideoStatus]);
+
+ const data = {
+ supportedFileFormats,
+ encodingsDownloadUrl,
+ fileIds: videoIds,
+ loadingStatus,
+ usagePathStatus,
+ usageErrorMessages: errorMessages.usageMetrics,
+ fileType: 'video',
+ };
+ const thumbnailPreview = (props) => VideoThumbnail({
+ ...props,
+ pageLoadStatus: loadingStatus,
+ handleAddThumbnail,
+ videoImageSettings,
+ });
+ const infoModalSidebar = (video, activeTab, setActiveTab) => (
+ VideoInfoModalSidebar({ video, activeTab, setActiveTab })
+ );
+ const maxFileSize = videoUploadMaxFileSize * 1073741824;
+ const transcriptColumn = {
+ id: 'transcriptStatus',
+ Header: 'Transcript',
+ accessor: 'transcriptStatus',
+ Cell: ({ row }) => TranscriptColumn({ row }),
+ Filter: CheckboxFilter,
+ filter: 'exactTextCase',
+ filterChoices: [
+ {
+ name: intl.formatMessage(messages.transcribedCheckboxLabel),
+ value: 'transcribed',
+ },
+ {
+ name: intl.formatMessage(messages.notTranscribedCheckboxLabel),
+ value: 'notTranscribed',
+ },
+ ],
+ };
+ const activeColumn = {
+ id: 'activeStatus',
+ Header: 'Active',
+ accessor: 'activeStatus',
+ Cell: ({ row }) => ActiveColumn({ row, pageLoadStatus: loadingStatus }),
+ Filter: CheckboxFilter,
+ filter: 'exactTextCase',
+ filterChoices: [
+ { name: intl.formatMessage(messages.activeCheckboxLabel), value: 'active' },
+ { name: intl.formatMessage(messages.inactiveCheckboxLabel), value: 'inactive' },
+ ],
+ };
+ const durationColumn = {
+ id: 'duration',
+ Header: 'Video length',
+ accessor: 'duration',
+ Cell: ({ row }) => {
+ const { duration } = row.original;
+ return getFormattedDuration(duration);
+ },
+ };
+ const processingStatusColumn = {
+ id: 'status',
+ Header: 'Status',
+ accessor: 'status',
+ Cell: ({ row }) => StatusColumn({ row }),
+ Filter: CheckboxFilter,
+ filterChoices: [
+ { name: intl.formatMessage(messages.processingCheckboxLabel), value: 'Processing' },
+
+ { name: intl.formatMessage(messages.failedCheckboxLabel), value: 'Failed' },
+ ],
+ };
+ const videoThumbnailColumn = {
+ id: 'courseVideoImageUrl',
+ Header: '',
+ Cell: ({ row }) => ThumbnailColumn({ row, thumbnailPreview }),
+ };
+ const tableColumns = [
+ { ...videoThumbnailColumn },
+ {
+ Header: 'File name',
+ accessor: 'clientVideoId',
+ },
+ { ...durationColumn },
+ { ...transcriptColumn },
+ { ...activeColumn },
+ { ...processingStatusColumn },
+ ];
+ return (
+
+
+
+ {isVideoTranscriptEnabled ? (
+
+ ) : null}
+
+ {loadingStatus !== RequestStatus.FAILED && (
+ <>
+ {isVideoTranscriptEnabled && (
+
+ )}
+
+ >
+ )}
+
+ );
+};
+
+CourseVideosSlot.propTypes = {
+ courseId: PropTypes.string.isRequired,
+};
+
+export default CourseVideosSlot;
diff --git a/src/plugin-slots/EditFileErrorAlertsSlot/index.jsx b/src/plugin-slots/EditFileErrorAlertsSlot/index.jsx
new file mode 100644
index 0000000000..b60ef29d55
--- /dev/null
+++ b/src/plugin-slots/EditFileErrorAlertsSlot/index.jsx
@@ -0,0 +1,5 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+const EditFileErrorAlertsSlot = () =>
;
+
+export default EditFileErrorAlertsSlot;