diff --git a/src/course-outline/page-alerts/PageAlerts.jsx b/src/course-outline/page-alerts/PageAlerts.jsx
index 4b12996395..828e937a28 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 { uniqBy } from 'lodash';
@@ -407,6 +408,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..afd79a5166 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 '../../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);
- 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}
/>
@@ -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..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..558928182d
--- /dev/null
+++ b/src/plugin-slots/CourseVideosSlot/index.jsx
@@ -0,0 +1,276 @@
+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 { 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({ 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 && (
+
+ )}
+
+ >
+ )}
+
+
+ );
+};
+
+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;