From 053822f20120f4953095518674b96940b7b24494 Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Wed, 20 Nov 2024 17:56:54 +0530 Subject: [PATCH] feat: Add slots for video and file upload components and alerts This change add plugin slots for the file and video upload components, and the alerts components on those pages. (cherry picked from commit 9c79a4184a95ecc6ac06ce8b56c64227af7528ef) --- src/course-outline/page-alerts/PageAlerts.jsx | 2 + src/files-and-videos/files-page/FilesPage.jsx | 192 ++----------- .../generic/EditFileErrors.jsx | 133 ++++----- src/files-and-videos/generic/FileTable.jsx | 9 +- .../videos-page/VideosPage.jsx | 270 ++---------------- src/plugin-slots/CourseFilesSlot/index.jsx | 177 ++++++++++++ .../CourseOutlinePageAlertsSlot/index.jsx | 5 + src/plugin-slots/CourseVideosSlot/index.jsx | 246 ++++++++++++++++ .../EditFileErrorAlertsSlot/index.jsx | 5 + 9 files changed, 540 insertions(+), 499 deletions(-) create mode 100644 src/plugin-slots/CourseFilesSlot/index.jsx create mode 100644 src/plugin-slots/CourseOutlinePageAlertsSlot/index.jsx create mode 100644 src/plugin-slots/CourseVideosSlot/index.jsx create mode 100644 src/plugin-slots/EditFileErrorAlertsSlot/index.jsx 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/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..a39e598aaa 100644 --- a/src/files-and-videos/videos-page/VideosPage.jsx +++ b/src/files-and-videos/videos-page/VideosPage.jsx @@ -1,217 +1,32 @@ -import React, { useEffect, useRef } from 'react'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { 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 { useModel } from '../../generic/model-store'; +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 (
@@ -231,62 +46,13 @@ const VideosPage = ({ updateFileStatus={updateVideoStatus} loadingStatus={loadingStatus} /> - -
-

-
-
- -
- -
- - {isVideoTranscriptEnabled ? ( - - ) : null} -
- {loadingStatus !== RequestStatus.FAILED && ( - <> - {isVideoTranscriptEnabled && ( - - )} - - - )} + +
+

{intl.formatMessage(messages.videoUploadAlertLabel)}

+
+
+

{intl.formatMessage(messages.heading)}

+ ); @@ -294,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..fd74667919 --- /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({ uploadData: {}, uploadCount: 0 }); + + 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;