feat: Add slots for video and file upload components and alerts (#1523)
This change add plugin slots for the file and video upload components, and the alerts components on those pages.
This commit is contained in:
@@ -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 {
|
||||
Campaign as CampaignIcon,
|
||||
InfoOutline as InfoOutlineIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import {
|
||||
Alert, Button, Hyperlink, Truncate,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Campaign as CampaignIcon,
|
||||
Error as ErrorIcon,
|
||||
InfoOutline as InfoOutlineIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
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()}
|
||||
<CourseOutlinePageAlertsSlot />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
183
src/files-and-videos/files-page/CourseFilesTable.tsx
Normal file
183
src/files-and-videos/files-page/CourseFilesTable.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<FileTable
|
||||
{...{
|
||||
courseId,
|
||||
data,
|
||||
handleAddFile,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleLockFile,
|
||||
handleUsagePaths,
|
||||
handleErrorReset,
|
||||
handleFileOrder,
|
||||
tableColumns,
|
||||
maxFileSize,
|
||||
thumbnailPreview,
|
||||
infoModalSidebar,
|
||||
files: assets,
|
||||
}}
|
||||
/>
|
||||
<FileValidationModal {...{ handleFileOverwrite }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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));
|
||||
const {
|
||||
loadingStatus,
|
||||
addingStatus: addAssetStatus,
|
||||
deletingStatus: deleteAssetStatus,
|
||||
updatingStatus: updateAssetStatus,
|
||||
errors: errorMessages,
|
||||
} = useSelector(state => state.assets);
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
||||
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 (loadingStatus === RequestStatus.DENIED) {
|
||||
return (
|
||||
@@ -188,31 +56,12 @@ const FilesPage = ({
|
||||
updateFileStatus={updateAssetStatus}
|
||||
loadingStatus={loadingStatus}
|
||||
/>
|
||||
<EditFileAlertsSlot />
|
||||
<div className="h2">
|
||||
<FormattedMessage {...messages.heading} />
|
||||
{intl.formatMessage(messages.heading)}
|
||||
</div>
|
||||
{loadingStatus !== RequestStatus.FAILED && (
|
||||
<>
|
||||
<FileTable
|
||||
{...{
|
||||
courseId,
|
||||
data,
|
||||
handleAddFile,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleLockFile,
|
||||
handleUsagePaths,
|
||||
handleErrorReset,
|
||||
handleFileOrder,
|
||||
tableColumns,
|
||||
maxFileSize,
|
||||
thumbnailPreview,
|
||||
infoModalSidebar,
|
||||
files: assets,
|
||||
}}
|
||||
/>
|
||||
<FileValidationModal {...{ handleFileOverwrite }} />
|
||||
</>
|
||||
<CourseFilesSlot />
|
||||
)}
|
||||
</Container>
|
||||
</FilesPageProvider>
|
||||
|
||||
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<FilesPage courseId={courseId} />
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[`/course/${courseId}/videos`]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/course/:courseId/*"
|
||||
element={
|
||||
<FilesPage courseId={courseId} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
288
src/files-and-videos/videos-page/CourseVideosTable.tsx
Normal file
288
src/files-and-videos/videos-page/CourseVideosTable.tsx
Normal file
@@ -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) => (
|
||||
<VideoInfoModalSidebar video={video} activeTab={activeTab} setActiveTab={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 (
|
||||
<>
|
||||
<ActionRow>
|
||||
<ActionRow.Spacer />
|
||||
{isVideoTranscriptEnabled ? (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
openTranscriptSettings();
|
||||
handleErrorReset({ errorType: 'transcript' });
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(messages.transcriptSettingsButtonLabel)}
|
||||
</Button>
|
||||
) : null}
|
||||
</ActionRow>
|
||||
{
|
||||
loadingStatus !== RequestStatus.FAILED && (
|
||||
<>
|
||||
{isVideoTranscriptEnabled && (
|
||||
<TranscriptSettings
|
||||
{...{
|
||||
isTranscriptSettingsOpen,
|
||||
closeTranscriptSettings,
|
||||
handleErrorReset,
|
||||
errorMessages,
|
||||
transcriptStatus,
|
||||
courseId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FileTable
|
||||
{...{
|
||||
courseId,
|
||||
data,
|
||||
handleAddFile,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleUsagePaths,
|
||||
handleErrorReset,
|
||||
handleFileOrder,
|
||||
tableColumns,
|
||||
maxFileSize,
|
||||
thumbnailPreview,
|
||||
infoModalSidebar,
|
||||
files: videos,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<UploadModal
|
||||
{...{
|
||||
isUploadTrackerOpen,
|
||||
currentUploadingIdsRef: uploadingIdsRef.current,
|
||||
handleUploadCancel,
|
||||
addVideoStatus,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,247 +1,41 @@
|
||||
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);
|
||||
const {
|
||||
loadingStatus,
|
||||
addingStatus: addVideoStatus,
|
||||
deletingStatus: deleteVideoStatus,
|
||||
updatingStatus: updateVideoStatus,
|
||||
errors: errorMessages,
|
||||
} = useSelector((state) => state.videos);
|
||||
|
||||
const handleErrorReset = (error) => dispatch(resetErrors(error));
|
||||
|
||||
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) => (
|
||||
<VideoInfoModalSidebar video={video} activeTab={activeTab} setActiveTab={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 (
|
||||
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
||||
@@ -264,65 +58,9 @@ const VideosPage = ({
|
||||
updateFileStatus={updateVideoStatus}
|
||||
loadingStatus={loadingStatus}
|
||||
/>
|
||||
<ActionRow>
|
||||
<div className="h2">
|
||||
<FormattedMessage {...messages.heading} />
|
||||
</div>
|
||||
<ActionRow.Spacer />
|
||||
{isVideoTranscriptEnabled ? (
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
openTranscriptSettings();
|
||||
handleErrorReset({ errorType: 'transcript' });
|
||||
}}
|
||||
>
|
||||
<FormattedMessage {...messages.transcriptSettingsButtonLabel} />
|
||||
</Button>
|
||||
) : null}
|
||||
</ActionRow>
|
||||
{loadingStatus !== RequestStatus.FAILED && (
|
||||
<>
|
||||
{isVideoTranscriptEnabled && (
|
||||
<TranscriptSettings
|
||||
{...{
|
||||
isTranscriptSettingsOpen,
|
||||
closeTranscriptSettings,
|
||||
handleErrorReset,
|
||||
errorMessages,
|
||||
transcriptStatus,
|
||||
courseId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FileTable
|
||||
{...{
|
||||
courseId,
|
||||
data,
|
||||
handleAddFile,
|
||||
handleDeleteFile,
|
||||
handleDownloadFile,
|
||||
handleUsagePaths,
|
||||
handleErrorReset,
|
||||
handleFileOrder,
|
||||
tableColumns,
|
||||
maxFileSize,
|
||||
thumbnailPreview,
|
||||
infoModalSidebar,
|
||||
files: videos,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<UploadModal
|
||||
{...{
|
||||
isUploadTrackerOpen,
|
||||
currentUploadingIdsRef: uploadingIdsRef.current,
|
||||
handleUploadCancel,
|
||||
addVideoStatus,
|
||||
}}
|
||||
/>
|
||||
<EditVideoAlertsSlot />
|
||||
<h2>{intl.formatMessage(messages.heading)}</h2>
|
||||
<CourseVideosSlot />
|
||||
</Container>
|
||||
</VideosPageProvider>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<VideosPage courseId={courseId} />
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<MemoryRouter initialEntries={[`/course/${courseId}/videos`]}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/course/:courseId/*"
|
||||
element={
|
||||
<VideosPage courseId={courseId} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
41
src/plugin-slots/CourseFilesSlot/README.md
Normal file
41
src/plugin-slots/CourseFilesSlot/README.md
Normal file
@@ -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}) => (
|
||||
<div style={{border:'thick dashed red'}}>
|
||||
{component}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
11
src/plugin-slots/CourseFilesSlot/index.tsx
Normal file
11
src/plugin-slots/CourseFilesSlot/index.tsx
Normal file
@@ -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 = () => (
|
||||
<PluginSlot id="org.openedx.frontend.authoring.files_upload_page_table.v1">
|
||||
<CourseFilesTable />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
export default CourseFilesSlot;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
44
src/plugin-slots/CourseOutlinePageAlertsSlot/README.md
Normal file
44
src/plugin-slots/CourseOutlinePageAlertsSlot/README.md
Normal file
@@ -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: () => (
|
||||
<Alert variant="warning">
|
||||
This is a test alert
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
5
src/plugin-slots/CourseOutlinePageAlertsSlot/index.tsx
Normal file
5
src/plugin-slots/CourseOutlinePageAlertsSlot/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import React from 'react';
|
||||
|
||||
const CourseOutlinePageAlertsSlot = () => <PluginSlot id="org.openedx.frontend.authoring.course_outline_page_alerts.v1" />;
|
||||
export default CourseOutlinePageAlertsSlot;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
42
src/plugin-slots/CourseVideosSlot/README.md
Normal file
42
src/plugin-slots/CourseVideosSlot/README.md
Normal file
@@ -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}) => (
|
||||
<div style={{border:'thick dashed red'}}>
|
||||
{component}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
11
src/plugin-slots/CourseVideosSlot/index.tsx
Normal file
11
src/plugin-slots/CourseVideosSlot/index.tsx
Normal file
@@ -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 = () => (
|
||||
<PluginSlot id="org.openedx.frontend.authoring.videos_upload_page_table.v1">
|
||||
<CourseVideosTable />
|
||||
</PluginSlot>
|
||||
);
|
||||
|
||||
export default CourseVideosSlot;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
44
src/plugin-slots/EditFileAlertsSlot/README.md
Normal file
44
src/plugin-slots/EditFileAlertsSlot/README.md
Normal file
@@ -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: () => (
|
||||
<Alert variant="warning">
|
||||
This is a test alert
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
5
src/plugin-slots/EditFileAlertsSlot/index.tsx
Normal file
5
src/plugin-slots/EditFileAlertsSlot/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const EditFileAlertsSlot = () => <PluginSlot id="org.openedx.frontend.authoring.edit_file_alerts.v1" />;
|
||||
|
||||
export default EditFileAlertsSlot;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
44
src/plugin-slots/EditVideoAlertsSlot/README.md
Normal file
44
src/plugin-slots/EditVideoAlertsSlot/README.md
Normal file
@@ -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: () => (
|
||||
<Alert variant="warning">
|
||||
This is a test alert
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
5
src/plugin-slots/EditVideoAlertsSlot/index.tsx
Normal file
5
src/plugin-slots/EditVideoAlertsSlot/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const EditVideoAlertsSlot = () => <PluginSlot id="org.openedx.frontend.authoring.edit_video_alerts.v1" />;
|
||||
|
||||
export default EditVideoAlertsSlot;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user