diff --git a/src/course-outline/page-alerts/PageAlerts.jsx b/src/course-outline/page-alerts/PageAlerts.jsx
index f71a9c368b..ce430d6e47 100644
--- a/src/course-outline/page-alerts/PageAlerts.jsx
+++ b/src/course-outline/page-alerts/PageAlerts.jsx
@@ -1,30 +1,31 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import { uniqBy } from 'lodash';
import { getConfig } from '@edx/frontend-platform';
-import { useDispatch, useSelector } from 'react-redux';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
+import {
+ Alert, Button, Hyperlink, Truncate,
+} from '@openedx/paragon';
import {
Campaign as CampaignIcon,
+ Error as ErrorIcon,
InfoOutline as InfoOutlineIcon,
Warning as WarningIcon,
- Error as ErrorIcon,
} from '@openedx/paragon/icons';
-import {
- Alert, Button, Hyperlink, Truncate,
-} from '@openedx/paragon';
+import { uniqBy } from 'lodash';
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
+import CourseOutlinePageAlertsSlot from '../../plugin-slots/CourseOutlinePageAlertsSlot';
+import advancedSettingsMessages from '../../advanced-settings/messages';
+import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
+import { RequestStatus } from '../../data/constants';
import ErrorAlert from '../../editors/sharedComponents/ErrorAlerts/ErrorAlert';
-import { RequestStatus } from '../../data/constants';
import AlertMessage from '../../generic/alert-message';
import AlertProctoringError from '../../generic/AlertProctoringError';
-import messages from './messages';
-import advancedSettingsMessages from '../../advanced-settings/messages';
+import { API_ERROR_TYPES } from '../constants';
import { getPasteFileNotices } from '../data/selectors';
import { dismissError, removePasteFileNotices } from '../data/slice';
-import { API_ERROR_TYPES } from '../constants';
-import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
+import messages from './messages';
const PageAlerts = ({
courseId,
@@ -437,6 +438,7 @@ const PageAlerts = ({
{conflictingFilesPasteAlert()}
{newFilesPasteAlert()}
{renderOutOfSyncAlert()}
+
>
);
};
diff --git a/src/files-and-videos/files-page/CourseFilesTable.tsx b/src/files-and-videos/files-page/CourseFilesTable.tsx
new file mode 100644
index 0000000000..98cb58157e
--- /dev/null
+++ b/src/files-and-videos/files-page/CourseFilesTable.tsx
@@ -0,0 +1,183 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { CheckboxFilter } from '@openedx/paragon';
+import {
+ addAssetFile,
+ deleteAssetFile,
+ fetchAssetDownload,
+ getUsagePaths,
+ resetErrors,
+ updateAssetLock,
+ updateAssetOrder,
+ validateAssetFiles,
+} from '@src/files-and-videos/files-page/data/thunks';
+import FileInfoModalSidebar from '@src/files-and-videos/files-page/FileInfoModalSidebar';
+import FileThumbnail from '@src/files-and-videos/files-page/FileThumbnail';
+import FileValidationModal from '@src/files-and-videos/files-page/FileValidationModal';
+import messages from '@src/files-and-videos/files-page/messages';
+import {
+ AccessColumn,
+ ActiveColumn,
+ FileTable,
+ ThumbnailColumn,
+} from '@src/files-and-videos/generic';
+import { useModels } from '@src/generic/model-store';
+import { DeprecatedReduxState } from '@src/store';
+import { getFileSizeToClosestByte } from '@src/utils';
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useParams } from 'react-router-dom';
+
+export const CourseFilesTable = () => {
+ const intl = useIntl();
+ const { courseId } = useParams() as { courseId: string };
+ const dispatch = useDispatch();
+ const {
+ assetIds,
+ loadingStatus,
+ usageStatus: usagePathStatus,
+ errors: errorMessages,
+ } = useSelector((state: DeprecatedReduxState) => 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 }) => {
+ dispatch(updateAssetOrder(courseId, newFileIdOrder));
+ };
+
+ 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: intl.formatMessage(messages.fileActiveColumn),
+ 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: intl.formatMessage(messages.fileAccessColumn),
+ 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: intl.formatMessage(messages.fileSizeColumn),
+ accessor: 'fileSize',
+ Cell: ({ row }) => {
+ const { fileSize } = row.original;
+ return getFileSizeToClosestByte(fileSize);
+ },
+ };
+
+ const tableColumns = [
+ { ...thumbnailColumn },
+ {
+ Header: intl.formatMessage(messages.fileNameColumn),
+ accessor: 'displayName',
+ },
+ { ...fileSizeColumn },
+ {
+ Header: intl.formatMessage(messages.fileTypeColumn),
+ 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 (!courseId) {
+ return null;
+ }
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.jsx
index 0976cc9bb6..71476ce0ba 100644
--- a/src/files-and-videos/files-page/FilesPage.jsx
+++ b/src/files-and-videos/files-page/FilesPage.jsx
@@ -1,37 +1,19 @@
-import React, { useEffect } from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Container } from '@openedx/paragon';
import PropTypes from 'prop-types';
+import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
-import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
-import { CheckboxFilter, Container } from '@openedx/paragon';
+import CourseFilesSlot from '../../plugin-slots/CourseFilesSlot';
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 { 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 EditFileAlertsSlot from '../../plugin-slots/EditFileAlertsSlot';
+import { EditFileErrors } from '../generic';
+import { fetchAssets, resetErrors } from './data/thunks';
+import FilesPageProvider from './FilesPageProvider';
+import messages from './messages';
import './FilesPage.scss';
const FilesPage = ({
@@ -41,133 +23,19 @@ const FilesPage = ({
const dispatch = useDispatch();
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: intl.formatMessage(messages.fileActiveColumn),
- 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: intl.formatMessage(messages.fileAccessColumn),
- 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: intl.formatMessage(messages.fileSizeColumn),
- accessor: 'fileSize',
- Cell: ({ row }) => {
- const { fileSize } = row.original;
- return getFileSizeToClosestByte(fileSize);
- },
- };
+ useEffect(() => {
+ dispatch(fetchAssets(courseId));
+ }, [courseId]);
- const tableColumns = [
- { ...thumbnailColumn },
- {
- Header: intl.formatMessage(messages.fileNameColumn),
- accessor: 'displayName',
- },
- { ...fileSizeColumn },
- {
- Header: intl.formatMessage(messages.fileTypeColumn),
- 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 (
@@ -188,31 +56,12 @@ const FilesPage = ({
updateFileStatus={updateAssetStatus}
loadingStatus={loadingStatus}
/>
+
-
+ {intl.formatMessage(messages.heading)}
{loadingStatus !== RequestStatus.FAILED && (
- <>
-
-
- >
+
)}
diff --git a/src/files-and-videos/files-page/FilesPage.test.jsx b/src/files-and-videos/files-page/FilesPage.test.jsx
index 2e2d1ada3e..0e5ab24eea 100644
--- a/src/files-and-videos/files-page/FilesPage.test.jsx
+++ b/src/files-and-videos/files-page/FilesPage.test.jsx
@@ -14,6 +14,7 @@ import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
import initializeStore from '../../store';
import { executeThunk } from '../../utils';
@@ -51,8 +52,17 @@ jest.mock('file-saver');
const renderComponent = () => {
render(
-
-
+
+
+
+
+ }
+ />
+
+
,
);
diff --git a/src/files-and-videos/videos-page/CourseVideosTable.tsx b/src/files-and-videos/videos-page/CourseVideosTable.tsx
new file mode 100644
index 0000000000..d1667ef13f
--- /dev/null
+++ b/src/files-and-videos/videos-page/CourseVideosTable.tsx
@@ -0,0 +1,288 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ ActionRow, Button, CheckboxFilter, useToggle,
+} from '@openedx/paragon';
+import { RequestStatus } from '@src/data/constants';
+import {
+ ActiveColumn,
+ FileTable,
+ StatusColumn,
+ ThumbnailColumn,
+ TranscriptColumn,
+} from '@src/files-and-videos/generic';
+import FILES_AND_UPLOAD_TYPE_FILTERS from '@src/files-and-videos/generic/constants';
+import {
+ addVideoFile,
+ addVideoThumbnail,
+ cancelAllUploads,
+ deleteVideoFile,
+ fetchVideoDownload,
+ getUsagePaths,
+ markVideoUploadsInProgressAsFailed,
+ newUploadData,
+ resetErrors,
+ updateVideoOrder,
+} from '@src/files-and-videos/videos-page/data/thunks';
+import { getFormattedDuration, resampleFile } from '@src/files-and-videos/videos-page/data/utils';
+import VideoInfoModalSidebar from '@src/files-and-videos/videos-page/info-sidebar';
+import messages from '@src/files-and-videos/videos-page/messages';
+import TranscriptSettings from '@src/files-and-videos/videos-page/transcript-settings';
+import UploadModal from '@src/files-and-videos/videos-page/upload-modal';
+import VideoThumbnail from '@src/files-and-videos/videos-page/VideoThumbnail';
+import { useModels } from '@src/generic/model-store';
+import { DeprecatedReduxState } from '@src/store';
+import React, { useEffect, useRef } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useParams } from 'react-router-dom';
+
+export const CourseVideosTable = () => {
+ const intl = useIntl();
+ const { courseId } = useParams() as { courseId: string };
+ const dispatch = useDispatch();
+ const [
+ isTranscriptSettingsOpen,
+ openTranscriptSettings,
+ closeTranscriptSettings,
+ ] = useToggle(false);
+ const [
+ isUploadTrackerOpen,
+ openUploadTracker,
+ closeUploadTracker,
+ ] = useToggle(false);
+
+ const {
+ videoIds,
+ loadingStatus,
+ transcriptStatus,
+ addingStatus: addVideoStatus,
+ usageStatus: usagePathStatus,
+ errors: errorMessages,
+ pageSettings,
+ } = useSelector((state: DeprecatedReduxState) => 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;
+
+ files.forEach((file, idx) => {
+ const name = file?.name || `Video ${idx + 1}`;
+ const progress = 0;
+
+ newUploadData({
+ status: RequestStatus.PENDING,
+ currentData: uploadingIdsRef.current.uploadData,
+ originalValue: { name, progress },
+ key: `video_${idx}`,
+ edxVideoId: undefined,
+ });
+ });
+ 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 }) => {
+ dispatch(updateVideoOrder(courseId, newFileIdOrder));
+ };
+ 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) => (
+
+ );
+ 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 && (
+
+ )}
+
+ >
+ )
+ }
+
+ >
+ );
+};
diff --git a/src/files-and-videos/videos-page/VideosPage.jsx b/src/files-and-videos/videos-page/VideosPage.jsx
index 18ed079826..1b164afdfd 100644
--- a/src/files-and-videos/videos-page/VideosPage.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.jsx
@@ -1,246 +1,40 @@
-import React, { useEffect, useRef } from 'react';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Container } from '@openedx/paragon';
import PropTypes from 'prop-types';
+import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useDispatch, useSelector } from 'react-redux';
-import {
- useIntl,
- FormattedMessage,
-} from '@edx/frontend-platform/i18n';
-import {
- ActionRow,
- Button,
- CheckboxFilter,
- Container,
- useToggle,
-} from '@openedx/paragon';
+import CourseVideosSlot from '../../plugin-slots/CourseVideosSlot';
+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,
- newUploadData,
-} from './data/thunks';
+import { useModel } from '../../generic/model-store';
+import getPageHeadTitle from '../../generic/utils';
+import EditVideoAlertsSlot from '../../plugin-slots/EditVideoAlertsSlot';
+import { EditFileErrors } from '../generic';
+import { fetchVideos, 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,
}) => {
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;
-
- files.forEach((file, idx) => {
- const name = file?.name || `Video ${idx + 1}`;
- const progress = 0;
-
- newUploadData({
- status: RequestStatus.PENDING,
- currentData: uploadingIdsRef.current.uploadData,
- originalValue: { name, progress },
- key: `video_${idx}`,
- });
- });
-
- 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) => (
-
- );
- 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 },
- ];
+ useEffect(() => {
+ dispatch(fetchVideos(courseId));
+ }, [courseId]);
if (loadingStatus === RequestStatus.DENIED) {
return (
@@ -264,65 +58,9 @@ const VideosPage = ({
updateFileStatus={updateVideoStatus}
loadingStatus={loadingStatus}
/>
-
-
-
-
-
- {isVideoTranscriptEnabled ? (
-
- ) : null}
-
- {loadingStatus !== RequestStatus.FAILED && (
- <>
- {isVideoTranscriptEnabled && (
-
- )}
-
- >
- )}
-
+
+ {intl.formatMessage(messages.heading)}
+
);
diff --git a/src/files-and-videos/videos-page/VideosPage.test.jsx b/src/files-and-videos/videos-page/VideosPage.test.jsx
index c7aa3c7ebf..7ae5aa70b7 100644
--- a/src/files-and-videos/videos-page/VideosPage.test.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.test.jsx
@@ -13,6 +13,8 @@ import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient, getHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
+import React from 'react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
import initializeStore from '../../store';
import { executeThunk } from '../../utils';
@@ -50,8 +52,17 @@ jest.mock('file-saver');
const renderComponent = () => {
render(
-
-
+
+
+
+
+ }
+ />
+
+
,
);
diff --git a/src/plugin-slots/CourseFilesSlot/README.md b/src/plugin-slots/CourseFilesSlot/README.md
new file mode 100644
index 0000000000..691dc19e49
--- /dev/null
+++ b/src/plugin-slots/CourseFilesSlot/README.md
@@ -0,0 +1,41 @@
+# Course Files Slot
+
+### Slot ID: `org.openedx.frontend.authoring.files_upload_page_table.v1`
+
+## Description
+
+This slot is used to replace/modify/hide the course file table UI.
+
+## Example
+
+### Wrapped with a div with dashed border
+
+
+
+The following `env.config.jsx` will wrap the files component with a div that has a large dashed
+red border.
+
+```js
+import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.authoring.files_upload_page_table.v1': {
+ keepDefault: true,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Wrap,
+ widgetId: 'default_contents',
+ wrapper: ({component}) => (
+
+ {component}
+
+ )
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/CourseFilesSlot/index.tsx b/src/plugin-slots/CourseFilesSlot/index.tsx
new file mode 100644
index 0000000000..3ee657aff7
--- /dev/null
+++ b/src/plugin-slots/CourseFilesSlot/index.tsx
@@ -0,0 +1,11 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import { CourseFilesTable } from '@src/files-and-videos/files-page/CourseFilesTable';
+import React from 'react';
+
+const CourseFilesSlot = () => (
+
+
+
+);
+
+export default CourseFilesSlot;
diff --git a/src/plugin-slots/CourseFilesSlot/screenshot_files_border_wrap.png b/src/plugin-slots/CourseFilesSlot/screenshot_files_border_wrap.png
new file mode 100644
index 0000000000..67cf99952d
Binary files /dev/null and b/src/plugin-slots/CourseFilesSlot/screenshot_files_border_wrap.png differ
diff --git a/src/plugin-slots/CourseOutlinePageAlertsSlot/README.md b/src/plugin-slots/CourseOutlinePageAlertsSlot/README.md
new file mode 100644
index 0000000000..3de9376571
--- /dev/null
+++ b/src/plugin-slots/CourseOutlinePageAlertsSlot/README.md
@@ -0,0 +1,44 @@
+# Course Outline Page Alerts Slot
+
+### Slot ID: `org.openedx.frontend.authoring.course_outline_page_alerts.v1`
+
+## Description
+
+This slot is used to add alerts to the course outline page.
+
+## Example
+
+### Additional Alert
+
+
+
+The following `env.config.jsx` display a custom additional alert on the course outline page.
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+import { Alert } from '@openedx/paragon';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.authoring.course_outline_page_alerts.v1': {
+ keepDefault: true,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'test-alert',
+ type: DIRECT_PLUGIN,
+ RenderWidget: () => (
+
+ This is a test alert
+
+ )
+ }
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/CourseOutlinePageAlertsSlot/index.tsx b/src/plugin-slots/CourseOutlinePageAlertsSlot/index.tsx
new file mode 100644
index 0000000000..5dbf44c50c
--- /dev/null
+++ b/src/plugin-slots/CourseOutlinePageAlertsSlot/index.tsx
@@ -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/CourseOutlinePageAlertsSlot/screenshot_outline_alert_added.png b/src/plugin-slots/CourseOutlinePageAlertsSlot/screenshot_outline_alert_added.png
new file mode 100644
index 0000000000..e859dcada1
Binary files /dev/null and b/src/plugin-slots/CourseOutlinePageAlertsSlot/screenshot_outline_alert_added.png differ
diff --git a/src/plugin-slots/CourseVideosSlot/README.md b/src/plugin-slots/CourseVideosSlot/README.md
new file mode 100644
index 0000000000..8580654e35
--- /dev/null
+++ b/src/plugin-slots/CourseVideosSlot/README.md
@@ -0,0 +1,42 @@
+# Course Video Upload Page Slot
+
+### Slot ID: `org.openedx.frontend.authoring.videos_upload_page_table.v1`
+
+## Description
+
+This slot is used to replace/modify/hide the course video upload page UI.
+
+## Example
+
+### Wrapped with a div with dashed border
+
+
+
+
+
+The following `env.config.jsx` will wrap the videos UI with a div that has a large dashed red border.
+
+```js
+import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.authoring.videos_upload_page_table.v1': {
+ keepDefault: true,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Wrap,
+ widgetId: 'default_contents',
+ wrapper: ({component}) => (
+
+ {component}
+
+ )
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/CourseVideosSlot/index.tsx b/src/plugin-slots/CourseVideosSlot/index.tsx
new file mode 100644
index 0000000000..276988eb11
--- /dev/null
+++ b/src/plugin-slots/CourseVideosSlot/index.tsx
@@ -0,0 +1,11 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+import { CourseVideosTable } from '@src/files-and-videos/videos-page/CourseVideosTable';
+import React from 'react';
+
+const CourseVideosSlot = () => (
+
+
+
+);
+
+export default CourseVideosSlot;
diff --git a/src/plugin-slots/CourseVideosSlot/screenshot_list_videos_border_wrap.png b/src/plugin-slots/CourseVideosSlot/screenshot_list_videos_border_wrap.png
new file mode 100644
index 0000000000..ac940f1ece
Binary files /dev/null and b/src/plugin-slots/CourseVideosSlot/screenshot_list_videos_border_wrap.png differ
diff --git a/src/plugin-slots/CourseVideosSlot/screenshot_upload_videos_border_wrap.png b/src/plugin-slots/CourseVideosSlot/screenshot_upload_videos_border_wrap.png
new file mode 100644
index 0000000000..3c36ba98aa
Binary files /dev/null and b/src/plugin-slots/CourseVideosSlot/screenshot_upload_videos_border_wrap.png differ
diff --git a/src/plugin-slots/EditFileAlertsSlot/README.md b/src/plugin-slots/EditFileAlertsSlot/README.md
new file mode 100644
index 0000000000..92c2c4ffa4
--- /dev/null
+++ b/src/plugin-slots/EditFileAlertsSlot/README.md
@@ -0,0 +1,44 @@
+# Files Page Alerts Slot
+
+### Slot ID: `org.openedx.frontend.authoring.edit_file_alerts.v1`
+
+## Description
+
+This slot is used to add alerts to the course file edit page.
+
+## Example
+
+### Additional Alert on Files Page
+
+
+
+The following `env.config.jsx` will display an additional custom alert on the files and uploads page.
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+import { Alert } from '@openedx/paragon';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.authoring.edit_file_alerts.v1': {
+ keepDefault: true,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'test-alert',
+ type: DIRECT_PLUGIN,
+ RenderWidget: () => (
+
+ This is a test alert
+
+ )
+ }
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/EditFileAlertsSlot/index.tsx b/src/plugin-slots/EditFileAlertsSlot/index.tsx
new file mode 100644
index 0000000000..43f6b66537
--- /dev/null
+++ b/src/plugin-slots/EditFileAlertsSlot/index.tsx
@@ -0,0 +1,5 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+const EditFileAlertsSlot = () => ;
+
+export default EditFileAlertsSlot;
diff --git a/src/plugin-slots/EditFileAlertsSlot/screenshot_files_alert_added.png b/src/plugin-slots/EditFileAlertsSlot/screenshot_files_alert_added.png
new file mode 100644
index 0000000000..3b02ee55d7
Binary files /dev/null and b/src/plugin-slots/EditFileAlertsSlot/screenshot_files_alert_added.png differ
diff --git a/src/plugin-slots/EditVideoAlertsSlot/README.md b/src/plugin-slots/EditVideoAlertsSlot/README.md
new file mode 100644
index 0000000000..92a6b478d9
--- /dev/null
+++ b/src/plugin-slots/EditVideoAlertsSlot/README.md
@@ -0,0 +1,44 @@
+# Videos Page Alerts Slot
+
+### Slot ID: `org.openedx.frontend.authoring.edit_video_alerts.v1`
+
+## Description
+
+This slot is used to add alerts to the course video edit page.
+
+## Example
+
+### Additional Alert on Videos Page
+
+
+
+The following `env.config.jsx` will display an additional custom alert on the videos page.
+
+```js
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+import { Alert } from '@openedx/paragon';
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.authoring.edit_video_alerts.v1': {
+ keepDefault: true,
+ plugins: [
+ {
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'test-alert',
+ type: DIRECT_PLUGIN,
+ RenderWidget: () => (
+
+ This is a test alert
+
+ )
+ }
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/EditVideoAlertsSlot/index.tsx b/src/plugin-slots/EditVideoAlertsSlot/index.tsx
new file mode 100644
index 0000000000..47ebbcc83d
--- /dev/null
+++ b/src/plugin-slots/EditVideoAlertsSlot/index.tsx
@@ -0,0 +1,5 @@
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+const EditVideoAlertsSlot = () => ;
+
+export default EditVideoAlertsSlot;
diff --git a/src/plugin-slots/EditVideoAlertsSlot/screenshot_videos_alert_added.png b/src/plugin-slots/EditVideoAlertsSlot/screenshot_videos_alert_added.png
new file mode 100644
index 0000000000..58e6f79787
Binary files /dev/null and b/src/plugin-slots/EditVideoAlertsSlot/screenshot_videos_alert_added.png differ