diff --git a/src/course-outline/page-alerts/PageAlerts.jsx b/src/course-outline/page-alerts/PageAlerts.jsx
index 4b12996395..61ebbdd2c7 100644
--- a/src/course-outline/page-alerts/PageAlerts.jsx
+++ b/src/course-outline/page-alerts/PageAlerts.jsx
@@ -1,3 +1,5 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import CourseOutlinePageAlertsSlot from 'CourseAuthoring/plugin-slots/CourseOutlinePageAlertsSlot';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { uniqBy } from 'lodash';
@@ -407,6 +409,7 @@ const PageAlerts = ({
{errorFilesPasteAlert()}
{conflictingFilesPasteAlert()}
{newFilesPasteAlert()}
+
>
);
};
diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.jsx
index be100bd55b..73af2636cd 100644
--- a/src/files-and-videos/files-page/FilesPage.jsx
+++ b/src/files-and-videos/files-page/FilesPage.jsx
@@ -1,174 +1,37 @@
-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 '../../editors/Placeholder';
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 Placeholder from '../../editors/Placeholder';
+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);
-
+ useEffect(() => {
+ dispatch(fetchAssets(courseId));
+ }, [courseId]);
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);
- },
- };
-
- 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 },
- ];
-
if (loadingStatus === RequestStatus.DENIED) {
return (
@@ -189,30 +52,10 @@ const FilesPage = ({
loadingStatus={loadingStatus}
/>
-
+ {intl.formatMessage(messages.heading)}
{loadingStatus !== RequestStatus.FAILED && (
- <>
-
-
- >
+
)}
@@ -221,8 +64,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 a964fbc9da..8ee5dcbd08 100644
--- a/src/files-and-videos/generic/EditFileErrors.jsx
+++ b/src/files-and-videos/generic/EditFileErrors.jsx
@@ -1,6 +1,8 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+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 { Alert } from '@openedx/paragon';
import ErrorAlert from '../../editors/sharedComponents/ErrorAlerts/ErrorAlert';
import { RequestStatus } from '../../data/constants';
@@ -13,71 +15,73 @@ 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}
- >
-
- {intl.formatMessage(messages.uploadErrorAlertTitle)}
-
-
- {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}
+ >
+
+ {intl.formatMessage(messages.uploadErrorAlertTitle)}
+
+
+ {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,
@@ -93,8 +97,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 33a770ffb8..76de2e0d92 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 = {
@@ -314,8 +313,6 @@ FileTable.propTypes = {
maxFileSize: PropTypes.number.isRequired,
thumbnailPreview: PropTypes.func.isRequired,
infoModalSidebar: PropTypes.func.isRequired,
- // injected
- intl: intlShape.isRequired,
};
FileTable.defaultProps = {
@@ -323,4 +320,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 bf1f78528c..0451a59f87 100644
--- a/src/files-and-videos/videos-page/VideosPage.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.jsx
@@ -1,234 +1,34 @@
-import React, { useEffect, useRef } from 'react';
+import { useIntl, } from '@edx/frontend-platform/i18n';
+import { Container } from '@openedx/paragon';
+import CourseVideosSlot from 'CourseAuthoring/plugin-slots/CourseVideosSlot';
import PropTypes from 'prop-types';
+import React from 'react';
import { Helmet } from 'react-helmet';
import { useDispatch, useSelector } from 'react-redux';
-import {
- injectIntl,
- FormattedMessage,
- intlShape,
-} from '@edx/frontend-platform/i18n';
-import {
- ActionRow,
- Button,
- CheckboxFilter,
- Container,
- useToggle,
-} from '@openedx/paragon';
+import { RequestStatus } from '../../data/constants';
import Placeholder from '../../editors/Placeholder';
-import { RequestStatus } from '../../data/constants';
-import { useModels, useModel } from '../../generic/model-store';
-import {
- addVideoFile,
- addVideoThumbnail,
- deleteVideoFile,
- fetchVideoDownload,
- fetchVideos,
- getUsagePaths,
- markVideoUploadsInProgressAsFailed,
- resetErrors,
- updateVideoOrder,
- cancelAllUploads,
-} from './data/thunks';
+import { useModel } from '../../generic/model-store';
+import getPageHeadTitle from '../../generic/utils';
+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 { getFormattedDuration, resampleFile } from './data/utils';
-import FILES_AND_UPLOAD_TYPE_FILTERS from '../generic/constants';
-import TranscriptSettings from './transcript-settings';
-import VideoInfoModalSidebar from './info-sidebar';
-import VideoThumbnail from './VideoThumbnail';
-import UploadModal from './upload-modal';
const VideosPage = ({
courseId,
- // injected
- intl,
}) => {
+ const intl = useIntl();
const dispatch = useDispatch();
- const [
- isTranscriptSettingsOpen,
- openTranscriptSettings,
- closeTranscriptSettings,
- ] = useToggle(false);
- const [
- isUploadTrackerOpen,
- openUploadTracker,
- closeUploadTracker,
- ] = useToggle(false);
const courseDetails = useModel('courseDetails', courseId);
-
- 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({ uploadData: {}, uploadCount: 0 });
-
- useEffect(() => {
- window.onbeforeunload = () => {
- dispatch(markVideoUploadsInProgressAsFailed({ uploadingIdsRef, courseId }));
- if (addVideoStatus === RequestStatus.IN_PROGRESS) {
- return '';
- }
- return undefined;
- };
- switch (addVideoStatus) {
- case RequestStatus.IN_PROGRESS:
- openUploadTracker();
- break;
- case RequestStatus.SUCCESSFUL:
- setTimeout(() => closeUploadTracker(), 500);
- break;
- case RequestStatus.FAILED:
- setTimeout(() => closeUploadTracker(), 500);
- break;
- default:
- closeUploadTracker();
- break;
- }
- }, [addVideoStatus]);
-
- const {
- isVideoTranscriptEnabled,
- encodingsDownloadUrl,
- videoUploadMaxFileSize,
- videoSupportedFileFormats,
- videoImageSettings,
- } = pageSettings;
-
- const supportedFileFormats = {
- 'video/*': videoSupportedFileFormats || FILES_AND_UPLOAD_TYPE_FILTERS.video,
- };
- const handleUploadCancel = () => dispatch(cancelAllUploads(courseId, uploadingIdsRef.current.uploadData));
const handleErrorReset = (error) => dispatch(resetErrors(error));
- const handleAddFile = (files) => {
- handleErrorReset({ errorType: 'add' });
- uploadingIdsRef.current.uploadCount = files.length;
- dispatch(addVideoFile(courseId, files, 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 (
@@ -251,65 +51,8 @@ const VideosPage = ({
updateFileStatus={updateVideoStatus}
loadingStatus={loadingStatus}
/>
-
-
-
-
-
- {isVideoTranscriptEnabled ? (
-
- ) : null}
-
- {loadingStatus !== RequestStatus.FAILED && (
- <>
- {isVideoTranscriptEnabled && (
-
- )}
-
- >
- )}
-
+
{intl.formatMessage(messages.heading)}
+
);
@@ -317,8 +60,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..fe0d45e89d
--- /dev/null
+++ b/src/plugin-slots/CourseFilesSlot/index.jsx
@@ -0,0 +1,169 @@
+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 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 (
+
+
+
+
+ );
+};
+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..7ed5e9137b
--- /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..8603f1957f
--- /dev/null
+++ b/src/plugin-slots/CourseVideosSlot/index.jsx
@@ -0,0 +1,271 @@
+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, cancelAllUploads,
+ 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 UploadModal from 'CourseAuthoring/files-and-videos/videos-page/upload-modal';
+import VideoThumbnail from 'CourseAuthoring/files-and-videos/videos-page/VideoThumbnail';
+import { useModel, useModels } from 'CourseAuthoring/generic/model-store';
+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({ uploadData: {}, uploadCount: 0 });
+
+ const {
+ isVideoTranscriptEnabled,
+ encodingsDownloadUrl,
+ videoUploadMaxFileSize,
+ videoSupportedFileFormats,
+ videoImageSettings,
+ } = pageSettings;
+ const supportedFileFormats = {
+ 'video/*': videoSupportedFileFormats || FILES_AND_UPLOAD_TYPE_FILTERS.video,
+ };
+ const handleUploadCancel = () => dispatch(cancelAllUploads(courseId, uploadingIdsRef.current.uploadData));
+ const handleErrorReset = (error) => dispatch(resetErrors(error));
+ const handleAddFile = (files) => {
+ handleErrorReset({ errorType: 'add' });
+ uploadingIdsRef.current.uploadCount = files.length;
+ dispatch(addVideoFile(courseId, files, 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 [
+ isUploadTrackerOpen,
+ openUploadTracker,
+ closeUploadTracker,
+ ] = useToggle(false);
+
+ useEffect(() => {
+ dispatch(fetchVideos(courseId));
+ }, [courseId]);
+
+ useEffect(() => {
+ window.onbeforeunload = () => {
+ dispatch(markVideoUploadsInProgressAsFailed({ uploadingIdsRef, courseId }));
+ if (addVideoStatus === RequestStatus.IN_PROGRESS) {
+ return '';
+ }
+ return undefined;
+ };
+ switch (addVideoStatus) {
+ case RequestStatus.IN_PROGRESS:
+ openUploadTracker();
+ break;
+ case RequestStatus.SUCCESSFUL:
+ setTimeout(() => closeUploadTracker(), 500);
+ break;
+ case RequestStatus.FAILED:
+ setTimeout(() => closeUploadTracker(), 500);
+ break;
+ default:
+ closeUploadTracker();
+ break;
+ }
+ }, [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 && (
+
+ )}
+
+ >
+ )}
+
+
+ );
+};
+
+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..d1903ffb3a
--- /dev/null
+++ b/src/plugin-slots/EditFileErrorAlertsSlot/index.jsx
@@ -0,0 +1,8 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+
+const EditFileErrorAlertsSlot = () =>
+
+
+
+export default EditFileErrorAlertsSlot;