From bfe3905f092f33a2dff21002a6947d724e8e677e 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. --- src/course-outline/page-alerts/PageAlerts.jsx | 3 + src/files-and-videos/files-page/FilesPage.jsx | 193 ++---------- .../generic/EditFileErrors.jsx | 140 ++++----- src/files-and-videos/generic/FileTable.jsx | 9 +- .../videos-page/VideosPage.jsx | 285 +----------------- src/plugin-slots/CourseFilesSlot/index.jsx | 169 +++++++++++ .../CourseOutlinePageAlertsSlot/index.jsx | 5 + src/plugin-slots/CourseVideosSlot/index.jsx | 271 +++++++++++++++++ .../EditFileErrorAlertsSlot/index.jsx | 8 + 9 files changed, 560 insertions(+), 523 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 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;