fix: solve conflicts
This commit is contained in:
9
.env
9
.env
@@ -9,7 +9,6 @@ EXAMS_BASE_URL=''
|
||||
FAVICON_URL=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
LMS_BASE_URL=''
|
||||
PREVIEW_BASE_URL=''
|
||||
LEARNING_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
@@ -32,11 +31,9 @@ ENABLE_ACCESSIBILITY_PAGE=false
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
ENABLE_NEW_COURSE_OUTLINE_PAGE = false
|
||||
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
|
||||
ENABLE_UNIT_PAGE = false
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES = false
|
||||
ENABLE_UNIT_PAGE=false
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=false
|
||||
BBB_LEARN_MORE_URL=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION=6
|
||||
|
||||
@@ -33,13 +33,12 @@ ENABLE_ACCESSIBILITY_PAGE=false
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES = true
|
||||
ENABLE_UNIT_PAGE=false
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=false
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
HOTJAR_APP_ID=''
|
||||
HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=true
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
|
||||
PREVIEW_BASE_URL='http://preview.localhost:18000'
|
||||
|
||||
@@ -8,7 +8,6 @@ EXAMS_BASE_URL=
|
||||
FAVICON_URL='https://edx-cdn.org/v3/default/favicon.ico'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
PREVIEW_BASE_URL='http://preview.localhost:18000'
|
||||
LEARNING_BASE_URL='http://localhost:2000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
@@ -30,10 +29,8 @@ USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS=false
|
||||
ENABLE_TEAM_TYPE_SETTING=false
|
||||
ENABLE_NEW_EDITOR_PAGES=true
|
||||
ENABLE_NEW_COURSE_OUTLINE_PAGE = true
|
||||
ENABLE_NEW_VIDEO_UPLOAD_PAGE = true
|
||||
ENABLE_UNIT_PAGE = true
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = true
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES = true
|
||||
ENABLE_UNIT_PAGE=true
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig(
|
||||
'eslint',
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ temp/babel-plugin-react-intl
|
||||
/temp
|
||||
/.vscode
|
||||
/module.config.js
|
||||
|
||||
# Local environment overrides
|
||||
.env.private
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
|
||||
17682
package-lock.json
generated
17682
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -38,14 +38,14 @@
|
||||
"dependencies": {
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-ai-translations": "^1.4.0",
|
||||
"@edx/frontend-component-footer": "^12.3.0",
|
||||
"@edx/frontend-component-header": "^4.7.0",
|
||||
"@edx/frontend-component-ai-translations": "^2.0.0",
|
||||
"@edx/frontend-component-footer": "^13.0.2",
|
||||
"@edx/frontend-component-header": "^5.0.2",
|
||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||
"@edx/frontend-lib-content-components": "^1.178.2",
|
||||
"@edx/frontend-platform": "5.6.1",
|
||||
"@edx/frontend-lib-content-components": "^2.0.0",
|
||||
"@edx/frontend-platform": "7.0.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/paragon": "^21.5.6",
|
||||
"@openedx/paragon": "^21.5.7",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
@@ -53,6 +53,7 @@
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
"broadcast-channel": "^7.0.0",
|
||||
"classnames": "2.2.6",
|
||||
"core-js": "3.8.1",
|
||||
"email-validator": "2.0.4",
|
||||
@@ -80,15 +81,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-build": "13.0.5",
|
||||
"@edx/react-unit-test-utils": "^1.7.0",
|
||||
"@openedx/frontend-build": "13.0.27",
|
||||
"@edx/react-unit-test-utils": "^2.0.0",
|
||||
"@edx/reactifex": "^1.0.3",
|
||||
"@edx/stylelint-config-edx": "^2.3.0",
|
||||
"@edx/stylelint-config-edx": "2.3.0",
|
||||
"@edx/typescript-config": "^1.0.1",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"axios": "^0.27.2",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"glob": "7.2.3",
|
||||
"husky": "7.0.4",
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import {
|
||||
Navigate, Routes, Route, useParams,
|
||||
} from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { PageWrap } from '@edx/frontend-platform/react';
|
||||
import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import { PagesAndResources } from './pages-and-resources';
|
||||
@@ -18,6 +19,7 @@ import { CourseUpdates } from './course-updates';
|
||||
import { CourseUnit } from './course-unit';
|
||||
import CourseExportPage from './export-page/CourseExportPage';
|
||||
import CourseImportPage from './import-page/CourseImportPage';
|
||||
import { DECODED_ROUTES } from './constants';
|
||||
|
||||
/**
|
||||
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||
@@ -55,7 +57,7 @@ const CourseAuthoringRoutes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="videos"
|
||||
element={process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' ? <PageWrap><VideosPage courseId={courseId} /></PageWrap> : null}
|
||||
element={getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true' ? <PageWrap><VideosPage courseId={courseId} /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="pages-and-resources/*"
|
||||
@@ -69,17 +71,19 @@ const CourseAuthoringRoutes = () => {
|
||||
path="custom-pages/*"
|
||||
element={<PageWrap><CustomPages courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="/container/:blockId"
|
||||
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
{DECODED_ROUTES.COURSE_UNIT.map((path) => (
|
||||
<Route
|
||||
path={path}
|
||||
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
))}
|
||||
<Route
|
||||
path="editor/course-videos/:blockId"
|
||||
element={process.env.ENABLE_NEW_EDITOR_PAGES === 'true' ? <PageWrap><VideoSelectorContainer courseId={courseId} /></PageWrap> : null}
|
||||
element={getConfig().ENABLE_NEW_EDITOR_PAGES === 'true' ? <PageWrap><VideoSelectorContainer courseId={courseId} /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="editor/:blockType/:blockId?"
|
||||
element={process.env.ENABLE_NEW_EDITOR_PAGES === 'true' ? <PageWrap><EditorContainer courseId={courseId} /></PageWrap> : null}
|
||||
element={getConfig().ENABLE_NEW_EDITOR_PAGES === 'true' ? <PageWrap><EditorContainer courseId={courseId} /></PageWrap> : null}
|
||||
/>
|
||||
<Route
|
||||
path="settings/details"
|
||||
|
||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
Container, Button, Layout, StatefulButton, TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
import { CheckCircle, Info, Warning } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { CheckCircle, Info, Warning } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import Placeholder from '@edx/frontend-lib-content-components';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
import { ActionRow, AlertModal, Button } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ModalErrorListItem from './ModalErrorListItem';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import { Alert, Icon } from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
import { transformKeysToCamelCase } from '../../utils';
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
IconButton,
|
||||
ModalPopup,
|
||||
useToggle,
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline, Warning } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline, Warning } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { capitalize } from 'lodash';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -20,9 +20,12 @@ export const BADGE_STATES = {
|
||||
};
|
||||
|
||||
export const NOTIFICATION_MESSAGES = {
|
||||
adding: 'Adding',
|
||||
saving: 'Saving',
|
||||
duplicating: 'Duplicating',
|
||||
deleting: 'Deleting',
|
||||
copying: 'Copying',
|
||||
pasting: 'Pasting',
|
||||
empty: '',
|
||||
};
|
||||
|
||||
@@ -35,3 +38,10 @@ export const COURSE_CREATOR_STATES = {
|
||||
denied: 'denied',
|
||||
disallowedForThisSite: 'disallowed_for_this_site',
|
||||
};
|
||||
|
||||
export const DECODED_ROUTES = {
|
||||
COURSE_UNIT: [
|
||||
'/container/:blockId/:sequenceId',
|
||||
'/container/:blockId',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ModalPopup,
|
||||
useToggle,
|
||||
SearchField,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
import React from 'react';
|
||||
import { useCheckboxSetValues } from '@edx/paragon';
|
||||
import { useCheckboxSetValues } from '@openedx/paragon';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { useContentTaxonomyTagsUpdater } from './data/apiHooks';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Container,
|
||||
CloseButton,
|
||||
Spinner,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
Icon,
|
||||
Spinner,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { ArrowDropDown, ArrowDropUp } from '@edx/paragon/icons';
|
||||
import { ArrowDropDown, ArrowDropUp } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from './messages';
|
||||
import './ContentTagsDropDownSelector.scss';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Chip,
|
||||
} from '@edx/paragon';
|
||||
import { Tag, Close } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { Tag, Close } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import TagOutlineIcon from './TagOutlineIcon';
|
||||
|
||||
@@ -75,8 +75,8 @@ export async function getContentData(contentId) {
|
||||
* @returns {Promise<import("./types.mjs").ContentTaxonomyTagsData>}
|
||||
*/
|
||||
export async function updateContentTaxonomyTags(contentId, taxonomyId, tags) {
|
||||
let url = getContentTaxonomyTagsApiUrl(contentId);
|
||||
url = `${url}&taxonomy=${taxonomyId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().put(url, { tags });
|
||||
const url = getContentTaxonomyTagsApiUrl(contentId);
|
||||
const params = { taxonomy: taxonomyId };
|
||||
const { data } = await getAuthenticatedHttpClient().put(url, { tags }, { params });
|
||||
return camelCaseObject(data[contentId]);
|
||||
}
|
||||
|
||||
@@ -110,10 +110,10 @@ describe('content tags drawer api calls', () => {
|
||||
const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b';
|
||||
const taxonomyId = 3;
|
||||
const tags = ['flat taxonomy tag 100', 'flat taxonomy tag 3856'];
|
||||
axiosMock.onPut(`${getContentTaxonomyTagsApiUrl(contentId)}&taxonomy=${taxonomyId}`).reply(200, updateContentTaxonomyTagsMock);
|
||||
axiosMock.onPut(`${getContentTaxonomyTagsApiUrl(contentId)}`).reply(200, updateContentTaxonomyTagsMock);
|
||||
const result = await updateContentTaxonomyTags(contentId, taxonomyId, tags);
|
||||
|
||||
expect(axiosMock.history.put[0].url).toEqual(`${getContentTaxonomyTagsApiUrl(contentId)}&taxonomy=${taxonomyId}`);
|
||||
expect(axiosMock.history.put[0].url).toEqual(`${getContentTaxonomyTagsApiUrl(contentId)}`);
|
||||
expect(result).toEqual(updateContentTaxonomyTagsMock[contentId]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,18 +7,15 @@ import {
|
||||
Layout,
|
||||
Row,
|
||||
TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import {
|
||||
Add as IconAdd,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
DraggableList,
|
||||
ErrorAlert,
|
||||
} from '@edx/frontend-lib-content-components';
|
||||
import { DraggableList } from '@edx/frontend-lib-content-components';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
@@ -41,6 +38,7 @@ import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
|
||||
import PublishModal from './publish-modal/PublishModal';
|
||||
import ConfigureModal from './configure-modal/ConfigureModal';
|
||||
import DeleteModal from './delete-modal/DeleteModal';
|
||||
import PageAlerts from './page-alerts/PageAlerts';
|
||||
import { useCourseOutline } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -53,6 +51,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
statusBarData,
|
||||
courseActions,
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading,
|
||||
isReIndexShow,
|
||||
showErrorAlert,
|
||||
@@ -94,6 +93,17 @@ const CourseOutline = ({ courseId }) => {
|
||||
handleSubsectionDragAndDrop,
|
||||
handleVideoSharingOptionChange,
|
||||
handleUnitDragAndDrop,
|
||||
handleCopyToClipboardClick,
|
||||
handlePasteClipboardClick,
|
||||
notificationDismissUrl,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
mfeProctoredExamSettingsUrl,
|
||||
handleDismissNotification,
|
||||
advanceSettingsUrl,
|
||||
} = useCourseOutline({ courseId });
|
||||
|
||||
const [sections, setSections] = useState(sectionsList);
|
||||
@@ -247,9 +257,18 @@ const CourseOutline = ({ courseId }) => {
|
||||
</Helmet>
|
||||
<Container size="xl" className="px-4">
|
||||
<section className="course-outline-container mb-4 mt-5">
|
||||
<ErrorAlert hideHeading isError={savingStatus === RequestStatus.FAILED}>
|
||||
{intl.formatMessage(messages.alertFailedGeneric, { actionName: 'save', type: 'changes' })}
|
||||
</ErrorAlert>
|
||||
<PageAlerts
|
||||
notificationDismissUrl={notificationDismissUrl}
|
||||
handleDismissNotification={handleDismissNotification}
|
||||
discussionsSettings={discussionsSettings}
|
||||
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
|
||||
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
|
||||
deprecatedBlocksInfo={deprecatedBlocksInfo}
|
||||
proctoringErrors={proctoringErrors}
|
||||
mfeProctoredExamSettingsUrl={mfeProctoredExamSettingsUrl}
|
||||
advanceSettingsUrl={advanceSettingsUrl}
|
||||
savingStatus={savingStatus}
|
||||
/>
|
||||
<TransitionReplace>
|
||||
{showSuccessAlert ? (
|
||||
<AlertMessage
|
||||
@@ -309,6 +328,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
section={section}
|
||||
index={sectionIndex}
|
||||
canMoveItem={canMoveItem(sections)}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
savingStatus={savingStatus}
|
||||
onOpenHighlightsModal={handleOpenHighlightsModal}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
@@ -332,6 +353,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
subsection={subsection}
|
||||
index={subsectionIndex}
|
||||
canMoveItem={canMoveItem(section.childInfo.children)}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
savingStatus={savingStatus}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
onOpenDeleteModal={openDeleteModal}
|
||||
@@ -344,6 +367,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
section,
|
||||
section.childInfo.children,
|
||||
)}
|
||||
onPasteClick={handlePasteClipboardClick}
|
||||
>
|
||||
<DraggableList
|
||||
itemList={subsection.childInfo.children}
|
||||
@@ -356,6 +380,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
unit={unit}
|
||||
subsection={subsection}
|
||||
section={section}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
index={unitIndex}
|
||||
canMoveItem={canMoveItem(subsection.childInfo.children)}
|
||||
savingStatus={savingStatus}
|
||||
@@ -372,6 +398,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
subsection,
|
||||
subsection.childInfo.children,
|
||||
)}
|
||||
onCopyToClipboardClick={handleCopyToClipboardClick}
|
||||
discussionsSettings={discussionsSettings}
|
||||
/>
|
||||
))}
|
||||
</DraggableList>
|
||||
|
||||
@@ -9,3 +9,5 @@
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/ConditionalSortableElement";
|
||||
@import "./xblock-status/XBlockStatus";
|
||||
@import "./paste-button/PasteButton";
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import {
|
||||
act, render, waitFor, cleanup, fireEvent, within,
|
||||
act, render, waitFor, fireEvent, within,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import {
|
||||
getCourseBestPracticesApiUrl,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
getCourseBlockApiUrl,
|
||||
getCourseItemApiUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
getClipboardUrl,
|
||||
} from './data/api';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import {
|
||||
@@ -42,6 +44,9 @@ import cardHeaderMessages from './card-header/messages';
|
||||
import enableHighlightsModalMessages from './enable-highlights-modal/messages';
|
||||
import statusBarMessages from './status-bar/messages';
|
||||
import configureModalMessages from './configure-modal/messages';
|
||||
import pasteButtonMessages from './paste-button/messages';
|
||||
import subsectionMessages from './subsection-card/messages';
|
||||
import pageAlertMessages from './page-alerts/messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -66,6 +71,13 @@ jest.mock('../help-urls/hooks', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage: (message) => message.defaultMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
@@ -115,7 +127,7 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check video sharing option udpates correctly', async () => {
|
||||
const { findByTestId } = render(<RootWrapper />);
|
||||
const { findByLabelText } = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseBlockApiUrl(courseId), {
|
||||
@@ -124,13 +136,10 @@ describe('<CourseOutline />', () => {
|
||||
},
|
||||
})
|
||||
.reply(200);
|
||||
const optionDropdownWrapper = await findByTestId('video-sharing-wrapper');
|
||||
const optionDropdown = await within(optionDropdownWrapper).findByRole('button');
|
||||
await act(async () => fireEvent.click(optionDropdown));
|
||||
const allOffOption = await within(optionDropdownWrapper).findByText(
|
||||
statusBarMessages.videoSharingAllOffText.defaultMessage,
|
||||
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
|
||||
await act(
|
||||
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
|
||||
);
|
||||
await act(async () => fireEvent.click(allOffOption));
|
||||
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
|
||||
@@ -141,7 +150,7 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check video sharing option shows error on failure', async () => {
|
||||
const { findByTestId, queryByRole } = render(<RootWrapper />);
|
||||
const { findByLabelText, queryByRole } = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseBlockApiUrl(courseId), {
|
||||
@@ -150,13 +159,10 @@ describe('<CourseOutline />', () => {
|
||||
},
|
||||
})
|
||||
.reply(500);
|
||||
const optionDropdownWrapper = await findByTestId('video-sharing-wrapper');
|
||||
const optionDropdown = await within(optionDropdownWrapper).findByRole('button');
|
||||
await act(async () => fireEvent.click(optionDropdown));
|
||||
const allOffOption = await within(optionDropdownWrapper).findByText(
|
||||
statusBarMessages.videoSharingAllOffText.defaultMessage,
|
||||
const optionDropdown = await findByLabelText(statusBarMessages.videoSharingTitle.defaultMessage);
|
||||
await act(
|
||||
async () => fireEvent.change(optionDropdown, { target: { value: VIDEO_SHARING_OPTIONS.allOff } }),
|
||||
);
|
||||
await act(async () => fireEvent.click(allOffOption));
|
||||
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
|
||||
@@ -165,7 +171,10 @@ describe('<CourseOutline />', () => {
|
||||
},
|
||||
}));
|
||||
|
||||
expect(queryByRole('alert')).toBeInTheDocument();
|
||||
const alertElement = queryByRole('alert');
|
||||
expect(alertElement).toHaveTextContent(
|
||||
pageAlertMessages.alertFailedGeneric.defaultMessage,
|
||||
);
|
||||
});
|
||||
|
||||
it('render error alert after failed reindex correctly', async () => {
|
||||
@@ -334,7 +343,6 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('render CourseOutline component without sections correctly', async () => {
|
||||
cleanup();
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(200, courseOutlineIndexWithoutSections);
|
||||
@@ -346,6 +354,25 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('render configuration alerts and check dismiss query', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseOutlineIndexMock,
|
||||
notificationDismissUrl: '/some/url',
|
||||
});
|
||||
|
||||
const { findByRole } = render(<RootWrapper />);
|
||||
expect(await findByRole('alert')).toBeInTheDocument();
|
||||
const dismissBtn = await findByRole('button', { name: 'Dismiss' });
|
||||
axiosMock
|
||||
.onDelete('/some/url')
|
||||
.reply(204);
|
||||
fireEvent.click(dismissBtn);
|
||||
|
||||
expect(axiosMock.history.delete.length).toBe(1);
|
||||
});
|
||||
|
||||
it('check edit title works for section, subsection and unit', async () => {
|
||||
const { findAllByTestId } = render(<RootWrapper />);
|
||||
const checkEditTitle = async (section, element, item, newName, elementName) => {
|
||||
@@ -542,7 +569,7 @@ describe('<CourseOutline />', () => {
|
||||
|
||||
const checkPublishBtn = async (item, element, elementName) => {
|
||||
expect(
|
||||
await within(element).findByTestId(`${elementName}-card-header__badge-status`),
|
||||
(await within(element).getAllByRole('status'))[0],
|
||||
`Failed for ${elementName}!`,
|
||||
).toHaveTextContent(cardHeaderMessages.statusBadgeDraft.defaultMessage);
|
||||
|
||||
@@ -598,7 +625,7 @@ describe('<CourseOutline />', () => {
|
||||
await act(async () => fireEvent.click(confirmButton));
|
||||
|
||||
expect(
|
||||
await within(element).findByTestId(`${elementName}-card-header__badge-status`),
|
||||
(await within(element).getAllByRole('status'))[0],
|
||||
`Failed for ${elementName}!`,
|
||||
).toHaveTextContent(cardHeaderMessages.statusBadgePublishedNotLive.defaultMessage);
|
||||
};
|
||||
@@ -669,43 +696,43 @@ describe('<CourseOutline />', () => {
|
||||
findAllByTestId,
|
||||
findByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
|
||||
const subsection = section.childInfo.children[0];
|
||||
const newReleaseDate = '2025-08-10T05:00:00Z';
|
||||
const newGraderType = 'Homework';
|
||||
const newDue = '2025-09-10T00:00:00Z';
|
||||
const isTimeLimited = true;
|
||||
const defaultTimeLimitMinutes = 3270;
|
||||
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
|
||||
const [subsection] = section.childInfo.children;
|
||||
const expectedRequestData = {
|
||||
publish: 'republish',
|
||||
graderType: 'Homework',
|
||||
isPrereq: false,
|
||||
prereqMinScore: 100,
|
||||
prereqMinCompletion: 100,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
due: '2025-09-10T05:00:00Z',
|
||||
hide_after_due: true,
|
||||
show_correctness: 'always',
|
||||
is_practice_exam: false,
|
||||
is_time_limited: true,
|
||||
is_proctored_enabled: false,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: 3270,
|
||||
is_onboarding_exam: false,
|
||||
start: '2025-08-10T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseItemApiUrl(subsection.id), {
|
||||
publish: 'republish',
|
||||
graderType: newGraderType,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
due: newDue,
|
||||
hide_after_due: false,
|
||||
show_correctness: 'always',
|
||||
is_practice_exam: false,
|
||||
is_time_limited: isTimeLimited,
|
||||
exam_review_rules: '',
|
||||
is_proctored_enabled: false,
|
||||
default_time_limit_minutes: defaultTimeLimitMinutes,
|
||||
is_onboarding_exam: false,
|
||||
start: newReleaseDate,
|
||||
},
|
||||
})
|
||||
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const [currentSection] = await findAllByTestId('section-card');
|
||||
const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
|
||||
const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
|
||||
|
||||
subsection.start = newReleaseDate;
|
||||
subsection.due = newDue;
|
||||
subsection.format = newGraderType;
|
||||
subsection.isTimeLimited = isTimeLimited;
|
||||
subsection.defaultTimeLimitMinutes = defaultTimeLimitMinutes;
|
||||
subsection.start = expectedRequestData.metadata.start;
|
||||
subsection.due = expectedRequestData.metadata.due;
|
||||
subsection.format = expectedRequestData.graderType;
|
||||
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
|
||||
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
|
||||
subsection.hideAfterDue = expectedRequestData.metadata.hideAfterDue;
|
||||
section.childInfo.children[0] = subsection;
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
@@ -717,15 +744,25 @@ describe('<CourseOutline />', () => {
|
||||
|
||||
// update fields
|
||||
let configureModal = await findByTestId('configure-modal');
|
||||
expect(await within(configureModal).findByText(newGraderType)).toBeInTheDocument();
|
||||
expect(await within(configureModal).findByText(expectedRequestData.graderType)).toBeInTheDocument();
|
||||
let releaseDateStack = await within(configureModal).findByTestId('release-date-stack');
|
||||
let releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
|
||||
fireEvent.change(releaseDatePicker, { target: { value: '08/10/2025' } });
|
||||
let releaseDateTimePicker = await within(releaseDateStack).findByPlaceholderText('HH:MM');
|
||||
fireEvent.change(releaseDateTimePicker, { target: { value: '00:00' } });
|
||||
let dueDateStack = await within(configureModal).findByTestId('due-date-stack');
|
||||
let dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY');
|
||||
fireEvent.change(dueDatePicker, { target: { value: '09/10/2025' } });
|
||||
let dueDateTimePicker = await within(dueDateStack).findByPlaceholderText('HH:MM');
|
||||
fireEvent.change(dueDateTimePicker, { target: { value: '05:00' } });
|
||||
let graderTypeDropdown = await within(configureModal).findByTestId('grader-type-select');
|
||||
fireEvent.change(graderTypeDropdown, { target: { value: newGraderType } });
|
||||
fireEvent.change(graderTypeDropdown, { target: { value: expectedRequestData.graderType } });
|
||||
|
||||
// visibility tab
|
||||
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(visibilityRadioButtons[1]);
|
||||
|
||||
let advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
|
||||
fireEvent.click(advancedTab);
|
||||
@@ -739,23 +776,7 @@ describe('<CourseOutline />', () => {
|
||||
|
||||
// verify request
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
|
||||
publish: 'republish',
|
||||
graderType: newGraderType,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
due: newDue,
|
||||
hide_after_due: false,
|
||||
show_correctness: 'always',
|
||||
is_practice_exam: false,
|
||||
is_time_limited: isTimeLimited,
|
||||
exam_review_rules: '',
|
||||
is_proctored_enabled: false,
|
||||
default_time_limit_minutes: defaultTimeLimitMinutes,
|
||||
is_onboarding_exam: false,
|
||||
start: newReleaseDate,
|
||||
},
|
||||
}));
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
|
||||
|
||||
// reopen modal and check values
|
||||
await act(async () => fireEvent.click(subsectionDropdownButton));
|
||||
@@ -765,11 +786,15 @@ describe('<CourseOutline />', () => {
|
||||
releaseDateStack = await within(configureModal).findByTestId('release-date-stack');
|
||||
releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
|
||||
expect(releaseDatePicker).toHaveValue('08/10/2025');
|
||||
releaseDateTimePicker = await within(releaseDateStack).findByPlaceholderText('HH:MM');
|
||||
expect(releaseDateTimePicker).toHaveValue('00:00');
|
||||
dueDateStack = await await within(configureModal).findByTestId('due-date-stack');
|
||||
dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY');
|
||||
expect(dueDatePicker).toHaveValue('09/10/2025');
|
||||
dueDateTimePicker = await within(dueDateStack).findByPlaceholderText('HH:MM');
|
||||
expect(dueDateTimePicker).toHaveValue('05:00');
|
||||
graderTypeDropdown = await within(configureModal).findByTestId('grader-type-select');
|
||||
expect(graderTypeDropdown).toHaveValue(newGraderType);
|
||||
expect(graderTypeDropdown).toHaveValue(expectedRequestData.graderType);
|
||||
|
||||
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
|
||||
fireEvent.click(advancedTab);
|
||||
@@ -781,6 +806,444 @@ describe('<CourseOutline />', () => {
|
||||
expect(hours).toHaveValue('54:30');
|
||||
});
|
||||
|
||||
it('check prereq and proctoring settings in configure modal for subsection', async () => {
|
||||
const {
|
||||
findAllByTestId,
|
||||
findByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
|
||||
const [subsection, secondSubsection] = section.childInfo.children;
|
||||
const expectedRequestData = {
|
||||
publish: 'republish',
|
||||
graderType: 'notgraded',
|
||||
isPrereq: true,
|
||||
prereqUsageKey: secondSubsection.id,
|
||||
prereqMinScore: 80,
|
||||
prereqMinCompletion: 90,
|
||||
metadata: {
|
||||
visible_to_staff_only: true,
|
||||
due: '',
|
||||
hide_after_due: false,
|
||||
show_correctness: 'always',
|
||||
is_practice_exam: false,
|
||||
is_time_limited: true,
|
||||
is_proctored_enabled: true,
|
||||
exam_review_rules: 'some rules for proctored exams',
|
||||
default_time_limit_minutes: 30,
|
||||
is_onboarding_exam: false,
|
||||
start: '1970-01-01T05:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const [currentSection] = await findAllByTestId('section-card');
|
||||
const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
|
||||
const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
|
||||
|
||||
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
|
||||
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
|
||||
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
|
||||
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
|
||||
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
|
||||
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
|
||||
subsection.isPrereq = expectedRequestData.isPrereq;
|
||||
subsection.prereq = expectedRequestData.prereqUsageKey;
|
||||
subsection.prereqMinScore = expectedRequestData.prereqMinScore;
|
||||
subsection.prereqMinCompletion = expectedRequestData.prereqMinCompletion;
|
||||
section.childInfo.children[0] = subsection;
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, section);
|
||||
|
||||
fireEvent.click(subsectionDropdownButton);
|
||||
const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button');
|
||||
fireEvent.click(configureBtn);
|
||||
|
||||
// update fields
|
||||
let configureModal = await findByTestId('configure-modal');
|
||||
let advancedTab = await within(configureModal).findByRole(
|
||||
'tab',
|
||||
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
|
||||
);
|
||||
|
||||
// visibility tab
|
||||
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(visibilityRadioButtons[2]);
|
||||
|
||||
fireEvent.click(advancedTab);
|
||||
let radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(radioButtons[2]);
|
||||
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
|
||||
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
|
||||
fireEvent.change(hours, { target: { value: '00:30' } });
|
||||
// select a prerequisite
|
||||
const prereqSelect = await within(configureModal).findByRole('combobox');
|
||||
fireEvent.change(prereqSelect, { target: { value: expectedRequestData.prereqUsageKey } });
|
||||
|
||||
// update minimum score and completion percentage
|
||||
let prereqMinScoreInput = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.minScoreLabel.defaultMessage,
|
||||
);
|
||||
fireEvent.change(prereqMinScoreInput, { target: { value: expectedRequestData.prereqMinScore } });
|
||||
let prereqMinCompletionInput = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.minCompletionLabel.defaultMessage,
|
||||
);
|
||||
fireEvent.change(prereqMinCompletionInput, { target: { value: expectedRequestData.prereqMinCompletion } });
|
||||
|
||||
// enable this subsection to be used as prerequisite by other subsections
|
||||
let prereqCheckbox = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.prereqCheckboxLabel.defaultMessage,
|
||||
);
|
||||
fireEvent.click(prereqCheckbox);
|
||||
|
||||
// fill some rules for proctored exams
|
||||
let examsRulesInput = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.reviewRulesLabel.defaultMessage,
|
||||
);
|
||||
fireEvent.change(examsRulesInput, { target: { value: expectedRequestData.metadata.exam_review_rules } });
|
||||
|
||||
const saveButton = await within(configureModal).findByTestId('configure-save-button');
|
||||
await act(async () => fireEvent.click(saveButton));
|
||||
|
||||
// verify request
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
|
||||
|
||||
// reopen modal and check values
|
||||
await act(async () => fireEvent.click(subsectionDropdownButton));
|
||||
await act(async () => fireEvent.click(configureBtn));
|
||||
|
||||
configureModal = await findByTestId('configure-modal');
|
||||
advancedTab = await within(configureModal).findByRole('tab', {
|
||||
name: configureModalMessages.advancedTabTitle.defaultMessage,
|
||||
});
|
||||
fireEvent.click(advancedTab);
|
||||
radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
expect(radioButtons[0]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[1]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[2]).toHaveProperty('checked', true);
|
||||
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
|
||||
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
|
||||
expect(hours).toHaveValue('00:30');
|
||||
prereqCheckbox = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.prereqCheckboxLabel.defaultMessage,
|
||||
);
|
||||
expect(prereqCheckbox).toBeChecked();
|
||||
const prereqSelectOption = await within(configureModal).findByRole('option', { selected: true });
|
||||
expect(prereqSelectOption).toHaveAttribute('value', expectedRequestData.prereqUsageKey);
|
||||
examsRulesInput = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.reviewRulesLabel.defaultMessage,
|
||||
);
|
||||
expect(examsRulesInput).toHaveTextContent(expectedRequestData.metadata.exam_review_rules);
|
||||
|
||||
prereqMinScoreInput = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.minScoreLabel.defaultMessage,
|
||||
);
|
||||
expect(prereqMinScoreInput).toHaveAttribute('value', `${expectedRequestData.prereqMinScore}`);
|
||||
prereqMinCompletionInput = await within(configureModal).findByLabelText(
|
||||
configureModalMessages.minCompletionLabel.defaultMessage,
|
||||
);
|
||||
expect(prereqMinCompletionInput).toHaveAttribute('value', `${expectedRequestData.prereqMinCompletion}`);
|
||||
});
|
||||
|
||||
it('check practice proctoring settings in configure modal', async () => {
|
||||
const {
|
||||
findAllByTestId,
|
||||
findByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
|
||||
const [subsection] = section.childInfo.children;
|
||||
const expectedRequestData = {
|
||||
publish: 'republish',
|
||||
graderType: 'notgraded',
|
||||
isPrereq: false,
|
||||
prereqMinScore: 100,
|
||||
prereqMinCompletion: 100,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
due: '',
|
||||
hide_after_due: false,
|
||||
show_correctness: 'never',
|
||||
is_practice_exam: true,
|
||||
is_time_limited: true,
|
||||
is_proctored_enabled: true,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: 30,
|
||||
is_onboarding_exam: false,
|
||||
start: '1970-01-01T05:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const [currentSection] = await findAllByTestId('section-card');
|
||||
const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
|
||||
const subsectionDropdownButton = await within(firstSubsection).findByTestId('subsection-card-header__menu-button');
|
||||
|
||||
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
|
||||
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
|
||||
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
|
||||
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
|
||||
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
|
||||
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
|
||||
section.childInfo.children[0] = subsection;
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, section);
|
||||
|
||||
fireEvent.click(subsectionDropdownButton);
|
||||
const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button');
|
||||
fireEvent.click(configureBtn);
|
||||
|
||||
// update fields
|
||||
let configureModal = await findByTestId('configure-modal');
|
||||
let advancedTab = await within(configureModal).findByRole(
|
||||
'tab',
|
||||
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
|
||||
);
|
||||
// visibility tab
|
||||
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(visibilityRadioButtons[4]);
|
||||
|
||||
// advancedTab
|
||||
fireEvent.click(advancedTab);
|
||||
let radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(radioButtons[3]);
|
||||
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
|
||||
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
|
||||
fireEvent.change(hours, { target: { value: '00:30' } });
|
||||
|
||||
// rules box should not be visible
|
||||
expect(within(configureModal).queryByLabelText(
|
||||
configureModalMessages.reviewRulesLabel.defaultMessage,
|
||||
)).not.toBeInTheDocument();
|
||||
|
||||
const saveButton = await within(configureModal).findByTestId('configure-save-button');
|
||||
await act(async () => fireEvent.click(saveButton));
|
||||
|
||||
// verify request
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
|
||||
|
||||
// reopen modal and check values
|
||||
await act(async () => fireEvent.click(subsectionDropdownButton));
|
||||
await act(async () => fireEvent.click(configureBtn));
|
||||
|
||||
configureModal = await findByTestId('configure-modal');
|
||||
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
|
||||
fireEvent.click(advancedTab);
|
||||
radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
expect(radioButtons[0]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[1]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[2]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[3]).toHaveProperty('checked', true);
|
||||
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
|
||||
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
|
||||
expect(hours).toHaveValue('00:30');
|
||||
});
|
||||
|
||||
it('check onboarding proctoring settings in configure modal', async () => {
|
||||
const {
|
||||
findAllByTestId,
|
||||
findByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[0]);
|
||||
const [, subsection] = section.childInfo.children;
|
||||
const expectedRequestData = {
|
||||
publish: 'republish',
|
||||
graderType: 'notgraded',
|
||||
isPrereq: true,
|
||||
prereqMinScore: 100,
|
||||
prereqMinCompletion: 100,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
due: '',
|
||||
hide_after_due: false,
|
||||
show_correctness: 'past_due',
|
||||
is_practice_exam: false,
|
||||
is_time_limited: true,
|
||||
is_proctored_enabled: true,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: 30,
|
||||
is_onboarding_exam: true,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const [currentSection] = await findAllByTestId('section-card');
|
||||
const [, secondSubsection] = await within(currentSection).findAllByTestId('subsection-card');
|
||||
const subsectionDropdownButton = await within(secondSubsection).findByTestId('subsection-card-header__menu-button');
|
||||
|
||||
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
|
||||
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
|
||||
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
|
||||
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
|
||||
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
|
||||
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
|
||||
section.childInfo.children[1] = subsection;
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, section);
|
||||
|
||||
fireEvent.click(subsectionDropdownButton);
|
||||
const configureBtn = await within(secondSubsection).findByTestId('subsection-card-header__menu-configure-button');
|
||||
fireEvent.click(configureBtn);
|
||||
|
||||
// update fields
|
||||
let configureModal = await findByTestId('configure-modal');
|
||||
// visibility tab
|
||||
const visibilityTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
const visibilityRadioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(visibilityRadioButtons[5]);
|
||||
|
||||
// advancedTab
|
||||
let advancedTab = await within(configureModal).findByRole(
|
||||
'tab',
|
||||
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
|
||||
);
|
||||
fireEvent.click(advancedTab);
|
||||
let radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(radioButtons[3]);
|
||||
let hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
|
||||
let hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
|
||||
fireEvent.change(hours, { target: { value: '00:30' } });
|
||||
|
||||
// rules box should not be visible
|
||||
expect(within(configureModal).queryByLabelText(
|
||||
configureModalMessages.reviewRulesLabel.defaultMessage,
|
||||
)).not.toBeInTheDocument();
|
||||
|
||||
const saveButton = await within(configureModal).findByTestId('configure-save-button');
|
||||
await act(async () => fireEvent.click(saveButton));
|
||||
|
||||
// verify request
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
|
||||
|
||||
// reopen modal and check values
|
||||
await act(async () => fireEvent.click(subsectionDropdownButton));
|
||||
await act(async () => fireEvent.click(configureBtn));
|
||||
|
||||
configureModal = await findByTestId('configure-modal');
|
||||
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
|
||||
fireEvent.click(advancedTab);
|
||||
radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
expect(radioButtons[0]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[1]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[2]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[3]).toHaveProperty('checked', true);
|
||||
hoursWrapper = await within(configureModal).findByTestId('advanced-tab-hours-picker-wrapper');
|
||||
hours = await within(hoursWrapper).findByPlaceholderText('HH:MM');
|
||||
expect(hours).toHaveValue('00:30');
|
||||
});
|
||||
|
||||
it('check no special exam setting in configure modal', async () => {
|
||||
const {
|
||||
findAllByTestId,
|
||||
findByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
const section = cloneDeep(courseOutlineIndexMock.courseStructure.childInfo.children[1]);
|
||||
const [subsection] = section.childInfo.children;
|
||||
const expectedRequestData = {
|
||||
publish: 'republish',
|
||||
graderType: 'notgraded',
|
||||
prereqMinScore: 100,
|
||||
prereqMinCompletion: 100,
|
||||
metadata: {
|
||||
visible_to_staff_only: null,
|
||||
due: '',
|
||||
hide_after_due: false,
|
||||
show_correctness: 'always',
|
||||
is_practice_exam: false,
|
||||
is_time_limited: false,
|
||||
is_proctored_enabled: false,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: 0,
|
||||
is_onboarding_exam: false,
|
||||
start: '1970-01-01T05:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
axiosMock
|
||||
.onPost(getCourseItemApiUrl(subsection.id), expectedRequestData)
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const [, currentSection] = await findAllByTestId('section-card');
|
||||
const [subsectionElement] = await within(currentSection).findAllByTestId('subsection-card');
|
||||
const subsectionDropdownButton = await within(subsectionElement).findByTestId('subsection-card-header__menu-button');
|
||||
|
||||
subsection.isTimeLimited = expectedRequestData.metadata.is_time_limited;
|
||||
subsection.defaultTimeLimitMinutes = expectedRequestData.metadata.default_time_limit_minutes;
|
||||
subsection.isProctoredExam = expectedRequestData.metadata.is_proctored_enabled;
|
||||
subsection.isPracticeExam = expectedRequestData.metadata.is_practice_exam;
|
||||
subsection.isOnboardingExam = expectedRequestData.metadata.is_onboarding_exam;
|
||||
subsection.examReviewRules = expectedRequestData.metadata.exam_review_rules;
|
||||
section.childInfo.children[0] = subsection;
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, section);
|
||||
|
||||
fireEvent.click(subsectionDropdownButton);
|
||||
const configureBtn = await within(subsectionElement).findByTestId('subsection-card-header__menu-configure-button');
|
||||
fireEvent.click(configureBtn);
|
||||
|
||||
// update fields
|
||||
let configureModal = await findByTestId('configure-modal');
|
||||
|
||||
// advancedTab
|
||||
let advancedTab = await within(configureModal).findByRole(
|
||||
'tab',
|
||||
{ name: configureModalMessages.advancedTabTitle.defaultMessage },
|
||||
);
|
||||
fireEvent.click(advancedTab);
|
||||
let radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
fireEvent.click(radioButtons[0]);
|
||||
|
||||
// time box should not be visible
|
||||
expect(within(configureModal).queryByLabelText(
|
||||
configureModalMessages.timeAllotted.defaultMessage,
|
||||
)).not.toBeInTheDocument();
|
||||
|
||||
// rules box should not be visible
|
||||
expect(within(configureModal).queryByLabelText(
|
||||
configureModalMessages.reviewRulesLabel.defaultMessage,
|
||||
)).not.toBeInTheDocument();
|
||||
|
||||
const saveButton = await within(configureModal).findByTestId('configure-save-button');
|
||||
await act(async () => fireEvent.click(saveButton));
|
||||
|
||||
// verify request
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify(expectedRequestData));
|
||||
|
||||
// reopen modal and check values
|
||||
await act(async () => fireEvent.click(subsectionDropdownButton));
|
||||
await act(async () => fireEvent.click(configureBtn));
|
||||
|
||||
configureModal = await findByTestId('configure-modal');
|
||||
advancedTab = await within(configureModal).findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
|
||||
fireEvent.click(advancedTab);
|
||||
radioButtons = await within(configureModal).findAllByRole('radio');
|
||||
expect(radioButtons[0]).toHaveProperty('checked', true);
|
||||
expect(radioButtons[1]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[2]).toHaveProperty('checked', false);
|
||||
expect(radioButtons[3]).toHaveProperty('checked', false);
|
||||
});
|
||||
|
||||
it('check configure modal for unit', async () => {
|
||||
const { findAllByTestId, findByTestId } = render(<RootWrapper />);
|
||||
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
|
||||
@@ -836,6 +1299,7 @@ describe('<CourseOutline />', () => {
|
||||
},
|
||||
],
|
||||
selectedPartitionIndex: 0,
|
||||
selectedGroupsLabel: '',
|
||||
};
|
||||
subsection.childInfo.children[0] = unit;
|
||||
section.childInfo.children[0] = subsection;
|
||||
@@ -1305,7 +1769,6 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check that drag handle is not visible for non-draggable sections', async () => {
|
||||
cleanup();
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(200, {
|
||||
@@ -1337,4 +1800,79 @@ describe('<CourseOutline />', () => {
|
||||
expect(within(sectionElement).queryByText(section.displayName)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('check whether unit copy & paste option works correctly', async () => {
|
||||
const { findAllByTestId } = render(<RootWrapper />);
|
||||
// get first section -> first subsection -> first unit element
|
||||
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||
const [sectionElement] = await findAllByTestId('section-card');
|
||||
const [subsection] = section.childInfo.children;
|
||||
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
||||
await act(async () => fireEvent.click(expandBtn));
|
||||
const [unit] = subsection.childInfo.children;
|
||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
const expectedClipboardContent = {
|
||||
content: {
|
||||
blockType: 'vertical',
|
||||
blockTypeDisplay: 'Unit',
|
||||
created: '2024-01-29T07:58:36.844249Z',
|
||||
displayName: unit.displayName,
|
||||
id: 15,
|
||||
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/15/olx',
|
||||
purpose: 'clipboard',
|
||||
status: 'ready',
|
||||
userId: 3,
|
||||
},
|
||||
sourceUsageKey: unit.id,
|
||||
sourceContexttitle: courseOutlineIndexMock.courseStructure.displayName,
|
||||
sourceEditUrl: unit.studioUrl,
|
||||
};
|
||||
// mock api call
|
||||
axiosMock
|
||||
.onPost(getClipboardUrl(), {
|
||||
usage_key: unit.id,
|
||||
}).reply(200, expectedClipboardContent);
|
||||
// check that initialUserClipboard state is empty
|
||||
const { initialUserClipboard } = store.getState().courseOutline;
|
||||
expect(initialUserClipboard).toBeUndefined();
|
||||
|
||||
// find menu button and click on it to open menu
|
||||
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
|
||||
await act(async () => fireEvent.click(menu));
|
||||
|
||||
// move first unit back to second position to test move down option
|
||||
const copyButton = await within(unitElement).findByText(cardHeaderMessages.menuCopy.defaultMessage);
|
||||
await act(async () => fireEvent.click(copyButton));
|
||||
|
||||
// check that initialUserClipboard state is updated
|
||||
expect(store.getState().courseOutline.initialUserClipboard).toEqual(expectedClipboardContent);
|
||||
|
||||
[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
// find clipboard content label
|
||||
const clipboardLabel = await within(subsectionElement).findByText(
|
||||
pasteButtonMessages.clipboardContentLabel.defaultMessage,
|
||||
);
|
||||
await act(async () => fireEvent.mouseOver(clipboardLabel));
|
||||
|
||||
// find clipboard content popup link
|
||||
expect(
|
||||
subsectionElement.querySelector('#vertical-paste-button-overlay'),
|
||||
).toHaveAttribute('href', unit.studioUrl);
|
||||
|
||||
// check paste button functionality
|
||||
// mock api call
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(), {
|
||||
parent_locator: subsection.id,
|
||||
staged_content: 'clipboard',
|
||||
}).reply(200, { dummy: 'value' });
|
||||
const pasteBtn = await within(subsectionElement).findByText(subsectionMessages.pasteButton.defaultMessage);
|
||||
await act(async () => fireEvent.click(pasteBtn));
|
||||
|
||||
[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
const lastUnitElement = (await within(subsectionElement).findAllByTestId('unit-card')).slice(-1)[0];
|
||||
expect(lastUnitElement).toHaveTextContent(unit.displayName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ module.exports = {
|
||||
highlightsEnabled: true,
|
||||
highlightsPreviewOnly: false,
|
||||
highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
|
||||
enableProctoredExams: false,
|
||||
enableProctoredExams: true,
|
||||
createZendeskTickets: true,
|
||||
enableTimedExams: true,
|
||||
childInfo: {
|
||||
@@ -152,6 +152,11 @@ module.exports = {
|
||||
due: null,
|
||||
relativeWeeksDue: null,
|
||||
format: null,
|
||||
isPrereq: false,
|
||||
prereqs: [{
|
||||
blockDisplayName: 'Sample Subsection',
|
||||
blockUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@7f75de8dcc261249250b71925f49810f',
|
||||
}],
|
||||
courseGraders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
@@ -261,6 +266,7 @@ module.exports = {
|
||||
ancestorHasStaffLock: true,
|
||||
staffOnlyMessage: false,
|
||||
hasPartitionGroupComponents: false,
|
||||
enableCopyPasteUnits: true,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
@@ -292,6 +298,7 @@ module.exports = {
|
||||
ancestorHasStaffLock: true,
|
||||
staffOnlyMessage: false,
|
||||
hasPartitionGroupComponents: false,
|
||||
enableCopyPasteUnits: true,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
@@ -379,10 +386,11 @@ module.exports = {
|
||||
is_practice_exam: false,
|
||||
is_onboarding_exam: false,
|
||||
is_time_limited: false,
|
||||
isPrereq: true,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: null,
|
||||
proctoring_exam_configuration_link: null,
|
||||
supports_onboarding: false,
|
||||
supports_onboarding: true,
|
||||
show_review_rules: true,
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
@@ -391,7 +399,7 @@ module.exports = {
|
||||
},
|
||||
ancestor_has_staff_lock: false,
|
||||
staff_only_message: false,
|
||||
enable_copy_paste_units: false,
|
||||
enable_copy_paste_units: true,
|
||||
has_partition_group_components: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
@@ -569,12 +577,12 @@ module.exports = {
|
||||
],
|
||||
showCorrectness: 'always',
|
||||
hideAfterDue: false,
|
||||
isProctoredExam: false,
|
||||
isProctoredExam: true,
|
||||
wasExamEverLinkedWithExternal: false,
|
||||
onlineProctoringRules: '',
|
||||
isPracticeExam: false,
|
||||
isOnboardingExam: false,
|
||||
isTimeLimited: false,
|
||||
isTimeLimited: true,
|
||||
examReviewRules: '',
|
||||
defaultTimeLimitMinutes: null,
|
||||
proctoringExamConfigurationLink: null,
|
||||
@@ -3048,7 +3056,7 @@ module.exports = {
|
||||
languageCode: 'en',
|
||||
lmsLink: '//localhost:18000/courses/course-v1:edx+101+y76/jump_to/block-v1:edx+101+y76+type@course+block@course',
|
||||
mfeProctoredExamSettingsUrl: '',
|
||||
notificationDismissUrl: '/course_notifications/course-v1:edx+101+y76/2',
|
||||
notificationDismissUrl: '',
|
||||
proctoringErrors: [],
|
||||
reindexLink: '/course/course-v1:edx+101+y76/search_reindex',
|
||||
rerunNotificationId: 2,
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Truncate } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import { ITEM_BADGE_STATUS } from '../constants';
|
||||
import { getItemStatusBadgeContent } from '../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const BaseTitleWithStatusBadge = ({
|
||||
title,
|
||||
status,
|
||||
namePrefix,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { badgeTitle, badgeIcon } = getItemStatusBadgeContent(status, messages, intl);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Truncate lines={1} className={`${namePrefix}-card-title mb-0`}>{title}</Truncate>
|
||||
{badgeTitle && (
|
||||
<div className="item-card-header__badge-status" data-testid={`${namePrefix}-card-header__badge-status`}>
|
||||
{badgeIcon && (
|
||||
<Icon
|
||||
src={badgeIcon}
|
||||
size="sm"
|
||||
className={classNames({ 'text-success-500': status === ITEM_BADGE_STATUS.live })}
|
||||
/>
|
||||
)}
|
||||
<span className="small">{badgeTitle}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BaseTitleWithStatusBadge.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
namePrefix: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default BaseTitleWithStatusBadge;
|
||||
@@ -5,17 +5,19 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Dropdown,
|
||||
Form,
|
||||
Hyperlink,
|
||||
Icon,
|
||||
IconButton,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
MoreVert as MoveVertIcon,
|
||||
EditOutline as EditIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { useEscapeClick } from '../../hooks';
|
||||
import { ITEM_BADGE_STATUS } from '../constants';
|
||||
import { scrollToElement } from '../utils';
|
||||
import CardStatus from './CardStatus';
|
||||
import messages from './messages';
|
||||
|
||||
const CardHeader = ({
|
||||
@@ -35,9 +37,17 @@ const CardHeader = ({
|
||||
onClickDuplicate,
|
||||
onClickMoveUp,
|
||||
onClickMoveDown,
|
||||
onClickCopy,
|
||||
titleComponent,
|
||||
namePrefix,
|
||||
actions,
|
||||
enableCopyPasteUnits,
|
||||
isVertical,
|
||||
isSequential,
|
||||
proctoringExamConfigurationLink,
|
||||
discussionEnabled,
|
||||
discussionsSettings,
|
||||
parentInfo,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -58,6 +68,17 @@ const CardHeader = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showDiscussionsEnabledBadge = (
|
||||
isVertical
|
||||
&& !parentInfo?.isTimeLimited
|
||||
&& discussionEnabled
|
||||
&& discussionsSettings?.providerType === 'openedx'
|
||||
&& (
|
||||
discussionsSettings?.enableGradedUnits
|
||||
|| (!discussionsSettings?.enableGradedUnits && !parentInfo.graded)
|
||||
)
|
||||
);
|
||||
|
||||
useEscapeClick({
|
||||
onEscape: () => {
|
||||
setTitleValue(title);
|
||||
@@ -73,7 +94,7 @@ const CardHeader = ({
|
||||
ref={cardHeaderRef}
|
||||
>
|
||||
{isFormOpen ? (
|
||||
<Form.Group className="m-0">
|
||||
<Form.Group className="m-0 w-75">
|
||||
<Form.Control
|
||||
data-testid={`${namePrefix}-edit-field`}
|
||||
ref={(e) => e && e.focus()}
|
||||
@@ -91,16 +112,20 @@ const CardHeader = ({
|
||||
/>
|
||||
</Form.Group>
|
||||
) : (
|
||||
titleComponent
|
||||
)}
|
||||
<div className="ml-auto d-flex">
|
||||
{!isFormOpen && (
|
||||
<>
|
||||
{titleComponent}
|
||||
<IconButton
|
||||
className="item-card-edit-icon"
|
||||
data-testid={`${namePrefix}-edit-button`}
|
||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
||||
iconAs={EditIcon}
|
||||
onClick={onClickEdit}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="ml-auto d-flex">
|
||||
{(isVertical || isSequential) && (
|
||||
<CardStatus status={status} showDiscussionsEnabledBadge={showDiscussionsEnabledBadge} />
|
||||
)}
|
||||
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
|
||||
<Dropdown.Toggle
|
||||
@@ -113,6 +138,17 @@ const CardHeader = ({
|
||||
iconAs={Icon}
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
{isSequential && proctoringExamConfigurationLink && (
|
||||
<Dropdown.Item
|
||||
as={Hyperlink}
|
||||
target="_blank"
|
||||
destination={proctoringExamConfigurationLink}
|
||||
href={proctoringExamConfigurationLink}
|
||||
externalLinkTitle={intl.formatMessage(messages.proctoringLinkTooltip)}
|
||||
>
|
||||
{intl.formatMessage(messages.menuProctoringLinkText)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-publish-button`}
|
||||
disabled={isDisabledPublish}
|
||||
@@ -126,6 +162,11 @@ const CardHeader = ({
|
||||
>
|
||||
{intl.formatMessage(messages.menuConfigure)}
|
||||
</Dropdown.Item>
|
||||
{isVertical && enableCopyPasteUnits && (
|
||||
<Dropdown.Item onClick={onClickCopy}>
|
||||
{intl.formatMessage(messages.menuCopy)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{actions.duplicable && (
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-duplicate-button`}
|
||||
@@ -168,6 +209,17 @@ const CardHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
CardHeader.defaultProps = {
|
||||
enableCopyPasteUnits: false,
|
||||
isVertical: false,
|
||||
isSequential: false,
|
||||
onClickCopy: null,
|
||||
proctoringExamConfigurationLink: null,
|
||||
discussionEnabled: false,
|
||||
discussionsSettings: {},
|
||||
parentInfo: {},
|
||||
};
|
||||
|
||||
CardHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
@@ -185,8 +237,10 @@ CardHeader.propTypes = {
|
||||
onClickDuplicate: PropTypes.func.isRequired,
|
||||
onClickMoveUp: PropTypes.func.isRequired,
|
||||
onClickMoveDown: PropTypes.func.isRequired,
|
||||
onClickCopy: PropTypes.func,
|
||||
titleComponent: PropTypes.node.isRequired,
|
||||
namePrefix: PropTypes.string.isRequired,
|
||||
proctoringExamConfigurationLink: PropTypes.string,
|
||||
actions: PropTypes.shape({
|
||||
deletable: PropTypes.bool.isRequired,
|
||||
draggable: PropTypes.bool.isRequired,
|
||||
@@ -195,6 +249,18 @@ CardHeader.propTypes = {
|
||||
allowMoveUp: PropTypes.bool,
|
||||
allowMoveDown: PropTypes.bool,
|
||||
}).isRequired,
|
||||
enableCopyPasteUnits: PropTypes.bool,
|
||||
isVertical: PropTypes.bool,
|
||||
isSequential: PropTypes.bool,
|
||||
discussionEnabled: PropTypes.bool,
|
||||
discussionsSettings: PropTypes.shape({
|
||||
providerType: PropTypes.string,
|
||||
enableGradedUnits: PropTypes.bool,
|
||||
}),
|
||||
parentInfo: PropTypes.shape({
|
||||
isTimeLimited: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CardHeader;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.item-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: -.5rem;
|
||||
|
||||
.item-card-header__title-btn {
|
||||
justify-content: flex-start;
|
||||
@@ -13,32 +12,18 @@
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.item-card-header__badge-status {
|
||||
display: flex;
|
||||
padding: 1px .5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
border-radius: .375rem;
|
||||
border: 1px solid $light-300;
|
||||
margin: 0 .75rem;
|
||||
.item-card-edit-icon {
|
||||
opacity: 0;
|
||||
transition: opacity .3s linear;
|
||||
|
||||
& span:last-child {
|
||||
color: $primary-700;
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.pgn__form-group {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.item-card-header-tooltip {
|
||||
.tooltip-inner {
|
||||
max-width: 18.75rem;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transform: translate(5.75rem, 0) !important;
|
||||
&:hover {
|
||||
.item-card-edit-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act, render, fireEvent, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ITEM_BADGE_STATUS } from '../constants';
|
||||
import CardHeader from './CardHeader';
|
||||
import BaseTitleWithStatusBadge from './BaseTitleWithStatusBadge';
|
||||
import TitleButton from './TitleButton';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -36,7 +37,8 @@ const cardHeaderProps = {
|
||||
onClickConfigure: onClickConfigureMock,
|
||||
onClickMoveUp: onClickMoveUpMock,
|
||||
onClickMoveDown: onClickMoveDownMock,
|
||||
namePrefix: 'section',
|
||||
isSequential: true,
|
||||
namePrefix: 'subsection',
|
||||
actions: {
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
@@ -49,17 +51,11 @@ const renderComponent = (props, entry = '/') => {
|
||||
const titleComponent = (
|
||||
<TitleButton
|
||||
isExpanded
|
||||
title={cardHeaderProps.title}
|
||||
onTitleClick={onExpandMock}
|
||||
namePrefix={cardHeaderProps.namePrefix}
|
||||
{...props}
|
||||
>
|
||||
<BaseTitleWithStatusBadge
|
||||
title={cardHeaderProps.title}
|
||||
status={cardHeaderProps.status}
|
||||
namePrefix={cardHeaderProps.namePrefix}
|
||||
{...props}
|
||||
/>
|
||||
</TitleButton>
|
||||
/>
|
||||
);
|
||||
|
||||
return render(
|
||||
@@ -80,9 +76,8 @@ describe('<CardHeader />', () => {
|
||||
const { findByText, findByTestId, queryByTestId } = renderComponent();
|
||||
|
||||
expect(await findByText(cardHeaderProps.title)).toBeInTheDocument();
|
||||
expect(await findByTestId('section-card-header__expanded-btn')).toBeInTheDocument();
|
||||
expect(await findByTestId('section-card-header__badge-status')).toBeInTheDocument();
|
||||
expect(await findByTestId('section-card-header__menu')).toBeInTheDocument();
|
||||
expect(await findByTestId('subsection-card-header__expanded-btn')).toBeInTheDocument();
|
||||
expect(await findByTestId('subsection-card-header__menu')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId('edit field')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -120,25 +115,25 @@ describe('<CardHeader />', () => {
|
||||
expect(await findByText(messages.statusBadgeDraft.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('check publish menu item is disabled when section status is live or published not live and it has no changes', async () => {
|
||||
it('check publish menu item is disabled when subsection status is live or published not live and it has no changes', async () => {
|
||||
const { findByText, findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
status: ITEM_BADGE_STATUS.publishedNotLive,
|
||||
});
|
||||
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
expect(await findByText(messages.menuPublish.defaultMessage)).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('check publish menu item is enabled when section status is live or published not live and it has changes', async () => {
|
||||
it('check publish menu item is enabled when subsection status is live or published not live and it has changes', async () => {
|
||||
const { findByText, findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
status: ITEM_BADGE_STATUS.publishedNotLive,
|
||||
hasChanges: true,
|
||||
});
|
||||
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
expect(await findByText(messages.menuPublish.defaultMessage)).not.toHaveAttribute('aria-disabled');
|
||||
});
|
||||
@@ -146,7 +141,7 @@ describe('<CardHeader />', () => {
|
||||
it('calls handleExpanded when button is clicked', async () => {
|
||||
const { findByTestId } = renderComponent();
|
||||
|
||||
const expandButton = await findByTestId('section-card-header__expanded-btn');
|
||||
const expandButton = await findByTestId('subsection-card-header__expanded-btn');
|
||||
fireEvent.click(expandButton);
|
||||
expect(onExpandMock).toHaveBeenCalled();
|
||||
});
|
||||
@@ -154,11 +149,9 @@ describe('<CardHeader />', () => {
|
||||
it('calls onClickMenuButton when menu is clicked', async () => {
|
||||
const { findByTestId } = renderComponent();
|
||||
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
waitFor(() => {
|
||||
expect(onClickMenuButtonMock).toHaveBeenCalled();
|
||||
});
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
await act(async () => fireEvent.click(menuButton));
|
||||
expect(onClickMenuButtonMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClickPublish when item is clicked', async () => {
|
||||
@@ -167,24 +160,20 @@ describe('<CardHeader />', () => {
|
||||
status: ITEM_BADGE_STATUS.draft,
|
||||
});
|
||||
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const publishMenuItem = await findByText(messages.menuPublish.defaultMessage);
|
||||
fireEvent.click(publishMenuItem);
|
||||
waitFor(() => {
|
||||
expect(onClickPublishMock).toHaveBeenCalled();
|
||||
});
|
||||
await act(async () => fireEvent.click(publishMenuItem));
|
||||
expect(onClickPublishMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClickEdit when the button is clicked', async () => {
|
||||
const { findByTestId } = renderComponent();
|
||||
|
||||
const editButton = await findByTestId('section-edit-button');
|
||||
fireEvent.click(editButton);
|
||||
waitFor(() => {
|
||||
expect(onClickEditMock).toHaveBeenCalled();
|
||||
});
|
||||
const editButton = await findByTestId('subsection-edit-button');
|
||||
await act(async () => fireEvent.click(editButton));
|
||||
expect(onClickEditMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('check is field visible when isFormOpen is true', async () => {
|
||||
@@ -193,9 +182,9 @@ describe('<CardHeader />', () => {
|
||||
isFormOpen: true,
|
||||
});
|
||||
|
||||
expect(await findByTestId('section-edit-field')).toBeInTheDocument();
|
||||
expect(await findByTestId('subsection-edit-field')).toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
expect(queryByTestId('section-card-header__expanded-btn')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('subsection-card-header__expanded-btn')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('edit-button')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -207,32 +196,59 @@ describe('<CardHeader />', () => {
|
||||
isDisabledEditField: true,
|
||||
});
|
||||
|
||||
expect(await findByTestId('section-edit-field')).toBeDisabled();
|
||||
expect(await findByTestId('subsection-edit-field')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('calls onClickDelete when item is clicked', async () => {
|
||||
const { findByText, findByTestId } = renderComponent();
|
||||
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
await act(async () => fireEvent.click(menuButton));
|
||||
const deleteMenuItem = await findByText(messages.menuDelete.defaultMessage);
|
||||
fireEvent.click(deleteMenuItem);
|
||||
waitFor(() => {
|
||||
expect(onClickDeleteMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
await act(async () => fireEvent.click(deleteMenuItem));
|
||||
expect(onClickDeleteMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onClickDuplicate when item is clicked', async () => {
|
||||
const { findByText, findByTestId } = renderComponent();
|
||||
|
||||
const menuButton = await findByTestId('section-card-header__menu-button');
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const duplicateMenuItem = await findByText(messages.menuDuplicate.defaultMessage);
|
||||
fireEvent.click(duplicateMenuItem);
|
||||
waitFor(() => {
|
||||
expect(onClickDuplicateMock).toHaveBeenCalled();
|
||||
await act(async () => fireEvent.click(duplicateMenuItem));
|
||||
expect(onClickDuplicateMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('check if proctoringExamConfigurationLink is visible', async () => {
|
||||
const { findByText, findByTestId } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
proctoringExamConfigurationLink: 'https://localhost:8000/',
|
||||
isSequential: true,
|
||||
});
|
||||
|
||||
const menuButton = await findByTestId('subsection-card-header__menu-button');
|
||||
await act(async () => fireEvent.click(menuButton));
|
||||
|
||||
expect(await findByText(messages.menuProctoringLinkText.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('check if discussion enabled badge is visible', async () => {
|
||||
const { queryByText } = renderComponent({
|
||||
...cardHeaderProps,
|
||||
isVertical: true,
|
||||
discussionEnabled: true,
|
||||
discussionsSettings: {
|
||||
providerType: 'openedx',
|
||||
enableGradedUnits: true,
|
||||
},
|
||||
parentInfo: {
|
||||
isTimeLimited: false,
|
||||
graded: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByText(messages.discussionEnabledBadgeText.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
40
src/course-outline/card-header/CardStatus.jsx
Normal file
40
src/course-outline/card-header/CardStatus.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { ITEM_BADGE_STATUS } from '../constants';
|
||||
import { getItemStatusBadgeContent } from '../utils';
|
||||
import messages from './messages';
|
||||
import StatusBadge from './StatusBadge';
|
||||
|
||||
const CardStatus = ({
|
||||
status,
|
||||
showDiscussionsEnabledBadge,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { badgeTitle, badgeIcon } = getItemStatusBadgeContent(status, messages, intl);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDiscussionsEnabledBadge && (
|
||||
<StatusBadge
|
||||
text={intl.formatMessage(messages.discussionEnabledBadgeText)}
|
||||
/>
|
||||
)}
|
||||
{badgeTitle && (
|
||||
<StatusBadge
|
||||
text={badgeTitle}
|
||||
icon={badgeIcon}
|
||||
iconClassName={classNames({ 'text-success-500': status === ITEM_BADGE_STATUS.live })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CardStatus.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
showDiscussionsEnabledBadge: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CardStatus;
|
||||
42
src/course-outline/card-header/StatusBadge.jsx
Normal file
42
src/course-outline/card-header/StatusBadge.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
|
||||
const StatusBadge = ({
|
||||
text,
|
||||
icon,
|
||||
iconClassName,
|
||||
}) => {
|
||||
if (text) {
|
||||
return (
|
||||
<div
|
||||
className="px-2 py-1 mr-2 rounded bg-white align-self-center align-items-center d-flex border border-light-300"
|
||||
role="status"
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
src={icon}
|
||||
size="sm"
|
||||
className={iconClassName}
|
||||
/>
|
||||
)}
|
||||
<span className="small ml-1">{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
StatusBadge.defaultProps = {
|
||||
text: '',
|
||||
icon: '',
|
||||
iconClassName: '',
|
||||
};
|
||||
|
||||
StatusBadge.propTypes = {
|
||||
text: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
iconClassName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StatusBadge;
|
||||
@@ -4,29 +4,29 @@ import {
|
||||
Button,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from '@edx/paragon';
|
||||
Truncate,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
ArrowDropDown as ArrowDownIcon,
|
||||
ArrowDropUp as ArrowUpIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
|
||||
const TitleButton = ({
|
||||
title,
|
||||
isExpanded,
|
||||
onTitleClick,
|
||||
namePrefix,
|
||||
children,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const titleTooltipMessage = intl.formatMessage(messages.expandTooltip);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="bottom-start"
|
||||
placement="bottom"
|
||||
overlay={(
|
||||
<Tooltip
|
||||
id={titleTooltipMessage}
|
||||
className="item-card-header-tooltip"
|
||||
id={`${title}-${titleTooltipMessage}`}
|
||||
>
|
||||
{titleTooltipMessage}
|
||||
</Tooltip>
|
||||
@@ -39,21 +39,17 @@ const TitleButton = ({
|
||||
className="item-card-header__title-btn"
|
||||
onClick={onTitleClick}
|
||||
>
|
||||
{children}
|
||||
<Truncate lines={1} className={`${namePrefix}-card-title mb-0`}>{title}</Truncate>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
TitleButton.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
TitleButton.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
onTitleClick: PropTypes.func.isRequired,
|
||||
namePrefix: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default TitleButton;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Button, Truncate } from '@openedx/paragon';
|
||||
|
||||
const TitleLink = ({
|
||||
title,
|
||||
titleLink,
|
||||
namePrefix,
|
||||
children,
|
||||
}) => (
|
||||
<Button
|
||||
as={Link}
|
||||
@@ -14,18 +14,14 @@ const TitleLink = ({
|
||||
className="item-card-header__title-btn"
|
||||
to={titleLink}
|
||||
>
|
||||
{children}
|
||||
<Truncate lines={1} className={`${namePrefix}-card-title mb-0`}>{title}</Truncate>
|
||||
</Button>
|
||||
);
|
||||
|
||||
TitleLink.defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
TitleLink.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
titleLink: PropTypes.string.isRequired,
|
||||
namePrefix: PropTypes.string.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default TitleLink;
|
||||
|
||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.card.status-badge.live',
|
||||
defaultMessage: 'Live',
|
||||
},
|
||||
statusBadgeGated: {
|
||||
id: 'course-authoring.course-outline.card.status-badge.gated',
|
||||
defaultMessage: 'Gated',
|
||||
},
|
||||
statusBadgePublishedNotLive: {
|
||||
id: 'course-authoring.course-outline.card.status-badge.published-not-live',
|
||||
defaultMessage: 'Published not live',
|
||||
@@ -53,6 +57,22 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.card.menu.delete',
|
||||
defaultMessage: 'Delete',
|
||||
},
|
||||
menuCopy: {
|
||||
id: 'course-authoring.course-outline.card.menu.delete',
|
||||
defaultMessage: 'Copy to clipboard',
|
||||
},
|
||||
menuProctoringLinkText: {
|
||||
id: 'course-authoring.course-outline.card.menu.proctoring-settings',
|
||||
defaultMessage: 'Proctoring settings',
|
||||
},
|
||||
proctoringLinkTooltip: {
|
||||
id: 'course-authoring.course-outline.card.menu.proctoring-settings-tooltip',
|
||||
defaultMessage: 'Proctoring settings',
|
||||
},
|
||||
discussionEnabledBadgeText: {
|
||||
id: 'course-authoring.course-outline.card.badge.discussionEnabled',
|
||||
defaultMessage: 'Discussions enabled',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { Alert, Form, Hyperlink } from '@openedx/paragon';
|
||||
import {
|
||||
Warning as WarningIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
import PrereqSettings from './PrereqSettings';
|
||||
|
||||
const AdvancedTab = ({
|
||||
isTimeLimited,
|
||||
setIsTimeLimited,
|
||||
defaultTimeLimit,
|
||||
setDefaultTimeLimit,
|
||||
values,
|
||||
setFieldValue,
|
||||
prereqs,
|
||||
releasedToStudents,
|
||||
wasExamEverLinkedWithExternal,
|
||||
enableProctoredExams,
|
||||
supportsOnboarding,
|
||||
wasProctoredExam,
|
||||
showReviewRules,
|
||||
onlineProctoringRules,
|
||||
}) => {
|
||||
const {
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
defaultTimeLimitMinutes,
|
||||
examReviewRules,
|
||||
} = values;
|
||||
let examTypeValue = 'none';
|
||||
|
||||
if (isTimeLimited && isProctoredExam) {
|
||||
if (isOnboardingExam) {
|
||||
examTypeValue = 'onboardingExam';
|
||||
} else if (isPracticeExam) {
|
||||
examTypeValue = 'practiceExam';
|
||||
} else {
|
||||
examTypeValue = 'proctoredExam';
|
||||
}
|
||||
} else if (isTimeLimited) {
|
||||
examTypeValue = 'timed';
|
||||
}
|
||||
|
||||
const formatHour = (hour) => {
|
||||
const hh = Math.floor(hour / 60);
|
||||
const mm = hour % 60;
|
||||
@@ -31,14 +64,35 @@ const AdvancedTab = ({
|
||||
return `${hhs}:${mms}`;
|
||||
};
|
||||
|
||||
const [timeLimit, setTimeLimit] = useState(formatHour(defaultTimeLimit));
|
||||
const [timeLimit, setTimeLimit] = useState(formatHour(defaultTimeLimitMinutes));
|
||||
const showReviewRulesDiv = showReviewRules && isProctoredExam && !isPracticeExam && !isOnboardingExam;
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (e.target.value === 'timed') {
|
||||
setIsTimeLimited(true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
setFieldValue('isProctoredExam', false);
|
||||
} else if (e.target.value === 'onboardingExam') {
|
||||
setFieldValue('isOnboardingExam', true);
|
||||
setFieldValue('isProctoredExam', true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
} else if (e.target.value === 'practiceExam') {
|
||||
setFieldValue('isPracticeExam', true);
|
||||
setFieldValue('isProctoredExam', true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
} else if (e.target.value === 'proctoredExam') {
|
||||
setFieldValue('isProctoredExam', true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
} else {
|
||||
setDefaultTimeLimit(null);
|
||||
setIsTimeLimited(false);
|
||||
setFieldValue('isTimeLimited', false);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
setFieldValue('isProctoredExam', false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,11 +102,29 @@ const AdvancedTab = ({
|
||||
value = value.trim();
|
||||
if (value && valid) {
|
||||
const minutes = moment.duration(value).asMinutes();
|
||||
setDefaultTimeLimit(minutes);
|
||||
setFieldValue('defaultTimeLimitMinutes', minutes);
|
||||
}
|
||||
setTimeLimit(value);
|
||||
};
|
||||
|
||||
const renderAlerts = () => {
|
||||
const proctoredExamLockedIn = releasedToStudents && wasExamEverLinkedWithExternal;
|
||||
return (
|
||||
<>
|
||||
{proctoredExamLockedIn && !wasProctoredExam && (
|
||||
<Alert variant="warning" icon={WarningIcon}>
|
||||
<FormattedMessage {...messages.proctoredExamLockedAndisNotProctoredExamAlert} />
|
||||
</Alert>
|
||||
)}
|
||||
{proctoredExamLockedIn && wasProctoredExam && (
|
||||
<Alert variant="warning" icon={WarningIcon}>
|
||||
<FormattedMessage {...messages.proctoredExamLockedAndisProctoredExamAlert} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.setSpecialExam} /></h5>
|
||||
@@ -60,15 +132,47 @@ const AdvancedTab = ({
|
||||
<Form.RadioSet
|
||||
name="specialExam"
|
||||
onChange={handleChange}
|
||||
value={isTimeLimited ? 'timed' : 'none'}
|
||||
value={examTypeValue}
|
||||
>
|
||||
{renderAlerts()}
|
||||
<Form.Radio value="none">
|
||||
<FormattedMessage {...messages.none} />
|
||||
</Form.Radio>
|
||||
<Form.Radio value="timed">
|
||||
<Form.Radio
|
||||
value="timed"
|
||||
description={<FormattedMessage {...messages.timedDescription} />}
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.timed} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.timedDescription} /></Form.Text>
|
||||
{enableProctoredExams && (
|
||||
<>
|
||||
<Form.Radio
|
||||
value="proctoredExam"
|
||||
description={<FormattedMessage {...messages.proctoredExamDescription} />}
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.proctoredExam} />
|
||||
</Form.Radio>
|
||||
{supportsOnboarding ? (
|
||||
<Form.Radio
|
||||
description={<FormattedMessage {...messages.onboardingExamDescription} />}
|
||||
value="onboardingExam"
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.onboardingExam} />
|
||||
</Form.Radio>
|
||||
) : (
|
||||
<Form.Radio
|
||||
value="practiceExam"
|
||||
controlClassName="mw-1-25rem"
|
||||
description={<FormattedMessage {...messages.practiceExamDescription} />}
|
||||
>
|
||||
<FormattedMessage {...messages.practiceExam} />
|
||||
</Form.Radio>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
{ isTimeLimited && (
|
||||
<div className="mt-3" data-testid="advanced-tab-hours-picker-wrapper">
|
||||
@@ -86,15 +190,87 @@ const AdvancedTab = ({
|
||||
<Form.Text><FormattedMessage {...messages.timeLimitDescription} /></Form.Text>
|
||||
</div>
|
||||
)}
|
||||
{ showReviewRulesDiv && (
|
||||
<div className="mt-3">
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage {...messages.reviewRulesLabel} />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(e) => setFieldValue('examReviewRules', e.target.value)}
|
||||
value={examReviewRules}
|
||||
as="textarea"
|
||||
rows="3"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Text>
|
||||
{ onlineProctoringRules ? (
|
||||
<FormattedMessage
|
||||
{...messages.reviewRulesDescriptionWithLink}
|
||||
values={{
|
||||
hyperlink: (
|
||||
<Hyperlink
|
||||
destination={onlineProctoringRules}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.reviewRulesDescriptionLinkText}
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage {...messages.reviewRulesDescription} />
|
||||
)}
|
||||
</Form.Text>
|
||||
</div>
|
||||
)}
|
||||
<PrereqSettings
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
prereqs={prereqs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedTab.defaultProps = {
|
||||
prereqs: [],
|
||||
wasExamEverLinkedWithExternal: false,
|
||||
enableProctoredExams: false,
|
||||
supportsOnboarding: false,
|
||||
wasProctoredExam: false,
|
||||
showReviewRules: false,
|
||||
onlineProctoringRules: '',
|
||||
};
|
||||
|
||||
AdvancedTab.propTypes = {
|
||||
isTimeLimited: PropTypes.bool.isRequired,
|
||||
setIsTimeLimited: PropTypes.func.isRequired,
|
||||
defaultTimeLimit: PropTypes.number.isRequired,
|
||||
setDefaultTimeLimit: PropTypes.func.isRequired,
|
||||
values: PropTypes.shape({
|
||||
isTimeLimited: PropTypes.bool.isRequired,
|
||||
defaultTimeLimitMinutes: PropTypes.number,
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqUsageKey: PropTypes.string,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
examReviewRules: PropTypes.string,
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
wasExamEverLinkedWithExternal: PropTypes.bool,
|
||||
enableProctoredExams: PropTypes.bool,
|
||||
supportsOnboarding: PropTypes.bool,
|
||||
wasProctoredExam: PropTypes.bool,
|
||||
showReviewRules: PropTypes.bool,
|
||||
onlineProctoringRules: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(AdvancedTab);
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Stack, Form } from '@edx/paragon';
|
||||
import { Stack, Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { DatepickerControl, DATEPICKER_TYPES } from '../../generic/datepicker-control';
|
||||
|
||||
const BasicTab = ({
|
||||
releaseDate,
|
||||
setReleaseDate,
|
||||
isSubsection,
|
||||
graderType,
|
||||
values,
|
||||
setFieldValue,
|
||||
courseGraders,
|
||||
setGraderType,
|
||||
dueDate,
|
||||
setDueDate,
|
||||
isSubsection,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onChangeGraderType = (e) => setGraderType(e.target.value);
|
||||
const {
|
||||
releaseDate,
|
||||
graderType,
|
||||
dueDate,
|
||||
} = values;
|
||||
|
||||
const onChangeGraderType = (e) => setFieldValue('graderType', e.target.value);
|
||||
|
||||
const createOptions = () => courseGraders.map((option) => (
|
||||
<option key={option} value={option}> {option} </option>
|
||||
@@ -34,14 +36,14 @@ const BasicTab = ({
|
||||
value={releaseDate}
|
||||
label={intl.formatMessage(messages.releaseDate)}
|
||||
controlName="state-date"
|
||||
onChange={setReleaseDate}
|
||||
onChange={(val) => setFieldValue('releaseDate', val)}
|
||||
/>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.time}
|
||||
value={releaseDate}
|
||||
label={intl.formatMessage(messages.releaseTimeUTC)}
|
||||
controlName="start-time"
|
||||
onChange={setReleaseDate}
|
||||
onChange={(val) => setFieldValue('releaseDate', val)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -50,16 +52,20 @@ const BasicTab = ({
|
||||
<div>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.grading} /></h5>
|
||||
<hr />
|
||||
<Form.Label><FormattedMessage {...messages.gradeAs} /></Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={graderType}
|
||||
onChange={(value) => onChangeGraderType(value)}
|
||||
data-testid="grader-type-select"
|
||||
>
|
||||
<option key="notGraded" value="Not Graded"> Not Graded </option>
|
||||
{createOptions()}
|
||||
</Form.Control>
|
||||
<Form.Group>
|
||||
<Form.Label><FormattedMessage {...messages.gradeAs} /></Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={graderType}
|
||||
onChange={onChangeGraderType}
|
||||
data-testid="grader-type-select"
|
||||
>
|
||||
<option key="notgraded" value="notgraded">
|
||||
{intl.formatMessage(messages.notGradedTypeOption)}
|
||||
</option>
|
||||
{createOptions()}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<div data-testid="due-date-stack">
|
||||
<Stack className="mt-3" direction="horizontal" gap={5}>
|
||||
<DatepickerControl
|
||||
@@ -67,7 +73,7 @@ const BasicTab = ({
|
||||
value={dueDate}
|
||||
label={intl.formatMessage(messages.dueDate)}
|
||||
controlName="state-date"
|
||||
onChange={setDueDate}
|
||||
onChange={(val) => setFieldValue('dueDate', val)}
|
||||
data-testid="due-date-picker"
|
||||
/>
|
||||
<DatepickerControl
|
||||
@@ -75,7 +81,7 @@ const BasicTab = ({
|
||||
value={dueDate}
|
||||
label={intl.formatMessage(messages.dueTimeUTC)}
|
||||
controlName="start-time"
|
||||
onChange={setDueDate}
|
||||
onChange={(val) => setFieldValue('dueDate', val)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -87,18 +93,14 @@ const BasicTab = ({
|
||||
};
|
||||
|
||||
BasicTab.propTypes = {
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
setReleaseDate: PropTypes.func.isRequired,
|
||||
isSubsection: PropTypes.bool.isRequired,
|
||||
graderType: PropTypes.string.isRequired,
|
||||
setGraderType: PropTypes.func.isRequired,
|
||||
dueDate: PropTypes.string,
|
||||
setDueDate: PropTypes.func.isRequired,
|
||||
values: PropTypes.shape({
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
graderType: PropTypes.string.isRequired,
|
||||
dueDate: PropTypes.string,
|
||||
}).isRequired,
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
BasicTab.defaultProps = {
|
||||
dueDate: null,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BasicTab);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/* eslint-disable import/named */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ModalDialog,
|
||||
Button,
|
||||
ActionRow,
|
||||
Form,
|
||||
Tab,
|
||||
Tabs,
|
||||
useCheckboxSetValues,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Formik } from 'formik';
|
||||
|
||||
import { VisibilityTypes } from '../../data/constants';
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
import { getCurrentItem } from '../data/selectors';
|
||||
import { getCurrentItem, getProctoredExamsFlag } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
import BasicTab from './BasicTab';
|
||||
import VisibilityTab from './VisibilityTab';
|
||||
@@ -41,27 +43,26 @@ const ConfigureModal = ({
|
||||
format,
|
||||
userPartitionInfo,
|
||||
ancestorHasStaffLock,
|
||||
isPrereq,
|
||||
prereqs,
|
||||
prereq,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
releasedToStudents,
|
||||
wasExamEverLinkedWithExternal,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
supportsOnboarding,
|
||||
showReviewRules,
|
||||
onlineProctoringRules,
|
||||
} = useSelector(getCurrentItem);
|
||||
const enableProctoredExams = useSelector(getProctoredExamsFlag);
|
||||
|
||||
const [releaseDate, setReleaseDate] = useState(sectionStartDate);
|
||||
const [isVisibleToStaffOnly, setIsVisibleToStaffOnly] = useState(visibilityState === VisibilityTypes.STAFF_ONLY);
|
||||
|
||||
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
|
||||
const [graderType, setGraderType] = useState(format == null ? 'Not Graded' : format);
|
||||
const [dueDateState, setDueDateState] = useState(due == null ? '' : due);
|
||||
const [isTimeLimitedState, setIsTimeLimitedState] = useState(false);
|
||||
const [defaultTimeLimitMin, setDefaultTimeLimitMin] = useState(defaultTimeLimitMinutes);
|
||||
const [hideAfterDueState, setHideAfterDueState] = useState(hideAfterDue === undefined ? false : hideAfterDue);
|
||||
const [showCorrectnessState, setShowCorrectnessState] = useState(false);
|
||||
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
|
||||
|
||||
/* TODO: The use of these useEffects needs to be updated to use Formik, please see,
|
||||
* https://github.com/open-craft/frontend-app-course-authoring/pull/22#discussion_r1435957797 as reference. */
|
||||
// by default it is -1 i.e. accessible to all learners & staff
|
||||
const [selectedPartitionIndex, setSelectedPartitionIndex] = useState(userPartitionInfo?.selectedPartitionIndex);
|
||||
const getSelectedGroups = () => {
|
||||
if (selectedPartitionIndex >= 0) {
|
||||
return userPartitionInfo?.selectablePartitions[selectedPartitionIndex]
|
||||
if (userPartitionInfo?.selectedPartitionIndex >= 0) {
|
||||
return userPartitionInfo?.selectablePartitions[userPartitionInfo?.selectedPartitionIndex]
|
||||
?.groups
|
||||
.filter(({ selected }) => selected)
|
||||
.map(({ id }) => `${id}`)
|
||||
@@ -70,140 +71,126 @@ const ConfigureModal = ({
|
||||
return [];
|
||||
};
|
||||
|
||||
const [selectedGroups, { add, remove, set }] = useCheckboxSetValues([]);
|
||||
const defaultPrereqScore = (val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return 100;
|
||||
}
|
||||
return parseFloat(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedPartitionIndex(userPartitionInfo?.selectedPartitionIndex);
|
||||
}, [userPartitionInfo]);
|
||||
const initialValues = {
|
||||
releaseDate: sectionStartDate,
|
||||
isVisibleToStaffOnly: visibilityState === VisibilityTypes.STAFF_ONLY,
|
||||
saveButtonDisabled: true,
|
||||
graderType: format == null ? 'notgraded' : format,
|
||||
dueDate: due == null ? '' : due,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
defaultTimeLimitMinutes,
|
||||
hideAfterDue: hideAfterDue === undefined ? false : hideAfterDue,
|
||||
showCorrectness,
|
||||
isPrereq,
|
||||
prereqUsageKey: prereq,
|
||||
prereqMinScore: defaultPrereqScore(prereqMinScore),
|
||||
prereqMinCompletion: defaultPrereqScore(prereqMinCompletion),
|
||||
// by default it is -1 i.e. accessible to all learners & staff
|
||||
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
|
||||
selectedGroups: getSelectedGroups(),
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
set(getSelectedGroups());
|
||||
}, [selectedPartitionIndex, userPartitionInfo]);
|
||||
const validationSchema = Yup.object().shape({
|
||||
isTimeLimited: Yup.boolean(),
|
||||
isProctoredExam: Yup.boolean(),
|
||||
isPracticeExam: Yup.boolean(),
|
||||
isOnboardingExam: Yup.boolean(),
|
||||
examReviewRules: Yup.string(),
|
||||
defaultTimeLimitMinutes: Yup.number().nullable(true),
|
||||
hideAfterDueState: Yup.boolean(),
|
||||
showCorrectness: Yup.string().required(),
|
||||
isPrereq: Yup.boolean(),
|
||||
prereqUsageKey: Yup.string().nullable(true),
|
||||
prereqMinScore: Yup.number().min(
|
||||
0,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).max(
|
||||
100,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).nullable(true),
|
||||
prereqMinCompletion: Yup.number().min(
|
||||
0,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).max(
|
||||
100,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).nullable(true),
|
||||
selectedPartitionIndex: Yup.number().integer(),
|
||||
selectedGroups: Yup.array().of(Yup.string()),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setReleaseDate(sectionStartDate);
|
||||
}, [sectionStartDate]);
|
||||
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
|
||||
|
||||
useEffect(() => {
|
||||
setGraderType(format == null ? 'Not Graded' : format);
|
||||
}, [format]);
|
||||
|
||||
useEffect(() => {
|
||||
setDueDateState(due == null ? '' : due);
|
||||
}, [due]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTimeLimitedState(isTimeLimited);
|
||||
}, [isTimeLimited]);
|
||||
|
||||
useEffect(() => {
|
||||
setDefaultTimeLimitMin(defaultTimeLimitMinutes);
|
||||
}, [defaultTimeLimitMinutes]);
|
||||
|
||||
useEffect(() => {
|
||||
setHideAfterDueState(hideAfterDue === undefined ? false : hideAfterDue);
|
||||
}, [hideAfterDue]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCorrectnessState(showCorrectness);
|
||||
}, [showCorrectness]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisibleToStaffOnly(visibilityState === VisibilityTypes.STAFF_ONLY);
|
||||
}, [visibilityState]);
|
||||
|
||||
useEffect(() => {
|
||||
const visibilityUnchanged = isVisibleToStaffOnly === (visibilityState === VisibilityTypes.STAFF_ONLY);
|
||||
const graderTypeUnchanged = graderType === (format == null ? 'Not Graded' : format);
|
||||
const dueDateUnchanged = dueDateState === (due == null ? '' : due);
|
||||
const hideAfterDueUnchanged = hideAfterDueState === (hideAfterDue === undefined ? false : hideAfterDue);
|
||||
const selectedGroupsUnchanged = selectedGroups.sort().join(',') === getSelectedGroups().sort().join(',');
|
||||
// handle the case of unrestricting access
|
||||
const accessRestrictionUnchanged = selectedPartitionIndex !== -1
|
||||
|| userPartitionInfo?.selectedPartitionIndex === -1;
|
||||
setSaveButtonDisabled(
|
||||
visibilityUnchanged
|
||||
&& releaseDate === sectionStartDate
|
||||
&& dueDateUnchanged
|
||||
&& isTimeLimitedState === isTimeLimited
|
||||
&& defaultTimeLimitMin === defaultTimeLimitMinutes
|
||||
&& hideAfterDueUnchanged
|
||||
&& showCorrectnessState === showCorrectness
|
||||
&& graderTypeUnchanged
|
||||
&& selectedGroupsUnchanged
|
||||
&& accessRestrictionUnchanged,
|
||||
);
|
||||
}, [
|
||||
releaseDate,
|
||||
isVisibleToStaffOnly,
|
||||
dueDateState,
|
||||
isTimeLimitedState,
|
||||
defaultTimeLimitMin,
|
||||
hideAfterDueState,
|
||||
showCorrectnessState,
|
||||
graderType,
|
||||
selectedGroups,
|
||||
]);
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = (data) => {
|
||||
const groupAccess = {};
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.chapter.id:
|
||||
onConfigureSubmit(isVisibleToStaffOnly, releaseDate);
|
||||
onConfigureSubmit(data.isVisibleToStaffOnly, data.releaseDate);
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.sequential.id:
|
||||
onConfigureSubmit(
|
||||
isVisibleToStaffOnly,
|
||||
releaseDate,
|
||||
graderType === 'Not Graded' ? 'notgraded' : graderType,
|
||||
dueDateState,
|
||||
isTimeLimitedState,
|
||||
defaultTimeLimitMin,
|
||||
hideAfterDueState,
|
||||
showCorrectnessState,
|
||||
data.isVisibleToStaffOnly,
|
||||
data.releaseDate,
|
||||
data.graderType,
|
||||
data.dueDate,
|
||||
data.isTimeLimited,
|
||||
data.isProctoredExam,
|
||||
data.isOnboardingExam,
|
||||
data.isPracticeExam,
|
||||
data.examReviewRules,
|
||||
data.isTimeLimited ? data.defaultTimeLimitMinutes : 0,
|
||||
data.hideAfterDue,
|
||||
data.showCorrectness,
|
||||
data.isPrereq,
|
||||
data.prereqUsageKey,
|
||||
data.prereqMinScore,
|
||||
data.prereqMinCompletion,
|
||||
);
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
|
||||
if (selectedPartitionIndex >= 0) {
|
||||
const partitionId = userPartitionInfo.selectablePartitions[selectedPartitionIndex].id;
|
||||
groupAccess[partitionId] = selectedGroups.map(g => parseInt(g, 10));
|
||||
if (data.selectedPartitionIndex >= 0) {
|
||||
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
||||
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
|
||||
}
|
||||
onConfigureSubmit(isVisibleToStaffOnly, groupAccess);
|
||||
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const renderModalBody = () => {
|
||||
const renderModalBody = (values, setFieldValue) => {
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.chapter.id:
|
||||
return (
|
||||
<Tabs>
|
||||
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
|
||||
<BasicTab
|
||||
releaseDate={releaseDate}
|
||||
setReleaseDate={setReleaseDate}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
isSubsection={isSubsection}
|
||||
graderType={graderType}
|
||||
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
||||
setGraderType={setGraderType}
|
||||
dueDate={dueDateState}
|
||||
setDueDate={setDueDateState}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||
<VisibilityTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
category={category}
|
||||
isSubsection={isSubsection}
|
||||
isVisibleToStaffOnly={isVisibleToStaffOnly}
|
||||
setIsVisibleToStaffOnly={setIsVisibleToStaffOnly}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
|
||||
hideAfterDue={hideAfterDueState}
|
||||
setHideAfterDue={setHideAfterDueState}
|
||||
showCorrectness={showCorrectnessState}
|
||||
setShowCorrectness={setShowCorrectnessState}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -213,35 +200,33 @@ const ConfigureModal = ({
|
||||
<Tabs>
|
||||
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
|
||||
<BasicTab
|
||||
releaseDate={releaseDate}
|
||||
setReleaseDate={setReleaseDate}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
isSubsection={isSubsection}
|
||||
graderType={graderType}
|
||||
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
||||
setGraderType={setGraderType}
|
||||
dueDate={dueDateState}
|
||||
setDueDate={setDueDateState}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||
<VisibilityTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
category={category}
|
||||
isSubsection={isSubsection}
|
||||
isVisibleToStaffOnly={isVisibleToStaffOnly}
|
||||
setIsVisibleToStaffOnly={setIsVisibleToStaffOnly}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
|
||||
hideAfterDue={hideAfterDueState}
|
||||
setHideAfterDue={setHideAfterDueState}
|
||||
showCorrectness={showCorrectnessState}
|
||||
setShowCorrectness={setShowCorrectnessState}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="advanced" title={intl.formatMessage(messages.advancedTabTitle)}>
|
||||
<AdvancedTab
|
||||
isTimeLimited={isTimeLimitedState}
|
||||
setIsTimeLimited={setIsTimeLimitedState}
|
||||
defaultTimeLimit={defaultTimeLimitMin}
|
||||
setDefaultTimeLimit={setDefaultTimeLimitMin}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
prereqs={prereqs}
|
||||
releasedToStudents={releasedToStudents}
|
||||
wasExamEverLinkedWithExternal={wasExamEverLinkedWithExternal}
|
||||
enableProctoredExams={enableProctoredExams}
|
||||
supportsOnboarding={supportsOnboarding}
|
||||
showReviewRules={showReviewRules}
|
||||
wasProctoredExam={isProctoredExam}
|
||||
onlineProctoringRules={onlineProctoringRules}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -249,15 +234,10 @@ const ConfigureModal = ({
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
return (
|
||||
<UnitTab
|
||||
isVisibleToStaffOnly={isVisibleToStaffOnly}
|
||||
setIsVisibleToStaffOnly={setIsVisibleToStaffOnly}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY && !ancestorHasStaffLock}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
selectedPartitionIndex={selectedPartitionIndex}
|
||||
setSelectedPartitionIndex={setSelectedPartitionIndex}
|
||||
selectedGroups={selectedGroups}
|
||||
add={add}
|
||||
remove={remove}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -266,36 +246,51 @@ const ConfigureModal = ({
|
||||
};
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<ModalDialog
|
||||
className="configure-modal"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<div data-testid="configure-modal">
|
||||
<ModalDialog.Header className="configure-modal__header">
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages.title, { title: displayName })}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body className="configure-modal__body">
|
||||
{renderModalBody(category)}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="pt-1">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button data-testid="configure-save-button" onClick={handleSave} disabled={saveButtonDisabled}>
|
||||
{intl.formatMessage(messages.saveButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
)
|
||||
<ModalDialog
|
||||
className="configure-modal"
|
||||
size="lg"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<div data-testid="configure-modal">
|
||||
<ModalDialog.Header className="configure-modal__header">
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages.title, { title: displayName })}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSave}
|
||||
validationSchema={validationSchema}
|
||||
validateOnBlur
|
||||
validateOnChange
|
||||
>
|
||||
{({
|
||||
values, handleSubmit, dirty, isValid, setFieldValue,
|
||||
}) => (
|
||||
<>
|
||||
<ModalDialog.Body className="configure-modal__body">
|
||||
<Form.Group size="sm" className="form-field">
|
||||
{renderModalBody(values, setFieldValue)}
|
||||
</Form.Group>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="pt-1">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button data-testid="configure-save-button" onClick={handleSubmit} disabled={!(dirty && isValid)}>
|
||||
{intl.formatMessage(messages.saveButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
.configure-modal {
|
||||
max-width: 33.6875rem;
|
||||
|
||||
.configure-modal__header {
|
||||
padding-top: 1.5rem;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.w-7rem {
|
||||
width: 7.2rem;
|
||||
}
|
||||
|
||||
.mw-1-25rem {
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
117
src/course-outline/configure-modal/PrereqSettings.jsx
Normal file
117
src/course-outline/configure-modal/PrereqSettings.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
|
||||
const PrereqSettings = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
prereqs,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
isPrereq,
|
||||
prereqUsageKey,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
} = values;
|
||||
|
||||
if (isPrereq === null || isPrereq === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectChange = (e) => {
|
||||
setFieldValue('prereqUsageKey', e.target.value);
|
||||
};
|
||||
|
||||
const prereqSelectionForm = () => (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.limitAccessTitle} /></h5>
|
||||
<hr />
|
||||
<Form>
|
||||
<Form.Text><FormattedMessage {...messages.limitAccessDescription} /></Form.Text>
|
||||
<Form.Group controlId="prereqForm.select">
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages.prerequisiteSelectLabel)}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={prereqUsageKey}
|
||||
onChange={handleSelectChange}
|
||||
role="combobox"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.noPrerequisiteOption)}
|
||||
</option>
|
||||
{prereqs.map((prereqOption) => (
|
||||
<option
|
||||
key={prereqOption.blockUsageKey}
|
||||
value={prereqOption.blockUsageKey}
|
||||
>
|
||||
{prereqOption.blockDisplayName}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
{prereqUsageKey && (
|
||||
<>
|
||||
<FormikControl
|
||||
name="prereqMinScore"
|
||||
value={prereqMinScore}
|
||||
label={<Form.Label>{intl.formatMessage(messages.minScoreLabel)}</Form.Label>}
|
||||
controlClassName="text-right"
|
||||
controlClasses="w-7rem"
|
||||
type="number"
|
||||
trailingElement="%"
|
||||
/>
|
||||
<FormikControl
|
||||
name="prereqMinCompletion"
|
||||
value={prereqMinCompletion}
|
||||
label={<Form.Label>{intl.formatMessage(messages.minCompletionLabel)}</Form.Label>}
|
||||
controlClassName="text-right"
|
||||
controlClasses="w-7rem"
|
||||
type="number"
|
||||
trailingElement="%"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleCheckboxChange = e => setFieldValue('isPrereq', e.target.checked);
|
||||
|
||||
return (
|
||||
<>
|
||||
{prereqs.length > 0 && prereqSelectionForm()}
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.prereqTitle} /></h5>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isPrereq} onChange={handleCheckboxChange}>
|
||||
<FormattedMessage {...messages.prereqCheckboxLabel} />
|
||||
</Form.Checkbox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PrereqSettings.defaultProps = {
|
||||
prereqs: [],
|
||||
};
|
||||
|
||||
PrereqSettings.propTypes = {
|
||||
values: PropTypes.shape({
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqUsageKey: PropTypes.string,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
}).isRequired,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PrereqSettings);
|
||||
@@ -1,36 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Form } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Form } from '@openedx/paragon';
|
||||
import {
|
||||
FormattedMessage, injectIntl, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Field } from 'formik';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const UnitTab = ({
|
||||
intl,
|
||||
isVisibleToStaffOnly,
|
||||
setIsVisibleToStaffOnly,
|
||||
values,
|
||||
setFieldValue,
|
||||
showWarning,
|
||||
userPartitionInfo,
|
||||
setSelectedPartitionIndex,
|
||||
selectedPartitionIndex,
|
||||
selectedGroups,
|
||||
add,
|
||||
remove,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
selectedPartitionIndex,
|
||||
} = values;
|
||||
|
||||
const handleChange = (e) => {
|
||||
setIsVisibleToStaffOnly(e.target.checked);
|
||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||
};
|
||||
|
||||
const handleSelect = (e) => {
|
||||
setSelectedPartitionIndex(parseInt(e.target.value, 10));
|
||||
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
||||
};
|
||||
|
||||
const handleCheckBoxChange = e => {
|
||||
if (e.target.checked) {
|
||||
add(e.target.value);
|
||||
} else {
|
||||
remove(e.target.value);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
|
||||
@@ -39,7 +36,7 @@ const UnitTab = ({
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{showWarning && (
|
||||
<Alert variant="warning">
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
@@ -71,16 +68,33 @@ const UnitTab = ({
|
||||
</Form.Control>
|
||||
|
||||
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
|
||||
<Form.Group>
|
||||
<Form.Group controlId="select-groups-checkboxes">
|
||||
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
|
||||
<Form.CheckboxSet
|
||||
name="groups"
|
||||
onChange={handleCheckBoxChange}
|
||||
value={selectedGroups}
|
||||
<div
|
||||
role="group"
|
||||
className="d-flex flex-column"
|
||||
data-testid="group-checkboxes"
|
||||
aria-labelledby="select-groups-checkboxes"
|
||||
>
|
||||
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (<Form.Checkbox key={group.id} value={`${group.id}`}>{group.name}</Form.Checkbox>))}
|
||||
</Form.CheckboxSet>
|
||||
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
|
||||
<Form.Group
|
||||
key={group.id}
|
||||
className="pgn__form-checkbox"
|
||||
>
|
||||
<Field
|
||||
as={Form.Control}
|
||||
className="flex-grow-0 mr-1"
|
||||
controlClassName="pgn__form-checkbox-input mr-1"
|
||||
type="checkbox"
|
||||
value={`${group.id}`}
|
||||
name="selectedGroups"
|
||||
/>
|
||||
<Form.Label isInline>
|
||||
{group.name}
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
))}
|
||||
</div>
|
||||
</Form.Group>
|
||||
)}
|
||||
</Form.Group>
|
||||
@@ -90,9 +104,14 @@ const UnitTab = ({
|
||||
};
|
||||
|
||||
UnitTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
setIsVisibleToStaffOnly: PropTypes.func.isRequired,
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
selectedPartitionIndex: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
|
||||
@@ -106,20 +125,9 @@ UnitTab.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
scheme: PropTypes.string.isRequired,
|
||||
}).isRequired).isRequired,
|
||||
selectedGroupsLabel: PropTypes.string.isRequired,
|
||||
selectedGroupsLabel: PropTypes.string,
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
setSelectedPartitionIndex: PropTypes.func.isRequired,
|
||||
selectedPartitionIndex: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
selectedGroups: PropTypes.arrayOf(PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
])).isRequired,
|
||||
add: PropTypes.func.isRequired,
|
||||
remove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(UnitTab);
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Form } from '@edx/paragon';
|
||||
import { Alert, Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
|
||||
const VisibilityTab = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
category,
|
||||
isVisibleToStaffOnly,
|
||||
setIsVisibleToStaffOnly,
|
||||
showWarning,
|
||||
isSubsection,
|
||||
hideAfterDue,
|
||||
setHideAfterDue,
|
||||
showCorrectness,
|
||||
setShowCorrectness,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const visibilityTitle = COURSE_BLOCK_NAMES[category]?.name;
|
||||
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
hideAfterDue,
|
||||
showCorrectness,
|
||||
} = values;
|
||||
|
||||
const handleChange = (e) => {
|
||||
setIsVisibleToStaffOnly(e.target.checked);
|
||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||
};
|
||||
|
||||
const getVisibilityValue = () => {
|
||||
@@ -35,19 +38,19 @@ const VisibilityTab = ({
|
||||
const visibilityChanged = (e) => {
|
||||
const selected = e.target.value;
|
||||
if (selected === 'hide') {
|
||||
setIsVisibleToStaffOnly(true);
|
||||
setHideAfterDue(false);
|
||||
setFieldValue('isVisibleToStaffOnly', true);
|
||||
setFieldValue('hideAfterDue', false);
|
||||
} else if (selected === 'hideDue') {
|
||||
setIsVisibleToStaffOnly(false);
|
||||
setHideAfterDue(true);
|
||||
setFieldValue('isVisibleToStaffOnly', false);
|
||||
setFieldValue('hideAfterDue', true);
|
||||
} else {
|
||||
setIsVisibleToStaffOnly(false);
|
||||
setHideAfterDue(false);
|
||||
setFieldValue('isVisibleToStaffOnly', false);
|
||||
setFieldValue('hideAfterDue', false);
|
||||
}
|
||||
};
|
||||
|
||||
const correctnessChanged = (e) => {
|
||||
setShowCorrectness(e.target.value);
|
||||
setFieldValue('showCorrectness', e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -77,6 +80,11 @@ const VisibilityTab = ({
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.hideEntireSubsectionDescription} /></Form.Text>
|
||||
</Form.RadioSet>
|
||||
{showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.subsectionVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.assessmentResultsVisibility} /></h5>
|
||||
<Form.RadioSet
|
||||
name="assessmentResultsVisibility"
|
||||
@@ -103,29 +111,25 @@ const VisibilityTab = ({
|
||||
</Form.Checkbox>
|
||||
)
|
||||
}
|
||||
{showWarning && (
|
||||
<>
|
||||
<hr />
|
||||
<Alert variant="warning">
|
||||
<FormattedMessage {...messages.sectionVisibilityWarning} />
|
||||
</Alert>
|
||||
</>
|
||||
|
||||
{showWarning && !isSubsection && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.sectionVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
VisibilityTab.propTypes = {
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
hideAfterDue: PropTypes.bool.isRequired,
|
||||
showCorrectness: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
setIsVisibleToStaffOnly: PropTypes.func.isRequired,
|
||||
isSubsection: PropTypes.bool.isRequired,
|
||||
hideAfterDue: PropTypes.bool.isRequired,
|
||||
setHideAfterDue: PropTypes.func.isRequired,
|
||||
showCorrectness: PropTypes.string.isRequired,
|
||||
setShowCorrectness: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(VisibilityTab);
|
||||
|
||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.title',
|
||||
defaultMessage: 'Basic',
|
||||
},
|
||||
notGradedTypeOption: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.notGradedTypeOption',
|
||||
defaultMessage: 'Not Graded',
|
||||
},
|
||||
releaseDateAndTime: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date-and-time',
|
||||
defaultMessage: 'Release Date and Time',
|
||||
@@ -43,12 +47,16 @@ const messages = defineMessages({
|
||||
},
|
||||
sectionVisibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility-warning',
|
||||
defaultMessage: 'If you make this section visible to learners, learners will be able to see its content after the release date has passed and you have published the unit. Only units that are explicitly hidden from learners will remain hidden after you clear this option for the section.',
|
||||
defaultMessage: 'If you make this section visible to learners, learners will be able to see its content after the release date has passed and you have published the section. Only units that are explicitly hidden from learners will remain hidden after you clear this option for the section.',
|
||||
},
|
||||
unitVisibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-visibility-warning',
|
||||
defaultMessage: 'If the unit was previously published and released to learners, any changes you made to the unit when it was hidden will now be visible to learners.',
|
||||
},
|
||||
subsectionVisibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.subsection-visibility-warning',
|
||||
defaultMessage: 'If you select an option other than "Hide entire subsection", published units in this subsection will become available to learners unless they are explicitly hidden.',
|
||||
},
|
||||
unitSelectGroup: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group',
|
||||
defaultMessage: 'Select one or more groups:',
|
||||
@@ -157,6 +165,30 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the instructor Dashboard.',
|
||||
},
|
||||
proctoredExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExam',
|
||||
defaultMessage: 'Proctored',
|
||||
},
|
||||
proctoredExamDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules. Please note that setting this exam as proctored will change the visibility settings to "Hide content after due date."',
|
||||
},
|
||||
onboardingExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.onboardingExam',
|
||||
defaultMessage: 'Onboarding',
|
||||
},
|
||||
onboardingExamDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Use Onboarding to introduce learners to proctoring, verify their identity, and create an onboarding profile. Learners must complete the onboarding profile step prior to taking a proctored exam. Profile reviews take 2+ business days.',
|
||||
},
|
||||
practiceExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.practiceExam',
|
||||
defaultMessage: 'Practice proctored',
|
||||
},
|
||||
practiceExamDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner\'s grade.',
|
||||
},
|
||||
advancedTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.title',
|
||||
defaultMessage: 'Advanced',
|
||||
@@ -169,6 +201,70 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description',
|
||||
defaultMessage: 'Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.',
|
||||
},
|
||||
prereqTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prereqTitle',
|
||||
defaultMessage: 'Use as a Prerequisite',
|
||||
},
|
||||
prereqCheckboxLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prereqCheckboxLabel',
|
||||
defaultMessage: 'Make this subsection available as a prerequisite to other content',
|
||||
},
|
||||
limitAccessTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.limitAccessTitle',
|
||||
defaultMessage: 'Limit access',
|
||||
},
|
||||
limitAccessDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.limitAccessDescription',
|
||||
defaultMessage: 'Select a prerequisite subsection and enter a minimum score percentage and minimum completion percentage to limit access to this subsection. Allowed values are 0-100',
|
||||
},
|
||||
noPrerequisiteOption: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.noPrerequisiteOption',
|
||||
defaultMessage: 'No prerequisite',
|
||||
},
|
||||
prerequisiteSelectLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prerequisiteSelectLabel',
|
||||
defaultMessage: 'Prerequisite:',
|
||||
},
|
||||
minScoreLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minScoreLabel',
|
||||
defaultMessage: 'Minimum score:',
|
||||
},
|
||||
minCompletionLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minCompletionLabel',
|
||||
defaultMessage: 'Minimum completion:',
|
||||
},
|
||||
minScoreError: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minScoreError',
|
||||
defaultMessage: 'The minimum score percentage must be a whole number between 0 and 100.',
|
||||
},
|
||||
minCompletionError: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minCompletionError',
|
||||
defaultMessage: 'The minimum completion percentage must be a whole number between 0 and 100.',
|
||||
},
|
||||
proctoredExamLockedAndisNotProctoredExamAlert: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExamLockedAndisNotProctoredExamAlert',
|
||||
defaultMessage: 'This subsection was released to learners as a proctored exam, but was reverted back to a basic or timed exam. You may not configure it as a proctored exam now. Contact edX Support for assistance.',
|
||||
},
|
||||
proctoredExamLockedAndisProctoredExamAlert: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExamLockedAndisProctoredExamAlert',
|
||||
defaultMessage: 'This proctored exam has been released to learners. You may not convert it to another type of special exam. You may revert this subsection back to being a basic exam by selecting \'None\', or a timed exam, but you will NOT be able to configure it as a proctored exam in the future.',
|
||||
},
|
||||
reviewRulesLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesLabel',
|
||||
defaultMessage: 'Review rules',
|
||||
},
|
||||
reviewRulesDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescription',
|
||||
defaultMessage: 'Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam.',
|
||||
},
|
||||
reviewRulesDescriptionWithLink: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescriptionWithLink',
|
||||
defaultMessage: 'Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam, along with the {hyperlink}.',
|
||||
},
|
||||
reviewRulesDescriptionLinkText: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescriptionLinkText',
|
||||
defaultMessage: 'general proctored exam rules',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const ITEM_BADGE_STATUS = /** @type {const} */ ({
|
||||
live: 'live',
|
||||
gated: 'gated',
|
||||
publishedNotLive: 'published_not_live',
|
||||
unpublishedChanges: 'unpublished_changes',
|
||||
staffOnly: 'staff_only',
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${rein
|
||||
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
|
||||
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
|
||||
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
|
||||
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
|
||||
|
||||
/**
|
||||
* @typedef {Object} courseOutline
|
||||
@@ -242,11 +243,19 @@ export async function configureCourseSection(sectionId, isVisibleToStaffOnly, st
|
||||
* @param {string} isVisibleToStaffOnly
|
||||
* @param {string} releaseDate
|
||||
* @param {string} graderType
|
||||
* @param {string} dueDateState
|
||||
* @param {string} isTimeLimitedState
|
||||
* @param {string} defaultTimeLimitMin
|
||||
* @param {string} hideAfterDueState
|
||||
* @param {string} showCorrectnessState
|
||||
* @param {string} dueDate
|
||||
* @param {boolean} isProctoredExam,
|
||||
* @param {boolean} isOnboardingExam,
|
||||
* @param {boolean} isPracticeExam,
|
||||
* @param {string} examReviewRules,
|
||||
* @param {boolean} isTimeLimited
|
||||
* @param {number} defaultTimeLimitMin
|
||||
* @param {string} hideAfterDue
|
||||
* @param {string} showCorrectness
|
||||
* @param {boolean} isPrereq,
|
||||
* @param {string} prereqUsageKey,
|
||||
* @param {number} prereqMinScore,
|
||||
* @param {number} prereqMinCompletion,
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function configureCourseSubsection(
|
||||
@@ -254,28 +263,40 @@ export async function configureCourseSubsection(
|
||||
isVisibleToStaffOnly,
|
||||
releaseDate,
|
||||
graderType,
|
||||
dueDateState,
|
||||
isTimeLimitedState,
|
||||
dueDate,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
defaultTimeLimitMin,
|
||||
hideAfterDueState,
|
||||
showCorrectnessState,
|
||||
hideAfterDue,
|
||||
showCorrectness,
|
||||
isPrereq,
|
||||
prereqUsageKey,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getCourseItemApiUrl(itemId), {
|
||||
publish: 'republish',
|
||||
graderType,
|
||||
isPrereq,
|
||||
prereqUsageKey,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
metadata: {
|
||||
// The backend expects metadata.visible_to_staff_only to either true or null
|
||||
visible_to_staff_only: isVisibleToStaffOnly ? true : null,
|
||||
due: dueDateState,
|
||||
hide_after_due: hideAfterDueState,
|
||||
show_correctness: showCorrectnessState,
|
||||
is_practice_exam: false,
|
||||
is_time_limited: isTimeLimitedState,
|
||||
exam_review_rules: '',
|
||||
is_proctored_enabled: false,
|
||||
due: dueDate,
|
||||
hide_after_due: hideAfterDue,
|
||||
show_correctness: showCorrectness,
|
||||
is_practice_exam: isPracticeExam,
|
||||
is_time_limited: isTimeLimited,
|
||||
is_proctored_enabled: isProctoredExam || isPracticeExam || isOnboardingExam,
|
||||
exam_review_rules: examReviewRules,
|
||||
default_time_limit_minutes: defaultTimeLimitMin,
|
||||
is_onboarding_exam: false,
|
||||
is_onboarding_exam: isOnboardingExam,
|
||||
start: releaseDate,
|
||||
},
|
||||
});
|
||||
@@ -412,3 +433,42 @@ export async function setVideoSharingOption(courseId, videoSharingOption) {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy block to clipboard
|
||||
* @param {string} usageKey
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function copyBlockToClipboard(usageKey) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getClipboardUrl(), {
|
||||
usage_key: usageKey,
|
||||
});
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste block to clipboard
|
||||
* @param {string} parentLocator
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function pasteBlock(parentLocator) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getXBlockBaseApiUrl(), {
|
||||
parent_locator: parentLocator,
|
||||
staged_content: 'clipboard',
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss notification
|
||||
* @param {string} url
|
||||
* @returns void
|
||||
*/
|
||||
export async function dismissNotification(url) {
|
||||
await getAuthenticatedHttpClient()
|
||||
.delete(url);
|
||||
}
|
||||
|
||||
@@ -7,3 +7,6 @@ export const getCurrentItem = (state) => state.courseOutline.currentItem;
|
||||
export const getCurrentSection = (state) => state.courseOutline.currentSection;
|
||||
export const getCurrentSubsection = (state) => state.courseOutline.currentSubsection;
|
||||
export const getCourseActions = (state) => state.courseOutline.actions;
|
||||
export const getCustomRelativeDatesActiveFlag = (state) => state.courseOutline.isCustomRelativeDatesActive;
|
||||
export const getInitialUserClipboard = (state) => state.courseOutline.initialUserClipboard;
|
||||
export const getProctoredExamsFlag = (state) => state.courseOutline.enableProctoredExams;
|
||||
|
||||
@@ -28,6 +28,7 @@ const slice = createSlice({
|
||||
videoSharingOptions: VIDEO_SHARING_OPTIONS.perVideo,
|
||||
},
|
||||
sectionsList: [],
|
||||
isCustomRelativeDatesActive: false,
|
||||
currentSection: {},
|
||||
currentSubsection: {},
|
||||
currentItem: {},
|
||||
@@ -37,11 +38,21 @@ const slice = createSlice({
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
initialUserClipboard: {
|
||||
content: {},
|
||||
sourceUsageKey: null,
|
||||
sourceContexttitle: null,
|
||||
sourceEditUrl: null,
|
||||
},
|
||||
enableProctoredExams: false,
|
||||
},
|
||||
reducers: {
|
||||
fetchOutlineIndexSuccess: (state, { payload }) => {
|
||||
state.outlineIndexData = payload;
|
||||
state.sectionsList = payload.courseStructure?.childInfo?.children || [];
|
||||
state.isCustomRelativeDatesActive = payload.isCustomRelativeDatesActive;
|
||||
state.initialUserClipboard = payload.initialUserClipboard;
|
||||
state.enableProctoredExams = payload.courseStructure?.enableProctoredExams;
|
||||
},
|
||||
updateOutlineIndexLoadingStatus: (state, { payload }) => {
|
||||
state.loadingStatus = {
|
||||
@@ -67,6 +78,9 @@ const slice = createSlice({
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
updateClipboardContent: (state, { payload }) => {
|
||||
state.initialUserClipboard = payload;
|
||||
},
|
||||
updateCourseActions: (state, { payload }) => {
|
||||
state.actions = {
|
||||
...state.actions,
|
||||
@@ -203,6 +217,7 @@ export const {
|
||||
reorderSectionList,
|
||||
reorderSubsectionList,
|
||||
reorderUnitList,
|
||||
updateClipboardContent,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -28,6 +28,9 @@ import {
|
||||
setSectionOrderList,
|
||||
setVideoSharingOption,
|
||||
setCourseItemOrderList,
|
||||
copyBlockToClipboard,
|
||||
pasteBlock,
|
||||
dismissNotification,
|
||||
} from './api';
|
||||
import {
|
||||
addSection,
|
||||
@@ -49,6 +52,7 @@ import {
|
||||
reorderSectionList,
|
||||
reorderSubsectionList,
|
||||
reorderUnitList,
|
||||
updateClipboardContent,
|
||||
} from './slice';
|
||||
|
||||
export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
@@ -258,11 +262,19 @@ export function configureCourseSubsectionQuery(
|
||||
isVisibleToStaffOnly,
|
||||
releaseDate,
|
||||
graderType,
|
||||
dueDateState,
|
||||
isTimeLimitedState,
|
||||
dueDate,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
defaultTimeLimitMin,
|
||||
hideAfterDueState,
|
||||
showCorrectnessState,
|
||||
hideAfterDue,
|
||||
showCorrectness,
|
||||
isPrereq,
|
||||
prereqUsageKey,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
) {
|
||||
return async (dispatch) => {
|
||||
dispatch(configureCourseItemQuery(
|
||||
@@ -272,11 +284,19 @@ export function configureCourseSubsectionQuery(
|
||||
isVisibleToStaffOnly,
|
||||
releaseDate,
|
||||
graderType,
|
||||
dueDateState,
|
||||
isTimeLimitedState,
|
||||
dueDate,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
defaultTimeLimitMin,
|
||||
hideAfterDueState,
|
||||
showCorrectnessState,
|
||||
hideAfterDue,
|
||||
showCorrectness,
|
||||
isPrereq,
|
||||
prereqUsageKey,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
),
|
||||
));
|
||||
};
|
||||
@@ -371,7 +391,7 @@ export function deleteCourseUnitQuery(unitId, subsectionId, sectionId) {
|
||||
function duplicateCourseItemQuery(itemId, parentLocator, duplicateFn) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.duplicating));
|
||||
|
||||
try {
|
||||
await duplicateCourseItem(itemId, parentLocator).then(async (result) => {
|
||||
@@ -560,3 +580,61 @@ export function setUnitOrderListQuery(sectionId, subsectionId, unitListIds, rest
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function setClipboardContent(usageKey, broadcastClipboard) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.copying));
|
||||
|
||||
try {
|
||||
await copyBlockToClipboard(usageKey).then(async (result) => {
|
||||
const status = result?.content?.status;
|
||||
if (status === 'ready') {
|
||||
dispatch(updateClipboardContent(result));
|
||||
broadcastClipboard(result);
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(hideProcessingNotification());
|
||||
} else {
|
||||
throw new Error(`Unexpected clipboard status "${status}" in successful API response.`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function pasteClipboardContent(parentLocator, sectionId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.pasting));
|
||||
|
||||
try {
|
||||
await pasteBlock(parentLocator).then(async (result) => {
|
||||
if (result) {
|
||||
dispatch(fetchCourseSectionQuery(sectionId, true));
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(hideProcessingNotification());
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function dismissNotificationQuery(url) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
|
||||
try {
|
||||
await dismissNotification(url).then(async () => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ActionRow,
|
||||
Button,
|
||||
AlertModal,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row } from '@edx/paragon';
|
||||
import { Col, Row } from '@openedx/paragon';
|
||||
import { SortableItem } from '@edx/frontend-lib-content-components';
|
||||
|
||||
const ConditionalSortableElement = ({
|
||||
@@ -24,9 +24,9 @@ const ConditionalSortableElement = ({
|
||||
id={id}
|
||||
componentStyle={style}
|
||||
>
|
||||
<div className="extend-margin">
|
||||
<Col className="extend-margin px-0">
|
||||
{children}
|
||||
</div>
|
||||
</Col>
|
||||
</SortableItem>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,9 @@ const ConditionalSortableElement = ({
|
||||
style={style}
|
||||
className="mx-0"
|
||||
>
|
||||
{children}
|
||||
<Col className="px-0">
|
||||
{children}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
.extend-margin {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
.item-children {
|
||||
margin-right: -2.75rem;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Add as IconAdd } from '@edx/paragon/icons/es5';
|
||||
import { Button, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons/es5';
|
||||
import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, AlertModal, Button, Hyperlink,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, OverlayTrigger, Tooltip } from '@edx/paragon';
|
||||
import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon';
|
||||
import {
|
||||
Add as IconAdd,
|
||||
ArrowDropDown as ArrowDownIcon,
|
||||
ArrowDropUp as ArrowUpIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Button,
|
||||
ActionRow,
|
||||
Hyperlink,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { Formik } from 'formik';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToggle } from '@edx/paragon';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { COURSE_BLOCK_NAMES } from './constants';
|
||||
import { useBroadcastChannel } from '../generic/broadcast-channel/hooks';
|
||||
import {
|
||||
setCurrentItem,
|
||||
setCurrentSection,
|
||||
updateSavingStatus,
|
||||
updateClipboardContent,
|
||||
} from './data/slice';
|
||||
import {
|
||||
getLoadingStatus,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
getCurrentItem,
|
||||
getCurrentSection,
|
||||
getCurrentSubsection,
|
||||
getCustomRelativeDatesActiveFlag,
|
||||
} from './data/selectors';
|
||||
import {
|
||||
addNewSectionQuery,
|
||||
@@ -47,13 +50,28 @@ import {
|
||||
setVideoSharingOptionQuery,
|
||||
setSubsectionOrderListQuery,
|
||||
setUnitOrderListQuery,
|
||||
setClipboardContent,
|
||||
pasteClipboardContent,
|
||||
dismissNotificationQuery,
|
||||
} from './data/thunk';
|
||||
|
||||
const useCourseOutline = ({ courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { reindexLink, courseStructure, lmsLink } = useSelector(getOutlineIndexData);
|
||||
const {
|
||||
reindexLink,
|
||||
courseStructure,
|
||||
lmsLink,
|
||||
notificationDismissUrl,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
mfeProctoredExamSettingsUrl,
|
||||
advanceSettingsUrl,
|
||||
} = useSelector(getOutlineIndexData);
|
||||
const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus);
|
||||
const statusBarData = useSelector(getStatusBarData);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
@@ -62,6 +80,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
const currentItem = useSelector(getCurrentItem);
|
||||
const currentSection = useSelector(getCurrentSection);
|
||||
const currentSubsection = useSelector(getCurrentSubsection);
|
||||
const isCustomRelativeDatesActive = useSelector(getCustomRelativeDatesActiveFlag);
|
||||
|
||||
const [isEnableHighlightsModalOpen, openEnableHighlightsModal, closeEnableHighlightsModal] = useToggle(false);
|
||||
const [isSectionsExpanded, setSectionsExpanded] = useState(true);
|
||||
@@ -72,6 +91,17 @@ const useCourseOutline = ({ courseId }) => {
|
||||
const [isPublishModalOpen, openPublishModal, closePublishModal] = useToggle(false);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
const clipboardBroadcastChannel = useBroadcastChannel('studio_clipboard_channel', (message) => {
|
||||
dispatch(updateClipboardContent(message));
|
||||
});
|
||||
|
||||
const handleCopyToClipboardClick = (usageKey) => {
|
||||
dispatch(setClipboardContent(usageKey, clipboardBroadcastChannel.postMessage));
|
||||
};
|
||||
|
||||
const handlePasteClipboardClick = (parentLocator, sectionId) => {
|
||||
dispatch(pasteClipboardContent(parentLocator, sectionId));
|
||||
};
|
||||
|
||||
const handleNewSectionSubmit = () => {
|
||||
dispatch(addNewSectionQuery(courseStructure.id));
|
||||
@@ -82,15 +112,15 @@ const useCourseOutline = ({ courseId }) => {
|
||||
};
|
||||
|
||||
const getUnitUrl = (locator) => {
|
||||
if (process.env.ENABLE_UNIT_PAGE === 'true') {
|
||||
return `/course/container/${locator}`;
|
||||
if (getConfig().ENABLE_UNIT_PAGE === 'true') {
|
||||
return `/course/${courseId}/container/${locator}`;
|
||||
}
|
||||
return `${getConfig().STUDIO_BASE_URL}/container/${locator}`;
|
||||
};
|
||||
|
||||
const openUnitPage = (locator) => {
|
||||
const url = getUnitUrl(locator);
|
||||
if (process.env.ENABLE_UNIT_PAGE === 'true') {
|
||||
if (getConfig().ENABLE_UNIT_PAGE === 'true') {
|
||||
navigate(url);
|
||||
} else {
|
||||
window.location.assign(url);
|
||||
@@ -222,6 +252,10 @@ const useCourseOutline = ({ courseId }) => {
|
||||
dispatch(setUnitOrderListQuery(sectionId, subsectionId, unitListIds, restoreCallback));
|
||||
};
|
||||
|
||||
const handleDismissNotification = () => {
|
||||
dispatch(dismissNotificationQuery(notificationDismissUrl));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseOutlineIndexQuery(courseId));
|
||||
dispatch(fetchCourseBestPracticesQuery({ courseId }));
|
||||
@@ -242,6 +276,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
courseActions,
|
||||
savingStatus,
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
|
||||
isReIndexShow: Boolean(reindexLink),
|
||||
showSuccessAlert,
|
||||
@@ -286,6 +321,17 @@ const useCourseOutline = ({ courseId }) => {
|
||||
handleSubsectionDragAndDrop,
|
||||
handleVideoSharingOptionChange,
|
||||
handleUnitDragAndDrop,
|
||||
handleCopyToClipboardClick,
|
||||
handlePasteClipboardClick,
|
||||
notificationDismissUrl,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
mfeProctoredExamSettingsUrl,
|
||||
handleDismissNotification,
|
||||
advanceSettingsUrl,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.section-list.button.new-section',
|
||||
defaultMessage: 'New section',
|
||||
},
|
||||
alertFailedGeneric: {
|
||||
id: 'course-authoring.course-outline.general.alert.error.description',
|
||||
defaultMessage: 'Unable to {actionName} {type}. Please try again.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { HelpSidebar } from '../../generic/help-sidebar';
|
||||
|
||||
283
src/course-outline/page-alerts/PageAlerts.jsx
Normal file
283
src/course-outline/page-alerts/PageAlerts.jsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ErrorAlert } from '@edx/frontend-lib-content-components';
|
||||
import {
|
||||
Campaign as CampaignIcon,
|
||||
InfoOutline as InfoOutlineIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
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';
|
||||
|
||||
const PageAlerts = ({
|
||||
notificationDismissUrl,
|
||||
handleDismissNotification,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
mfeProctoredExamSettingsUrl,
|
||||
advanceSettingsUrl,
|
||||
savingStatus,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
const [showConfigAlert, setShowConfigAlert] = useState(true);
|
||||
const [showDiscussionAlert, setShowDiscussionAlert] = useState(true);
|
||||
|
||||
const configurationErrors = () => {
|
||||
if (!notificationDismissUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onDismiss = () => {
|
||||
setShowConfigAlert(false);
|
||||
handleDismissNotification();
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertMessage
|
||||
title={intl.formatMessage(messages.configurationErrorTitle)}
|
||||
description={intl.formatMessage(messages.configurationErrorText)}
|
||||
dismissible
|
||||
show={showConfigAlert}
|
||||
icon={CampaignIcon}
|
||||
variant="info"
|
||||
onClose={onDismiss}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const discussionNotification = () => {
|
||||
const { providerType } = discussionsSettings || {};
|
||||
if (providerType !== 'openedx') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onDismiss = () => {
|
||||
setShowDiscussionAlert(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
dismissible
|
||||
show={showDiscussionAlert}
|
||||
icon={InfoOutlineIcon}
|
||||
variant="info"
|
||||
onClose={onDismiss}
|
||||
actions={[
|
||||
<Button
|
||||
href={discussionsIncontextLearnmoreUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{intl.formatMessage(messages.discussionNotificationLearnMore)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="font-weight-normal text-gray mw-md">
|
||||
{intl.formatMessage(messages.discussionNotificationText, {
|
||||
platformName: process.env.SITE_NAME,
|
||||
})}
|
||||
</div>
|
||||
<Hyperlink
|
||||
showLaunchIcon={false}
|
||||
destination={discussionsIncontextFeedbackUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{intl.formatMessage(messages.discussionNotificationFeedback)}
|
||||
</Hyperlink>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
const deprecationWarning = () => {
|
||||
const {
|
||||
blocks,
|
||||
deprecatedEnabledBlockTypes,
|
||||
} = deprecatedBlocksInfo || {};
|
||||
|
||||
if (blocks?.length > 0 || deprecatedEnabledBlockTypes?.length > 0) {
|
||||
return (
|
||||
<Alert
|
||||
icon={WarningIcon}
|
||||
variant="warning"
|
||||
>
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages.deprecationWarningTitle)}
|
||||
</Alert.Heading>
|
||||
{blocks?.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
{intl.formatMessage(messages.deprecationWarningBlocksText)}
|
||||
</div>
|
||||
<ul>
|
||||
{blocks.map(([parentUrl, name]) => (
|
||||
<li key={parentUrl}>
|
||||
<Hyperlink
|
||||
destination={parentUrl}
|
||||
>
|
||||
{name || intl.formatMessage(messages.deprecatedComponentName)}
|
||||
</Hyperlink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{deprecatedEnabledBlockTypes?.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
{...messages.deprecationWarningDeprecatedBlockText}
|
||||
values={{
|
||||
platformName: process.env.SITE_NAME,
|
||||
hyperlink: (
|
||||
<Hyperlink
|
||||
destination={`${studioBaseUrl}${deprecatedBlocksInfo.advanceSettingsUrl}`}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.advancedSettingLinkText}
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ul>
|
||||
{deprecatedEnabledBlockTypes.map((name) => (
|
||||
<li key={name}>
|
||||
{name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const proctoringAlerts = () => {
|
||||
if (proctoringErrors?.length > 0) {
|
||||
return (
|
||||
<AlertProctoringError
|
||||
icon={InfoOutlineIcon}
|
||||
proctoringErrorsData={proctoringErrors}
|
||||
aria-hidden="true"
|
||||
aria-labelledby={intl.formatMessage(advancedSettingsMessages.alertProctoringAriaLabelledby)}
|
||||
aria-describedby={intl.formatMessage(advancedSettingsMessages.alertProctoringDescribedby)}
|
||||
>
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages.proctoringErrorTitle)}
|
||||
</Alert.Heading>
|
||||
<div className="mb-2">
|
||||
{mfeProctoredExamSettingsUrl
|
||||
? (
|
||||
<FormattedMessage
|
||||
{...messages.proctoringErrorText}
|
||||
values={{
|
||||
hyperlink: (
|
||||
<Hyperlink
|
||||
destination={mfeProctoredExamSettingsUrl}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.proctoredSettingsLinkText}
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
{...messages.proctoringErrorText}
|
||||
values={{
|
||||
hyperlink: (
|
||||
<Hyperlink
|
||||
destination={`${studioBaseUrl}${advanceSettingsUrl}`}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.advancedSettingLinkText}
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AlertProctoringError>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{configurationErrors()}
|
||||
{discussionNotification()}
|
||||
{deprecationWarning()}
|
||||
{proctoringAlerts()}
|
||||
<ErrorAlert hideHeading isError={savingStatus === RequestStatus.FAILED}>
|
||||
{intl.formatMessage(messages.alertFailedGeneric, { actionName: 'save', type: 'changes' })}
|
||||
</ErrorAlert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PageAlerts.defaultProps = {
|
||||
notificationDismissUrl: '',
|
||||
handleDismissNotification: null,
|
||||
discussionsSettings: {},
|
||||
discussionsIncontextFeedbackUrl: '',
|
||||
discussionsIncontextLearnmoreUrl: '',
|
||||
deprecatedBlocksInfo: {},
|
||||
proctoringErrors: [],
|
||||
mfeProctoredExamSettingsUrl: '',
|
||||
advanceSettingsUrl: '',
|
||||
savingStatus: '',
|
||||
};
|
||||
|
||||
PageAlerts.propTypes = {
|
||||
notificationDismissUrl: PropTypes.string,
|
||||
handleDismissNotification: PropTypes.func,
|
||||
discussionsSettings: PropTypes.shape({
|
||||
providerType: PropTypes.string,
|
||||
}),
|
||||
discussionsIncontextFeedbackUrl: PropTypes.string,
|
||||
discussionsIncontextLearnmoreUrl: PropTypes.string,
|
||||
deprecatedBlocksInfo: PropTypes.shape({
|
||||
blocks: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||
deprecatedEnabledBlockTypes: PropTypes.arrayOf(PropTypes.string),
|
||||
advanceSettingsUrl: PropTypes.string,
|
||||
}),
|
||||
proctoringErrors: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
model: PropTypes.shape({
|
||||
deprecated: PropTypes.bool,
|
||||
displayName: PropTypes.string,
|
||||
help: PropTypes.string,
|
||||
hideOnEnabledPublisher: PropTypes.bool,
|
||||
}),
|
||||
value: PropTypes.string,
|
||||
})),
|
||||
mfeProctoredExamSettingsUrl: PropTypes.string,
|
||||
advanceSettingsUrl: PropTypes.string,
|
||||
savingStatus: PropTypes.string,
|
||||
};
|
||||
|
||||
export default PageAlerts;
|
||||
155
src/course-outline/page-alerts/PageAlerts.test.jsx
Normal file
155
src/course-outline/page-alerts/PageAlerts.test.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React from 'react';
|
||||
import { act, render, fireEvent } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import PageAlerts from './PageAlerts';
|
||||
import messages from './messages';
|
||||
import initializeStore from '../../store';
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage: (message) => message.defaultMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
let store;
|
||||
const handleDismissNotification = jest.fn();
|
||||
|
||||
const pageAlertsData = {
|
||||
notificationDismissUrl: '',
|
||||
handleDismissNotification: null,
|
||||
discussionsSettings: {},
|
||||
discussionsIncontextFeedbackUrl: '',
|
||||
discussionsIncontextLearnmoreUrl: '',
|
||||
deprecatedBlocksInfo: {},
|
||||
proctoringErrors: [],
|
||||
mfeProctoredExamSettingsUrl: '',
|
||||
advanceSettingsUrl: '',
|
||||
savingStatus: '',
|
||||
};
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store} messages={{}}>
|
||||
<IntlProvider locale="en">
|
||||
<PageAlerts
|
||||
{...pageAlertsData}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<PageAlerts />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('renders null when no alerts are present', () => {
|
||||
const { queryByTestId } = renderComponent();
|
||||
expect(queryByTestId('browser-router')).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders configuration alerts', async () => {
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
notificationDismissUrl: 'some-url',
|
||||
handleDismissNotification,
|
||||
});
|
||||
|
||||
expect(queryByText(messages.configurationErrorTitle.defaultMessage)).toBeInTheDocument();
|
||||
const dismissBtn = queryByText('Dismiss');
|
||||
await act(async () => fireEvent.click(dismissBtn));
|
||||
|
||||
expect(handleDismissNotification).toBeCalled();
|
||||
});
|
||||
|
||||
it('renders discussion alerts', async () => {
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
discussionsSettings: {
|
||||
providerType: 'openedx',
|
||||
},
|
||||
discussionsIncontextFeedbackUrl: 'some-feedback-url',
|
||||
discussionsIncontextLearnmoreUrl: 'some-learn-more-url',
|
||||
});
|
||||
|
||||
expect(queryByText(messages.discussionNotificationText.defaultMessage)).toBeInTheDocument();
|
||||
const learnMoreBtn = queryByText(messages.discussionNotificationLearnMore.defaultMessage);
|
||||
expect(learnMoreBtn).toBeInTheDocument();
|
||||
expect(learnMoreBtn).toHaveAttribute('href', 'some-learn-more-url');
|
||||
|
||||
const feedbackLink = queryByText(messages.discussionNotificationFeedback.defaultMessage);
|
||||
expect(feedbackLink).toBeInTheDocument();
|
||||
expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
|
||||
});
|
||||
|
||||
it('renders deprecation warning alerts', async () => {
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
deprecatedBlocksInfo: {
|
||||
blocks: [['url1', 'block1'], ['url2']],
|
||||
deprecatedEnabledBlockTypes: ['lti', 'video'],
|
||||
advanceSettingsUrl: '/some-url',
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByText(messages.deprecationWarningTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText(messages.deprecationWarningBlocksText.defaultMessage)).toBeInTheDocument();
|
||||
expect(queryByText('block1')).toHaveAttribute('href', 'url1');
|
||||
expect(queryByText(messages.deprecatedComponentName.defaultMessage)).toHaveAttribute('href', 'url2');
|
||||
|
||||
const feedbackLink = queryByText(messages.advancedSettingLinkText.defaultMessage);
|
||||
expect(feedbackLink).toBeInTheDocument();
|
||||
expect(feedbackLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}/some-url`);
|
||||
expect(queryByText('lti')).toBeInTheDocument();
|
||||
expect(queryByText('video')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders proctoring alerts with mfe settings link', async () => {
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
mfeProctoredExamSettingsUrl: 'mfe-url',
|
||||
proctoringErrors: [
|
||||
{ key: '1', model: { displayName: 'error 1' }, message: 'message 1' },
|
||||
{ key: '2', model: { displayName: 'error 2' }, message: 'message 2' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(queryByText('error 1')).toBeInTheDocument();
|
||||
expect(queryByText('error 2')).toBeInTheDocument();
|
||||
expect(queryByText('message 1')).toBeInTheDocument();
|
||||
expect(queryByText('message 2')).toBeInTheDocument();
|
||||
expect(queryByText(messages.proctoredSettingsLinkText.defaultMessage)).toHaveAttribute('href', 'mfe-url');
|
||||
});
|
||||
|
||||
it('renders proctoring alerts without mfe settings link', async () => {
|
||||
const { queryByText } = renderComponent({
|
||||
...pageAlertsData,
|
||||
advanceSettingsUrl: '/some-url',
|
||||
proctoringErrors: [
|
||||
{ key: '1', model: { displayName: 'error 1' }, message: 'message 1' },
|
||||
{ key: '2', model: { displayName: 'error 2' }, message: 'message 2' },
|
||||
],
|
||||
});
|
||||
|
||||
expect(queryByText('error 1')).toBeInTheDocument();
|
||||
expect(queryByText('error 2')).toBeInTheDocument();
|
||||
expect(queryByText('message 1')).toBeInTheDocument();
|
||||
expect(queryByText('message 2')).toBeInTheDocument();
|
||||
expect(queryByText(messages.advancedSettingLinkText.defaultMessage)).toHaveAttribute(
|
||||
'href',
|
||||
`${getConfig().STUDIO_BASE_URL}/some-url`,
|
||||
);
|
||||
});
|
||||
});
|
||||
62
src/course-outline/page-alerts/messages.js
Normal file
62
src/course-outline/page-alerts/messages.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
configurationErrorTitle: {
|
||||
id: 'course-authoring.course-outline.page-alerts.configurationErrorTitle',
|
||||
defaultMessage: 'This course was created as a re-run. Some manual configuration is needed.',
|
||||
},
|
||||
configurationErrorText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.configurationErrorText',
|
||||
defaultMessage: 'No course content is currently visible, and no learners are enrolled. Be sure to review and reset all dates, including the Course Start Date; set up the course team; review course updates and other assets for dated material; and seed the discussions and wiki.',
|
||||
},
|
||||
discussionNotificationText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.discussionNotificationText',
|
||||
defaultMessage: 'This course run is using an upgraded version of {platformName} discussion forum. In order to display the discussions sidebar, discussions xBlocks will no longer be visible to learners.',
|
||||
},
|
||||
discussionNotificationLearnMore: {
|
||||
id: 'course-authoring.course-outline.page-alerts.discussionNotificationLearnMore',
|
||||
defaultMessage: 'Learn more',
|
||||
},
|
||||
discussionNotificationFeedback: {
|
||||
id: 'course-authoring.course-outline.page-alerts.discussionNotificationLearnMore',
|
||||
defaultMessage: 'Share feedback',
|
||||
},
|
||||
deprecationWarningTitle: {
|
||||
id: 'course-authoring.course-outline.page-alerts.deprecationWarningTitle',
|
||||
defaultMessage: 'This course uses features that are no longer supported.',
|
||||
},
|
||||
deprecationWarningBlocksText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.deprecationWarningBlocksText',
|
||||
defaultMessage: 'You must delete or replace the following components.',
|
||||
},
|
||||
deprecationWarningDeprecatedBlockText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.deprecationWarningDeprecatedBlockText',
|
||||
defaultMessage: 'To avoid errors, {platformName} strongly recommends that you remove unsupported features from the course advanced settings. To do this, go to the {hyperlink}, locate the "Advanced Module List" setting, and then delete the following modules from the list.',
|
||||
},
|
||||
advancedSettingLinkText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.advancedSettingLinkText',
|
||||
defaultMessage: 'Advanced Settings page',
|
||||
},
|
||||
deprecatedComponentName: {
|
||||
id: 'course-authoring.course-outline.page-alerts.deprecatedComponentName',
|
||||
defaultMessage: 'Deprecated Component',
|
||||
},
|
||||
proctoringErrorTitle: {
|
||||
id: 'course-authoring.course-outline.page-alerts.proctoringErrorTitle',
|
||||
defaultMessage: 'This course has proctored exam settings that are incomplete or invalid.',
|
||||
},
|
||||
proctoringErrorText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.proctoringErrorText',
|
||||
defaultMessage: 'To update these settings go to the {hyperlink}.',
|
||||
},
|
||||
proctoredSettingsLinkText: {
|
||||
id: 'course-authoring.course-outline.page-alerts.proctoredSettingsLinkText',
|
||||
defaultMessage: 'Proctored Exam Settings page',
|
||||
},
|
||||
alertFailedGeneric: {
|
||||
id: 'course-authoring.course-outline.page-alert.generic-error.description',
|
||||
defaultMessage: 'Unable to {actionName} {type}. Please try again.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
115
src/course-outline/paste-button/PasteButton.jsx
Normal file
115
src/course-outline/paste-button/PasteButton.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Hyperlink, Icon, Button, OverlayTrigger,
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FileCopy as PasteIcon,
|
||||
Question as QuestionIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { getInitialUserClipboard } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const PasteButton = ({
|
||||
text,
|
||||
blockType,
|
||||
onClick,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const initialUserClipboard = useSelector(getInitialUserClipboard);
|
||||
const {
|
||||
content,
|
||||
sourceContextTitle,
|
||||
sourceEditUrl,
|
||||
} = initialUserClipboard || {};
|
||||
// Show button only if clipboard has content
|
||||
const showPasteButton = (
|
||||
content?.status === 'ready'
|
||||
&& content?.blockType === blockType
|
||||
);
|
||||
|
||||
const [show, setShow] = useState(false);
|
||||
const handleOnMouseEnter = () => {
|
||||
setShow(true);
|
||||
};
|
||||
const handleOnMouseLeave = () => {
|
||||
setShow(false);
|
||||
};
|
||||
const ref = useRef(null);
|
||||
|
||||
if (!showPasteButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderBlockLink = (props) => (
|
||||
<Hyperlink
|
||||
id={`${blockType}-paste-button-overlay`}
|
||||
className="d-flex bg-white p-3 text-decoration-none popup-link shadow mb-2 zindex-2"
|
||||
target="_blank"
|
||||
destination={sourceEditUrl}
|
||||
onMouseEnter={handleOnMouseEnter}
|
||||
onMouseLeave={handleOnMouseLeave}
|
||||
onFocus={handleOnMouseEnter}
|
||||
onBlur={handleOnMouseLeave}
|
||||
{...props}
|
||||
>
|
||||
<div className="text-gray mr-5 mw-xs">
|
||||
<h4>
|
||||
{content?.displayName}<br />
|
||||
<span className="micro text-gray-400">
|
||||
{content?.blockTypeDisplay}
|
||||
</span>
|
||||
</h4>
|
||||
<span className="x-small">
|
||||
{intl.formatMessage(messages.clipboardContentFromLabel)}
|
||||
<em>{sourceContextTitle}</em>
|
||||
</span>
|
||||
</div>
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
iconBefore={PasteIcon}
|
||||
block
|
||||
onClick={onClick}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
<OverlayTrigger
|
||||
key={`${blockType}-paste-button-overlay`}
|
||||
show={show}
|
||||
placement="top"
|
||||
container={ref}
|
||||
overlay={renderBlockLink}
|
||||
>
|
||||
<div
|
||||
className="float-right d-inline-flex align-items-center x-small mt-2 cursor-help"
|
||||
ref={ref}
|
||||
onMouseEnter={handleOnMouseEnter}
|
||||
onMouseLeave={handleOnMouseLeave}
|
||||
onFocus={handleOnMouseEnter}
|
||||
onBlur={handleOnMouseLeave}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||
tabIndex="0"
|
||||
>
|
||||
<Icon className="mr-1" size="sm" src={QuestionIcon} />
|
||||
{intl.formatMessage(messages.clipboardContentLabel)}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PasteButton.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
blockType: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PasteButton;
|
||||
20
src/course-outline/paste-button/PasteButton.scss
Normal file
20
src/course-outline/paste-button/PasteButton.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
// adds bottom arrow to popup link
|
||||
.popup-link {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: solid .5rem white;
|
||||
border-left: solid .5rem transparent;
|
||||
border-right: solid .5rem transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-help {
|
||||
cursor: help !important;
|
||||
}
|
||||
14
src/course-outline/paste-button/messages.js
Normal file
14
src/course-outline/paste-button/messages.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
clipboardContentFromLabel: {
|
||||
id: 'course-authoring.course-outline.paste-button.whats-in-clipboard.from-label',
|
||||
defaultMessage: 'From: ',
|
||||
},
|
||||
clipboardContentLabel: {
|
||||
id: 'course-authoring.course-outline.paste-button.whats-in-clipboard.label',
|
||||
defaultMessage: 'What\'s in my clipboard?',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ModalDialog,
|
||||
Button,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getCurrentItem } from '../data/selectors';
|
||||
|
||||
@@ -4,21 +4,23 @@ import React, {
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Badge, Button, useToggle } from '@edx/paragon';
|
||||
import { Add as IconAdd } from '@edx/paragon/icons';
|
||||
import { Bubble, Button, useToggle } from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { setCurrentItem, setCurrentSection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
|
||||
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const SectionCard = ({
|
||||
section,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
children,
|
||||
index,
|
||||
canMoveItem,
|
||||
@@ -60,7 +62,6 @@ const SectionCard = ({
|
||||
highlights,
|
||||
actions: sectionActions,
|
||||
isHeaderVisible = true,
|
||||
explanatoryMessage = '',
|
||||
} = section;
|
||||
|
||||
// re-create actions object for customizations
|
||||
@@ -121,16 +122,11 @@ const SectionCard = ({
|
||||
|
||||
const titleComponent = (
|
||||
<TitleButton
|
||||
title={displayName}
|
||||
isExpanded={isExpanded}
|
||||
onTitleClick={handleExpandContent}
|
||||
namePrefix={namePrefix}
|
||||
>
|
||||
<BaseTitleWithStatusBadge
|
||||
title={displayName}
|
||||
status=""
|
||||
namePrefix={namePrefix}
|
||||
/>
|
||||
</TitleButton>
|
||||
/>
|
||||
);
|
||||
|
||||
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
|
||||
@@ -174,18 +170,24 @@ const SectionCard = ({
|
||||
/>
|
||||
)}
|
||||
<div className="section-card__content" data-testid="section-card__content">
|
||||
{explanatoryMessage && <p className="text-secondary-400 x-small mb-1">{explanatoryMessage}</p>}
|
||||
<div className="outline-section__status">
|
||||
<div className="outline-section__status mb-1">
|
||||
<Button
|
||||
className="section-card__highlights"
|
||||
className="p-0 bg-transparent"
|
||||
data-destid="section-card-highlights-button"
|
||||
variant="tertiary"
|
||||
onClick={handleOpenHighlightsModal}
|
||||
>
|
||||
<Badge className="highlights-badge">{highlights.length}</Badge>
|
||||
<Bubble className="mr-1">
|
||||
{highlights.length}
|
||||
</Bubble>
|
||||
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
|
||||
</Button>
|
||||
</div>
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={section}
|
||||
/>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div
|
||||
@@ -226,7 +228,6 @@ SectionCard.propTypes = {
|
||||
visibilityState: PropTypes.string.isRequired,
|
||||
highlights: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
shouldScroll: PropTypes.bool,
|
||||
explanatoryMessage: PropTypes.string,
|
||||
actions: PropTypes.shape({
|
||||
deletable: PropTypes.bool.isRequired,
|
||||
draggable: PropTypes.bool.isRequired,
|
||||
@@ -235,6 +236,8 @@ SectionCard.propTypes = {
|
||||
}).isRequired,
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
}).isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
onOpenHighlightsModal: PropTypes.func.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
|
||||
@@ -13,26 +13,7 @@
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
.section-card__highlights {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.highlights-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 1.375rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.section-card__content {
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useContext } from 'react';
|
||||
import moment from 'moment/moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Hyperlink, SelectMenu, MenuItem, Stack,
|
||||
} from '@edx/paragon';
|
||||
Button, Hyperlink, Form, Stack,
|
||||
} from '@openedx/paragon';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
@@ -37,6 +38,7 @@ const StatusBar = ({
|
||||
totalCourseBestPracticesChecks,
|
||||
} = checklist;
|
||||
|
||||
const courseReleaseDateObj = moment.utc(courseReleaseDate, 'MMM DD, YYYY at HH:mm UTC', true);
|
||||
const checkListTitle = `${completedCourseLaunchChecks + completedCourseBestPracticesChecks}/${totalCourseLaunchChecks + totalCourseBestPracticesChecks}`;
|
||||
const checklistDestination = () => new URL(`checklists/${courseId}`, config.STUDIO_BASE_URL).href;
|
||||
const scheduleDestination = () => new URL(`settings/details/${courseId}#schedule`, config.STUDIO_BASE_URL).href;
|
||||
@@ -52,18 +54,27 @@ const StatusBar = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="horizontal" gap={3.5} className="outline-status-bar" data-testid="outline-status-bar">
|
||||
<div className="outline-status-bar__item">
|
||||
<Stack direction="horizontal" gap={3.5} className="d-flex align-items-stretch outline-status-bar" data-testid="outline-status-bar">
|
||||
<div className="d-flex flex-column justify-content-between">
|
||||
<h5>{intl.formatMessage(messages.startDateTitle)}</h5>
|
||||
<Hyperlink
|
||||
className="small"
|
||||
destination={scheduleDestination()}
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{courseReleaseDate}
|
||||
{courseReleaseDateObj.isValid() ? (
|
||||
<FormattedDate
|
||||
value={courseReleaseDateObj}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="2-digit"
|
||||
hour="numeric"
|
||||
minute="numeric"
|
||||
/>
|
||||
) : courseReleaseDate}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<div className="outline-status-bar__item">
|
||||
<div className="d-flex flex-column justify-content-between">
|
||||
<h5>{intl.formatMessage(messages.pacingTypeTitle)}</h5>
|
||||
<span className="small">
|
||||
{isSelfPaced
|
||||
@@ -71,7 +82,7 @@ const StatusBar = ({
|
||||
: intl.formatMessage(messages.pacingTypeInstructorPaced)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="outline-status-bar__item mr-4">
|
||||
<div className="d-flex flex-column justify-content-between">
|
||||
<h5>{intl.formatMessage(messages.checklistTitle)}</h5>
|
||||
<Hyperlink
|
||||
className="small"
|
||||
@@ -81,9 +92,9 @@ const StatusBar = ({
|
||||
{checkListTitle} {intl.formatMessage(messages.checklistCompleted)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<div className="outline-status-bar__item ml-4">
|
||||
<div className="d-flex flex-column justify-content-between">
|
||||
<h5>{intl.formatMessage(messages.highlightEmailsTitle)}</h5>
|
||||
<div className="d-flex align-items-end">
|
||||
<div className="d-flex align-items-center">
|
||||
{highlightsEnabledForMessaging ? (
|
||||
<span data-testid="highlights-enabled-span" className="small">
|
||||
{intl.formatMessage(messages.highlightEmailsEnabled)}
|
||||
@@ -104,26 +115,31 @@ const StatusBar = ({
|
||||
</div>
|
||||
</div>
|
||||
{videoSharingEnabled && (
|
||||
<div
|
||||
data-testid="video-sharing-wrapper"
|
||||
className="outline-status-bar__item ml-2"
|
||||
<Form.Group
|
||||
size="sm"
|
||||
className="d-flex flex-column justify-content-between m-0"
|
||||
>
|
||||
<h5>{intl.formatMessage(messages.videoSharingTitle)}</h5>
|
||||
<div className="d-flex align-items-end">
|
||||
<SelectMenu variant="sm btn-outline-primary">
|
||||
<Form.Label
|
||||
className="h5"
|
||||
>{intl.formatMessage(messages.videoSharingTitle)}
|
||||
</Form.Label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={videoSharingOptions}
|
||||
onChange={(e) => handleVideoSharingOptionChange(e.target.value)}
|
||||
>
|
||||
{Object.values(VIDEO_SHARING_OPTIONS).map((option) => (
|
||||
<MenuItem
|
||||
<option
|
||||
key={option}
|
||||
value={option}
|
||||
defaultSelected={option === videoSharingOptions}
|
||||
onClick={() => handleVideoSharingOptionChange(option)}
|
||||
>
|
||||
{getVideoSharingOptionText(option, messages, intl)}
|
||||
</MenuItem>
|
||||
</option>
|
||||
))}
|
||||
</SelectMenu>
|
||||
</Form.Control>
|
||||
<Hyperlink
|
||||
className="small ml-2"
|
||||
className="small"
|
||||
destination={socialSharingUrl}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
@@ -131,7 +147,8 @@ const StatusBar = ({
|
||||
{intl.formatMessage(messages.videoSharingLink)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -74,10 +74,10 @@ describe('<StatusBar />', () => {
|
||||
});
|
||||
|
||||
it('renders StatusBar component correctly', () => {
|
||||
const { queryByTestId, getByText } = renderComponent();
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
expect(getByText(messages.startDateTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(statusBarData.courseReleaseDate)).toBeInTheDocument();
|
||||
expect(getByText('Feb 05, 2013, 5:00 AM')).toBeInTheDocument();
|
||||
|
||||
expect(getByText(messages.pacingTypeTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.pacingTypeSelfPaced.defaultMessage)).toBeInTheDocument();
|
||||
@@ -88,7 +88,7 @@ describe('<StatusBar />', () => {
|
||||
expect(getByText(messages.highlightEmailsTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.highlightEmailsEnabled.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(queryByTestId('video-sharing-wrapper')).toBeInTheDocument();
|
||||
expect(getByText(messages.videoSharingTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders StatusBar when isSelfPaced is false', () => {
|
||||
|
||||
@@ -3,22 +3,26 @@ import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, useToggle } from '@edx/paragon';
|
||||
import { Add as IconAdd } from '@edx/paragon/icons';
|
||||
import { Button, useToggle } from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
|
||||
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import PasteButton from '../paste-button/PasteButton';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const SubsectionCard = ({
|
||||
section,
|
||||
subsection,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
children,
|
||||
index,
|
||||
canMoveItem,
|
||||
@@ -30,6 +34,7 @@ const SubsectionCard = ({
|
||||
onNewUnitSubmit,
|
||||
onOrderChange,
|
||||
onOpenConfigureModal,
|
||||
onPasteClick,
|
||||
}) => {
|
||||
const currentRef = useRef(null);
|
||||
const intl = useIntl();
|
||||
@@ -48,6 +53,8 @@ const SubsectionCard = ({
|
||||
visibilityState,
|
||||
actions: subsectionActions,
|
||||
isHeaderVisible = true,
|
||||
enableCopyPasteUnits = false,
|
||||
proctoringExamConfigurationLink,
|
||||
} = subsection;
|
||||
|
||||
// re-create actions object for customizations
|
||||
@@ -92,19 +99,15 @@ const SubsectionCard = ({
|
||||
};
|
||||
|
||||
const handleNewButtonClick = () => onNewUnitSubmit(id);
|
||||
const handlePasteButtonClick = () => onPasteClick(id, section.id);
|
||||
|
||||
const titleComponent = (
|
||||
<TitleButton
|
||||
title={displayName}
|
||||
isExpanded={isExpanded}
|
||||
onTitleClick={handleExpandContent}
|
||||
namePrefix={namePrefix}
|
||||
>
|
||||
<BaseTitleWithStatusBadge
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
namePrefix={namePrefix}
|
||||
/>
|
||||
</TitleButton>
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -140,26 +143,37 @@ const SubsectionCard = ({
|
||||
>
|
||||
<div className="subsection-card" data-testid="subsection-card" ref={currentRef}>
|
||||
{isHeaderVisible && (
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
hasChanges={hasChanges}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
/>
|
||||
<>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
hasChanges={hasChanges}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
|
||||
isSequential
|
||||
/>
|
||||
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={subsection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<div
|
||||
@@ -168,16 +182,25 @@ const SubsectionCard = ({
|
||||
>
|
||||
{children}
|
||||
{actions.childAddable && (
|
||||
<Button
|
||||
data-testid="new-unit-button"
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
iconBefore={IconAdd}
|
||||
block
|
||||
onClick={handleNewButtonClick}
|
||||
>
|
||||
{intl.formatMessage(messages.newUnitButton)}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
data-testid="new-unit-button"
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
iconBefore={IconAdd}
|
||||
block
|
||||
onClick={handleNewButtonClick}
|
||||
>
|
||||
{intl.formatMessage(messages.newUnitButton)}
|
||||
</Button>
|
||||
{enableCopyPasteUnits && (
|
||||
<PasteButton
|
||||
text={intl.formatMessage(messages.pasteButton)}
|
||||
blockType={COURSE_BLOCK_NAMES.vertical.id}
|
||||
onClick={handlePasteButtonClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -206,6 +229,8 @@ SubsectionCard.propTypes = {
|
||||
hasChanges: PropTypes.bool.isRequired,
|
||||
visibilityState: PropTypes.string.isRequired,
|
||||
shouldScroll: PropTypes.bool,
|
||||
enableCopyPasteUnits: PropTypes.bool,
|
||||
proctoringExamConfigurationLink: PropTypes.string,
|
||||
actions: PropTypes.shape({
|
||||
deletable: PropTypes.bool.isRequired,
|
||||
draggable: PropTypes.bool.isRequired,
|
||||
@@ -215,6 +240,8 @@ SubsectionCard.propTypes = {
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
}).isRequired,
|
||||
children: PropTypes.node,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
onEditSubmit: PropTypes.func.isRequired,
|
||||
savingStatus: PropTypes.string.isRequired,
|
||||
@@ -225,6 +252,7 @@ SubsectionCard.propTypes = {
|
||||
canMoveItem: PropTypes.func.isRequired,
|
||||
onOrderChange: PropTypes.func.isRequired,
|
||||
onOpenConfigureModal: PropTypes.func.isRequired,
|
||||
onPasteClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SubsectionCard;
|
||||
|
||||
@@ -5,10 +5,6 @@
|
||||
margin-top: $spacer;
|
||||
}
|
||||
|
||||
.item-card-header__badge-status {
|
||||
background: $light-100;
|
||||
}
|
||||
|
||||
.subsection-card-title {
|
||||
font-size: $h4-font-size;
|
||||
font-family: $headings-font-family;
|
||||
@@ -16,4 +12,8 @@
|
||||
line-height: $headings-line-height;
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
.subsection-card__content {
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.subsection.button.new-unit',
|
||||
defaultMessage: 'New unit',
|
||||
},
|
||||
pasteButton: {
|
||||
id: 'course-authoring.course-outline.subsection.button.new-unit',
|
||||
defaultMessage: 'Paste unit',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useToggle } from '@edx/paragon';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
|
||||
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
|
||||
import TitleLink from '../card-header/TitleLink';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
|
||||
const UnitCard = ({
|
||||
unit,
|
||||
subsection,
|
||||
section,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
index,
|
||||
canMoveItem,
|
||||
onOpenPublishModal,
|
||||
@@ -25,6 +27,8 @@ const UnitCard = ({
|
||||
onDuplicateSubmit,
|
||||
getTitleLink,
|
||||
onOrderChange,
|
||||
onCopyToClipboardClick,
|
||||
discussionsSettings,
|
||||
}) => {
|
||||
const currentRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
@@ -39,6 +43,8 @@ const UnitCard = ({
|
||||
visibilityState,
|
||||
actions: unitActions,
|
||||
isHeaderVisible = true,
|
||||
enableCopyPasteUnits = false,
|
||||
discussionEnabled,
|
||||
} = unit;
|
||||
|
||||
// re-create actions object for customizations
|
||||
@@ -47,6 +53,11 @@ const UnitCard = ({
|
||||
actions.allowMoveUp = canMoveItem(index, -1);
|
||||
actions.allowMoveDown = canMoveItem(index, 1);
|
||||
|
||||
const parentInfo = {
|
||||
graded: subsection.graded,
|
||||
isTimeLimited: subsection.isTimeLimited,
|
||||
};
|
||||
|
||||
const unitStatus = getItemStatus({
|
||||
published,
|
||||
visibilityState,
|
||||
@@ -77,17 +88,16 @@ const UnitCard = ({
|
||||
onOrderChange(index, index + 1);
|
||||
};
|
||||
|
||||
const handleCopyClick = () => {
|
||||
onCopyToClipboardClick(unit.id);
|
||||
};
|
||||
|
||||
const titleComponent = (
|
||||
<TitleLink
|
||||
title={displayName}
|
||||
titleLink={getTitleLink(id)}
|
||||
namePrefix={namePrefix}
|
||||
>
|
||||
<BaseTitleWithStatusBadge
|
||||
title={displayName}
|
||||
status={unitStatus}
|
||||
namePrefix={namePrefix}
|
||||
/>
|
||||
</TitleLink>
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -145,12 +155,29 @@ const UnitCard = ({
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
isVertical
|
||||
enableCopyPasteUnits={enableCopyPasteUnits}
|
||||
onClickCopy={handleCopyClick}
|
||||
discussionEnabled={discussionEnabled}
|
||||
discussionsSettings={discussionsSettings}
|
||||
parentInfo={parentInfo}
|
||||
/>
|
||||
<div className="unit-card__content item-children" data-testid="unit-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={unit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalSortableElement>
|
||||
);
|
||||
};
|
||||
|
||||
UnitCard.defaultProps = {
|
||||
discussionsSettings: {},
|
||||
};
|
||||
|
||||
UnitCard.propTypes = {
|
||||
unit: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -166,6 +193,8 @@ UnitCard.propTypes = {
|
||||
duplicable: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
enableCopyPasteUnits: PropTypes.bool,
|
||||
discussionEnabled: PropTypes.bool,
|
||||
}).isRequired,
|
||||
subsection: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -174,6 +203,8 @@ UnitCard.propTypes = {
|
||||
hasChanges: PropTypes.bool.isRequired,
|
||||
visibilityState: PropTypes.string.isRequired,
|
||||
shouldScroll: PropTypes.bool,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
}).isRequired,
|
||||
section: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -193,6 +224,13 @@ UnitCard.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
canMoveItem: PropTypes.func.isRequired,
|
||||
onOrderChange: PropTypes.func.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
onCopyToClipboardClick: PropTypes.func.isRequired,
|
||||
discussionsSettings: PropTypes.shape({
|
||||
providerType: PropTypes.string,
|
||||
enableGradedUnits: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default UnitCard;
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
.unit-card {
|
||||
flex-grow: 1;
|
||||
|
||||
.unit-card__content {
|
||||
margin: $spacer;
|
||||
}
|
||||
|
||||
.item-card-header__badge-status {
|
||||
background: $light-100;
|
||||
}
|
||||
|
||||
// used in src/course-outline/card-header/TitleLink.jsx &
|
||||
// src/course-outline/card-header/TitleButton.jsx as
|
||||
// `${namePrefix}-card-title`
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import UnitCard from './UnitCard';
|
||||
import cardMessages from '../card-header/messages';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let axiosMock;
|
||||
@@ -119,4 +120,17 @@ describe('<UnitCard />', () => {
|
||||
expect(within(element).queryByTestId('unit-card-header__menu-duplicate-button')).not.toBeInTheDocument();
|
||||
expect(within(element).queryByTestId('unit-card-header__menu-delete-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows copy option based on enableCopyPasteUnits flag', async () => {
|
||||
const { findByTestId } = renderComponent({
|
||||
unit: {
|
||||
...unit,
|
||||
enableCopyPasteUnits: true,
|
||||
},
|
||||
});
|
||||
const element = await findByTestId('unit-card');
|
||||
const menu = await within(element).findByTestId('unit-card-header__menu-button');
|
||||
await act(async () => fireEvent.click(menu));
|
||||
expect(within(element).queryByText(cardMessages.menuCopy.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Lock as LockIcon,
|
||||
EditOutline as EditOutlineIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { ITEM_BADGE_STATUS, VIDEO_SHARING_OPTIONS } from './constants';
|
||||
import { VisibilityTypes } from '../data/constants';
|
||||
@@ -21,6 +21,8 @@ const getItemStatus = ({
|
||||
switch (true) {
|
||||
case visibilityState === VisibilityTypes.STAFF_ONLY:
|
||||
return ITEM_BADGE_STATUS.staffOnly;
|
||||
case visibilityState === VisibilityTypes.GATED:
|
||||
return ITEM_BADGE_STATUS.gated;
|
||||
case visibilityState === VisibilityTypes.LIVE:
|
||||
return ITEM_BADGE_STATUS.live;
|
||||
case published && !hasChanges:
|
||||
@@ -42,6 +44,11 @@ const getItemStatus = ({
|
||||
*/
|
||||
const getItemStatusBadgeContent = (status, messages, intl) => {
|
||||
switch (status) {
|
||||
case ITEM_BADGE_STATUS.gated:
|
||||
return {
|
||||
badgeTitle: intl.formatMessage(messages.statusBadgeGated),
|
||||
badgeIcon: LockIcon,
|
||||
};
|
||||
case ITEM_BADGE_STATUS.live:
|
||||
return {
|
||||
badgeTitle: intl.formatMessage(messages.statusBadgeLive),
|
||||
@@ -92,6 +99,10 @@ const getItemStatusBorder = (status) => {
|
||||
return {
|
||||
borderLeft: '5px solid #0D7D4D',
|
||||
};
|
||||
case ITEM_BADGE_STATUS.gated:
|
||||
return {
|
||||
borderLeft: '5px solid #000000',
|
||||
};
|
||||
case ITEM_BADGE_STATUS.staffOnly:
|
||||
return {
|
||||
borderLeft: '5px solid #000000',
|
||||
|
||||
48
src/course-outline/xblock-status/GradingPolicyAlert.jsx
Normal file
48
src/course-outline/xblock-status/GradingPolicyAlert.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import {
|
||||
WarningFilled as WarningIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const GradingPolicyAlert = ({
|
||||
graded,
|
||||
gradingType,
|
||||
courseGraders,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
let gradingPolicyMismatch = false;
|
||||
if (graded) {
|
||||
if (gradingType) {
|
||||
gradingPolicyMismatch = (
|
||||
courseGraders.filter((cg) => cg.toLowerCase() === gradingType.toLowerCase())
|
||||
).length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (gradingPolicyMismatch) {
|
||||
return (
|
||||
<Alert className="mt-2 grading-mismatch-alert" variant="warning" icon={WarningIcon}>
|
||||
{intl.formatMessage(messages.gradingPolicyMismatchText, { gradingType })}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
GradingPolicyAlert.defaultProps = {
|
||||
graded: false,
|
||||
gradingType: '',
|
||||
};
|
||||
|
||||
GradingPolicyAlert.propTypes = {
|
||||
graded: PropTypes.bool,
|
||||
gradingType: PropTypes.string,
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
};
|
||||
|
||||
export default GradingPolicyAlert;
|
||||
134
src/course-outline/xblock-status/GradingTypeAndDueDate.jsx
Normal file
134
src/course-outline/xblock-status/GradingTypeAndDueDate.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import {
|
||||
Check as CheckIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const GradingTypeAndDueDate = ({
|
||||
isSelfPaced,
|
||||
isInstructorPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
graded,
|
||||
gradingType,
|
||||
dueDate,
|
||||
relativeWeeksDue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const showRelativeWeeks = isSelfPaced && isCustomRelativeDatesActive && relativeWeeksDue;
|
||||
|
||||
let examValue = '';
|
||||
if (isProctoredExam) {
|
||||
if (isOnboardingExam) {
|
||||
examValue = messages.onboardingExam;
|
||||
} else if (isPracticeExam) {
|
||||
examValue = messages.practiceProctoredExam;
|
||||
} else {
|
||||
examValue = messages.proctoredExam;
|
||||
}
|
||||
} else {
|
||||
examValue = messages.timedExam;
|
||||
}
|
||||
|
||||
const gradingTypeDiv = () => (
|
||||
<div className="d-flex align-items-center mr-1" data-testid="grading-type-div">
|
||||
<span className="sr-only status-grading-label">
|
||||
{intl.formatMessage(messages.gradedAsScreenReaderLabel)}
|
||||
</span>
|
||||
<Icon className="mr-1" size="sm" src={CheckIcon} />
|
||||
<span className="status-grading-value">
|
||||
{gradingType || intl.formatMessage(messages.ungradedText)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const dueDateDiv = () => {
|
||||
if (dueDate && isInstructorPaced) {
|
||||
return (
|
||||
<div className="status-grading-date" data-testid="due-date-div">
|
||||
{intl.formatMessage(messages.dueLabel)} {dueDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const selfPacedRelativeDueWeeksDiv = () => (
|
||||
<div className="d-flex align-items-center" data-testid="self-paced-relative-due-weeks-div">
|
||||
<Icon className="mr-1" size="sm" src={CalendarIcon} />
|
||||
<span className="status-custom-grading-date">
|
||||
{intl.formatMessage(messages.customDueDateLabel, { relativeWeeksDue })}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isTimeLimited) {
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center">
|
||||
{gradingTypeDiv()} -
|
||||
<span className="sr-only">{intl.formatMessage(examValue)}</span>
|
||||
<span className="mx-2" data-testid="exam-value-span">
|
||||
{intl.formatMessage(examValue)}
|
||||
</span>
|
||||
{dueDateDiv()}
|
||||
</div>
|
||||
{showRelativeWeeks && (selfPacedRelativeDueWeeksDiv())}
|
||||
</>
|
||||
);
|
||||
} if ((dueDate && !isSelfPaced) || graded) {
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center">
|
||||
{gradingTypeDiv()}
|
||||
{dueDateDiv()}
|
||||
</div>
|
||||
{showRelativeWeeks && (selfPacedRelativeDueWeeksDiv())}
|
||||
</>
|
||||
);
|
||||
} if (showRelativeWeeks) {
|
||||
return (
|
||||
<>
|
||||
{gradingTypeDiv()}
|
||||
{selfPacedRelativeDueWeeksDiv()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
GradingTypeAndDueDate.defaultProps = {
|
||||
isCustomRelativeDatesActive: false,
|
||||
isTimeLimited: false,
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
graded: false,
|
||||
gradingType: '',
|
||||
dueDate: '',
|
||||
relativeWeeksDue: null,
|
||||
};
|
||||
|
||||
GradingTypeAndDueDate.propTypes = {
|
||||
isInstructorPaced: PropTypes.bool.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
gradingType: PropTypes.string,
|
||||
dueDate: PropTypes.string,
|
||||
relativeWeeksDue: PropTypes.number,
|
||||
};
|
||||
|
||||
export default GradingTypeAndDueDate;
|
||||
29
src/course-outline/xblock-status/HideAfterDueMessage.jsx
Normal file
29
src/course-outline/xblock-status/HideAfterDueMessage.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import {
|
||||
VisibilityOff as HideIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const HideAfterDueMessage = ({ isSelfPaced }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="d-flex align-items-center" data-testid="hide-after-due-message">
|
||||
<Icon className="mr-1" size="sm" src={HideIcon} />
|
||||
<span className="status-hide-after-due-value">
|
||||
{isSelfPaced
|
||||
? intl.formatMessage(messages.hiddenAfterEndDate)
|
||||
: intl.formatMessage(messages.hiddenAfterDueDate)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HideAfterDueMessage.propTypes = {
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default HideAfterDueMessage;
|
||||
65
src/course-outline/xblock-status/ReleaseStatus.jsx
Normal file
65
src/course-outline/xblock-status/ReleaseStatus.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import {
|
||||
AccessTime as ClockIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ReleaseStatus = ({
|
||||
isInstructorPaced,
|
||||
explanatoryMessage,
|
||||
releaseDate,
|
||||
releasedToStudents,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const explanatoryMessageDiv = () => (
|
||||
<span data-testid="explanatory-message-span">
|
||||
{explanatoryMessage}
|
||||
</span>
|
||||
);
|
||||
|
||||
let releaseLabel = messages.unscheduledLabel;
|
||||
if (releasedToStudents) {
|
||||
releaseLabel = messages.releasedLabel;
|
||||
} else if (releaseDate) {
|
||||
releaseLabel = messages.scheduledLabel;
|
||||
}
|
||||
|
||||
const releaseStatusDiv = () => (
|
||||
<div className="d-flex align-items-center" data-testid="release-status-div">
|
||||
<span className="sr-only status-release-label">
|
||||
{intl.formatMessage(messages.releaseStatusScreenReaderTitle)}
|
||||
</span>
|
||||
<Icon className="mr-1" size="sm" src={ClockIcon} />
|
||||
{intl.formatMessage(releaseLabel)}
|
||||
{releaseDate && releaseDate}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (explanatoryMessage) {
|
||||
return explanatoryMessageDiv();
|
||||
}
|
||||
|
||||
if (isInstructorPaced) {
|
||||
return releaseStatusDiv();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ReleaseStatus.defaultProps = {
|
||||
explanatoryMessage: '',
|
||||
};
|
||||
|
||||
ReleaseStatus.propTypes = {
|
||||
isInstructorPaced: PropTypes.bool.isRequired,
|
||||
explanatoryMessage: PropTypes.string,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default ReleaseStatus;
|
||||
88
src/course-outline/xblock-status/StatusMessages.jsx
Normal file
88
src/course-outline/xblock-status/StatusMessages.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import {
|
||||
Lock as LockIcon,
|
||||
Groups as GroupsIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const StatusMessages = ({
|
||||
isVertical,
|
||||
staffOnlyMessage,
|
||||
prereq,
|
||||
prereqs,
|
||||
userPartitionInfo,
|
||||
hasPartitionGroupComponents,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const statusMessages = [];
|
||||
|
||||
if (prereq) {
|
||||
let prereqDisplayName = '';
|
||||
prereqs.forEach((block) => {
|
||||
if (block.blockUsageKey === prereq) {
|
||||
prereqDisplayName = block.blockDisplayName;
|
||||
}
|
||||
});
|
||||
statusMessages.push({
|
||||
icon: LockIcon,
|
||||
text: intl.formatMessage(messages.prerequisiteLabel, { prereqDisplayName }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!staffOnlyMessage && isVertical) {
|
||||
const { selectedPartitionIndex, selectedGroupsLabel } = userPartitionInfo;
|
||||
if (selectedPartitionIndex !== -1 && !Number.isNaN(selectedPartitionIndex)) {
|
||||
statusMessages.push({
|
||||
icon: GroupsIcon,
|
||||
text: intl.formatMessage(messages.restrictedUnitAccess, { selectedGroupsLabel }),
|
||||
});
|
||||
} else if (hasPartitionGroupComponents) {
|
||||
statusMessages.push({
|
||||
icon: GroupsIcon,
|
||||
text: intl.formatMessage(messages.restrictedUnitAccessToSomeContent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (statusMessages.length > 0) {
|
||||
return (
|
||||
<div className="border-top border-light mt-2 text-dark" data-testid="status-messages-div">
|
||||
{statusMessages.map(({ icon, text }) => (
|
||||
<div key={text} className="d-flex align-items-center pt-1">
|
||||
<Icon className="mr-1" size="sm" src={icon} />
|
||||
{text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
StatusMessages.defaultProps = {
|
||||
staffOnlyMessage: false,
|
||||
prereq: '',
|
||||
prereqs: [],
|
||||
userPartitionInfo: {},
|
||||
};
|
||||
|
||||
StatusMessages.propTypes = {
|
||||
isVertical: PropTypes.bool.isRequired,
|
||||
staffOnlyMessage: PropTypes.bool,
|
||||
prereq: PropTypes.string,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
selectedGroupsLabel: PropTypes.string.isRequired,
|
||||
}),
|
||||
hasPartitionGroupComponents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default StatusMessages;
|
||||
122
src/course-outline/xblock-status/XBlockStatus.jsx
Normal file
122
src/course-outline/xblock-status/XBlockStatus.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
import ReleaseStatus from './ReleaseStatus';
|
||||
import GradingPolicyAlert from './GradingPolicyAlert';
|
||||
import GradingTypeAndDueDate from './GradingTypeAndDueDate';
|
||||
import StatusMessages from './StatusMessages';
|
||||
import HideAfterDueMessage from './HideAfterDueMessage';
|
||||
|
||||
const XBlockStatus = ({
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
blockData,
|
||||
}) => {
|
||||
const {
|
||||
category,
|
||||
explanatoryMessage,
|
||||
releasedToStudents,
|
||||
releaseDate,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
prereq,
|
||||
prereqs,
|
||||
staffOnlyMessage,
|
||||
userPartitionInfo,
|
||||
hasPartitionGroupComponents,
|
||||
format: gradingType,
|
||||
dueDate,
|
||||
relativeWeeksDue,
|
||||
isTimeLimited,
|
||||
graded,
|
||||
courseGraders,
|
||||
hideAfterDue,
|
||||
} = blockData;
|
||||
|
||||
const isInstructorPaced = !isSelfPaced;
|
||||
const isVertical = category === COURSE_BLOCK_NAMES.vertical.id;
|
||||
|
||||
return (
|
||||
<div className="text-secondary-400 x-small mb-1">
|
||||
{!isVertical && (
|
||||
<ReleaseStatus
|
||||
isInstructorPaced={isInstructorPaced}
|
||||
explanatoryMessage={explanatoryMessage}
|
||||
releaseDate={releaseDate}
|
||||
releasedToStudents={releasedToStudents}
|
||||
/>
|
||||
)}
|
||||
{!isVertical && (
|
||||
<GradingTypeAndDueDate
|
||||
isSelfPaced={isSelfPaced}
|
||||
isInstructorPaced={isInstructorPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
isTimeLimited={isTimeLimited}
|
||||
isProctoredExam={isProctoredExam}
|
||||
isOnboardingExam={isOnboardingExam}
|
||||
isPracticeExam={isPracticeExam}
|
||||
graded={graded}
|
||||
gradingType={gradingType}
|
||||
dueDate={dueDate}
|
||||
relativeWeeksDue={relativeWeeksDue}
|
||||
/>
|
||||
)}
|
||||
{hideAfterDue && (
|
||||
<HideAfterDueMessage isSelfPaced={isSelfPaced} />
|
||||
)}
|
||||
<StatusMessages
|
||||
isVertical={isVertical}
|
||||
staffOnlyMessage={staffOnlyMessage}
|
||||
prereq={prereq}
|
||||
prereqs={prereqs}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
hasPartitionGroupComponents={hasPartitionGroupComponents}
|
||||
/>
|
||||
<GradingPolicyAlert
|
||||
graded={graded}
|
||||
gradingType={gradingType}
|
||||
courseGraders={courseGraders}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
XBlockStatus.defaultProps = {
|
||||
isCustomRelativeDatesActive: false,
|
||||
};
|
||||
|
||||
XBlockStatus.propTypes = {
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool,
|
||||
blockData: PropTypes.shape({
|
||||
category: PropTypes.string.isRequired,
|
||||
explanatoryMessage: PropTypes.string,
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
prereq: PropTypes.string,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
staffOnlyMessage: PropTypes.bool,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
selectedGroupsLabel: PropTypes.string.isRequired,
|
||||
}),
|
||||
hasPartitionGroupComponents: PropTypes.bool.isRequired,
|
||||
format: PropTypes.string,
|
||||
dueDate: PropTypes.string,
|
||||
relativeWeeksDue: PropTypes.number,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
hideAfterDue: PropTypes.bool,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default XBlockStatus;
|
||||
4
src/course-outline/xblock-status/XBlockStatus.scss
Normal file
4
src/course-outline/xblock-status/XBlockStatus.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.grading-mismatch-alert {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
503
src/course-outline/xblock-status/XBlockStatus.test.jsx
Normal file
503
src/course-outline/xblock-status/XBlockStatus.test.jsx
Normal file
@@ -0,0 +1,503 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import XBlockStatus from './XBlockStatus';
|
||||
import messages from './messages';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage: (message) => message.defaultMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
const section = {
|
||||
id: '123',
|
||||
displayName: 'Section Name',
|
||||
published: true,
|
||||
visibilityState: 'live',
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
category: 'chapter',
|
||||
explanatoryMessage: '',
|
||||
releasedToStudents: true,
|
||||
releaseDate: 'Feb 05, 2013 at 01:00 UTC',
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
staffOnlyMessage: false,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
hasPartitionGroupComponents: false,
|
||||
format: 'Homework',
|
||||
dueDate: 'Dec 28, 2023 at 22:00 UTC',
|
||||
isTimeLimited: true,
|
||||
graded: true,
|
||||
courseGraders: ['Homework'],
|
||||
hideAfterDue: true,
|
||||
};
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<XBlockStatus
|
||||
isSelfPaced={false}
|
||||
isCustomRelativeDatesActive={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<XBlockStatus /> for Instructor paced Section', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('render XBlockStatus with explanatoryMessage', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...section,
|
||||
explanatoryMessage: 'some explanatory message',
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).toBeInTheDocument();
|
||||
// when explanatory message is displayed, release date should not be visible
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with release status, grading type, due date etc.', () => {
|
||||
const { queryByTestId } = renderComponent({ blockData: section });
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
// when explanatory message is not displayed, release date should be visible
|
||||
const releaseStatusDiv = queryByTestId('release-status-div');
|
||||
expect(releaseStatusDiv).toBeInTheDocument();
|
||||
expect(releaseStatusDiv).toHaveTextContent(
|
||||
`${messages.releasedLabel.defaultMessage}${section.releaseDate}`,
|
||||
);
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(section.format);
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.timedExam.defaultMessage);
|
||||
// check due date div
|
||||
const dueDateDiv = queryByTestId('due-date-div');
|
||||
expect(dueDateDiv).toBeInTheDocument();
|
||||
expect(dueDateDiv).toHaveTextContent(
|
||||
`${messages.dueLabel.defaultMessage} ${section.dueDate}`,
|
||||
);
|
||||
// self paced weeks should not be visible as
|
||||
// isSelfPaced is false as well as isCustomRelativeDatesActive is false
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterDueDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<XBlockStatus /> for self paced Section', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with grading type, due weeks etc.', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
isSelfPaced: true,
|
||||
isCustomRelativeDatesActive: true,
|
||||
blockData: {
|
||||
...section,
|
||||
relativeWeeksDue: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// both explanatoryMessage & releaseStatusDiv should not be visible
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(section.format);
|
||||
// due date should not be visible for self paced courses.
|
||||
expect(queryByTestId('due-date-div')).not.toBeInTheDocument();
|
||||
// check selfPacedRelativeDueWeeksDiv
|
||||
const selfPacedRelativeDueWeeksDiv = queryByTestId('self-paced-relative-due-weeks-div');
|
||||
expect(selfPacedRelativeDueWeeksDiv).toBeInTheDocument();
|
||||
expect(selfPacedRelativeDueWeeksDiv).toHaveTextContent(
|
||||
messages.customDueDateLabel.defaultMessage,
|
||||
);
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterEndDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
expect(queryByTestId('status-messages-div')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with grading mismatch alert', () => {
|
||||
const { queryByText } = renderComponent({
|
||||
blockData: {
|
||||
...section,
|
||||
format: 'Fun',
|
||||
},
|
||||
});
|
||||
|
||||
// check alert
|
||||
const alert = queryByText(messages.gradingPolicyMismatchText.defaultMessage);
|
||||
expect(alert).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const subsection = {
|
||||
id: '123',
|
||||
displayName: 'Subsection Name',
|
||||
published: true,
|
||||
visibilityState: 'live',
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
category: 'sequential',
|
||||
explanatoryMessage: '',
|
||||
releasedToStudents: false,
|
||||
releaseDate: 'Feb 05, 2025 at 01:00 UTC',
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
prereq: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc0',
|
||||
prereqs: [
|
||||
{
|
||||
blockDisplayName: 'Find your study buddy',
|
||||
blockUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc0',
|
||||
},
|
||||
{
|
||||
blockDisplayName: 'Something else',
|
||||
blockUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@sdafyrb',
|
||||
},
|
||||
],
|
||||
staffOnlyMessage: false,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
hasPartitionGroupComponents: false,
|
||||
format: 'Homework',
|
||||
dueDate: 'Dec 28, 2023 at 22:00 UTC',
|
||||
isTimeLimited: true,
|
||||
graded: true,
|
||||
courseGraders: ['Homework'],
|
||||
hideAfterDue: true,
|
||||
};
|
||||
|
||||
describe('<XBlockStatus /> for Instructor paced Subsection', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with release status, grading type, due date etc.', () => {
|
||||
const { queryByTestId } = renderComponent({ blockData: subsection });
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
// when explanatory message is not displayed, release date should be visible
|
||||
const releaseStatusDiv = queryByTestId('release-status-div');
|
||||
expect(releaseStatusDiv).toBeInTheDocument();
|
||||
expect(releaseStatusDiv).toHaveTextContent(
|
||||
`${messages.scheduledLabel.defaultMessage}${subsection.releaseDate}`,
|
||||
);
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(subsection.format);
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.timedExam.defaultMessage);
|
||||
// check due date div
|
||||
const dueDateDiv = queryByTestId('due-date-div');
|
||||
expect(dueDateDiv).toBeInTheDocument();
|
||||
expect(dueDateDiv).toHaveTextContent(
|
||||
`${messages.dueLabel.defaultMessage} ${subsection.dueDate}`,
|
||||
);
|
||||
// self paced weeks should not be visible as
|
||||
// isSelfPaced is false as well as isCustomRelativeDatesActive is false
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterDueDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.prerequisiteLabel.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with proctored exam info', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.proctoredExam.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with practice proctored exam info', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: true,
|
||||
},
|
||||
});
|
||||
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.practiceProctoredExam.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with onboarding exam info', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: true,
|
||||
isPracticeExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.onboardingExam.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus correctly for graded but not time limited subsection', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isTimeLimited: false,
|
||||
graded: true,
|
||||
},
|
||||
});
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(subsection.format);
|
||||
// exam value label should not be visible
|
||||
expect(queryByTestId('exam-value-span')).not.toBeInTheDocument();
|
||||
// check due date div
|
||||
const dueDateDiv = queryByTestId('due-date-div');
|
||||
expect(dueDateDiv).toBeInTheDocument();
|
||||
expect(dueDateDiv).toHaveTextContent(
|
||||
`${messages.dueLabel.defaultMessage} ${subsection.dueDate}`,
|
||||
);
|
||||
// self paced weeks should not be visible as
|
||||
// isSelfPaced is false as well as isCustomRelativeDatesActive is false
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<XBlockStatus /> for self paced Subsection', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with grading type, due weeks etc.', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
isSelfPaced: true,
|
||||
isCustomRelativeDatesActive: true,
|
||||
blockData: {
|
||||
...subsection,
|
||||
relativeWeeksDue: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// both explanatoryMessage & releaseStatusDiv should not be visible
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(subsection.format);
|
||||
// due date should not be visible for self paced courses.
|
||||
expect(queryByTestId('due-date-div')).not.toBeInTheDocument();
|
||||
// check selfPacedRelativeDueWeeksDiv
|
||||
const selfPacedRelativeDueWeeksDiv = queryByTestId('self-paced-relative-due-weeks-div');
|
||||
expect(selfPacedRelativeDueWeeksDiv).toBeInTheDocument();
|
||||
expect(selfPacedRelativeDueWeeksDiv).toHaveTextContent(
|
||||
messages.customDueDateLabel.defaultMessage,
|
||||
);
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterEndDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.prerequisiteLabel.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
const unit = {
|
||||
id: '123',
|
||||
displayName: 'Unit Name',
|
||||
published: true,
|
||||
visibilityState: 'live',
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
category: 'vertical',
|
||||
explanatoryMessage: '',
|
||||
releasedToStudents: true,
|
||||
releaseDate: 'Feb 05, 2013 at 01:00 UTC',
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
staffOnlyMessage: false,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: 1,
|
||||
selectedGroupsLabel: 'Some label',
|
||||
},
|
||||
hasPartitionGroupComponents: false,
|
||||
format: 'Homework',
|
||||
dueDate: 'Dec 28, 2023 at 22:00 UTC',
|
||||
isTimeLimited: true,
|
||||
graded: true,
|
||||
courseGraders: ['Homework'],
|
||||
};
|
||||
|
||||
describe('<XBlockStatus /> for unit', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with status messages', () => {
|
||||
const { queryByTestId } = renderComponent({ blockData: unit });
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
|
||||
// grading type should not be visible
|
||||
expect(queryByTestId('grading-type-div')).not.toBeInTheDocument();
|
||||
// due date should not be visible
|
||||
expect(queryByTestId('due-date-div')).not.toBeInTheDocument();
|
||||
|
||||
// self paced weeks should not be visible for units
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
|
||||
// check hide after due date message
|
||||
// hide after due date message should not be visible as the flag is set to false
|
||||
expect(queryByTestId('hide-after-due-message')).not.toBeInTheDocument();
|
||||
|
||||
// check status messages for partition info
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.restrictedUnitAccess.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with status messages', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...unit,
|
||||
hasPartitionGroupComponents: true,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// check status messages for partition info
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.restrictedUnitAccessToSomeContent.defaultMessage);
|
||||
});
|
||||
});
|
||||
78
src/course-outline/xblock-status/messages.js
Normal file
78
src/course-outline/xblock-status/messages.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
unscheduledLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.unscheduled.label',
|
||||
defaultMessage: 'Unscheduled',
|
||||
},
|
||||
releasedLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.released.label',
|
||||
defaultMessage: 'Released: ',
|
||||
},
|
||||
scheduledLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.scheduled.label',
|
||||
defaultMessage: 'Scheduled: ',
|
||||
},
|
||||
onboardingExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.onboardingExam.value',
|
||||
defaultMessage: 'Onboarding Exam',
|
||||
},
|
||||
practiceProctoredExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.practiceProctoredExam.value',
|
||||
defaultMessage: 'Practice proctored Exam',
|
||||
},
|
||||
proctoredExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.proctoredExam.value',
|
||||
defaultMessage: 'Proctored Exam',
|
||||
},
|
||||
timedExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.timedExam.value',
|
||||
defaultMessage: 'Timed Exam',
|
||||
},
|
||||
releaseStatusScreenReaderTitle: {
|
||||
id: 'course-authoring.course-outline.xblock-status.releaseStatusScreenReader.title',
|
||||
defaultMessage: 'Release Status: ',
|
||||
},
|
||||
gradedAsScreenReaderLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.gradedAsScreenReader.label',
|
||||
defaultMessage: 'Graded as: ',
|
||||
},
|
||||
ungradedText: {
|
||||
id: 'course-authoring.course-outline.xblock-status.ungraded.text',
|
||||
defaultMessage: 'Ungraded',
|
||||
},
|
||||
dueLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.due.label',
|
||||
defaultMessage: 'Due:',
|
||||
},
|
||||
customDueDateLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.custom-due-date.label',
|
||||
defaultMessage: 'Custom due date: {relativeWeeksDue, plural, one {# week} other {# weeks}} from enrollment',
|
||||
},
|
||||
prerequisiteLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.prerequisite.label',
|
||||
defaultMessage: 'Prerequisite: {prereqDisplayName}',
|
||||
},
|
||||
restrictedUnitAccess: {
|
||||
id: 'course-authoring.course-outline.xblock-status.restrictedUnitAccess.text',
|
||||
defaultMessage: 'Access to this unit is restricted to: {selectedGroupsLabel}',
|
||||
},
|
||||
restrictedUnitAccessToSomeContent: {
|
||||
id: 'course-authoring.course-outline.xblock-status.restrictedUnitAccessToSomeContent.text',
|
||||
defaultMessage: 'Access to some content in this unit is restricted to specific groups of learners',
|
||||
},
|
||||
gradingPolicyMismatchText: {
|
||||
id: 'course-authoring.course-outline.xblock-status.gradingPolicyMismatch.text',
|
||||
defaultMessage: 'This subsection is configured as "{gradingType}", which doesn\'t exist in the current grading policy.',
|
||||
},
|
||||
hiddenAfterEndDate: {
|
||||
id: 'course-authoring.course-outline.xblock-status.hiddenAfterEndDate.text',
|
||||
defaultMessage: 'Subsection is hidden after course end date',
|
||||
},
|
||||
hiddenAfterDueDate: {
|
||||
id: 'course-authoring.course-outline.xblock-status.hiddenAfterDueDate.text',
|
||||
defaultMessage: 'Subsection is hidden after due date',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useHelpUrls } from '../../help-urls/hooks';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Stack,
|
||||
ActionRow,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
Button,
|
||||
Container,
|
||||
Layout,
|
||||
} from '@edx/paragon';
|
||||
import { Add as IconAdd } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import { useModel } from '../generic/model-store';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Add as IconAdd } from '@edx/paragon/icons';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Button,
|
||||
Form,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { Formik } from 'formik';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
Icon,
|
||||
IconButtonWithTooltip,
|
||||
MailtoLink,
|
||||
} from '@edx/paragon';
|
||||
import { DeleteOutline } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { DeleteOutline } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
import { USER_ROLES, BADGE_STATES } from '../constants';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useToggle } from '@edx/paragon';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ActionRow,
|
||||
Button,
|
||||
AlertModal,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { MODAL_TYPES } from '../constants';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Container, Layout } from '@edx/paragon';
|
||||
import { Container, Layout } from '@openedx/paragon';
|
||||
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ErrorAlert } from '@edx/frontend-lib-content-components';
|
||||
|
||||
@@ -11,19 +11,21 @@ import { RequestStatus } from '../data/constants';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import Loading from '../generic/Loading';
|
||||
import AddComponent from './add-component/AddComponent';
|
||||
import HeaderTitle from './header-title/HeaderTitle';
|
||||
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||
import HeaderNavigations from './header-navigations/HeaderNavigations';
|
||||
import Sequence from './course-sequence';
|
||||
import { useCourseUnit } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
import './CourseUnit.scss';
|
||||
|
||||
const CourseUnit = ({ courseId }) => {
|
||||
const { blockId } = useParams();
|
||||
const intl = useIntl();
|
||||
const {
|
||||
isLoading,
|
||||
sequenceId,
|
||||
unitTitle,
|
||||
savingStatus,
|
||||
isTitleEditFormOpen,
|
||||
@@ -32,6 +34,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
headerNavigationsActions,
|
||||
handleTitleEdit,
|
||||
handleInternetConnectionFailed,
|
||||
handleCreateNewCourseXblock,
|
||||
} = useCourseUnit({ courseId, blockId });
|
||||
|
||||
document.title = getPageHeadTitle('', unitTitle);
|
||||
@@ -42,7 +45,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
} = useSelector(getProcessingNotification);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -63,9 +66,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
/>
|
||||
)}
|
||||
breadcrumbs={(
|
||||
<Breadcrumbs
|
||||
courseId={courseId}
|
||||
/>
|
||||
<Breadcrumbs />
|
||||
)}
|
||||
headerActions={(
|
||||
<HeaderNavigations
|
||||
@@ -73,6 +74,11 @@ const CourseUnit = ({ courseId }) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Sequence
|
||||
courseId={courseId}
|
||||
sequenceId={sequenceId}
|
||||
unitId={blockId}
|
||||
/>
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
@@ -80,7 +86,12 @@ const CourseUnit = ({ courseId }) => {
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element />
|
||||
<Layout.Element>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
handleCreateNewCourseXblock={handleCreateNewCourseXblock}
|
||||
/>
|
||||
</Layout.Element>
|
||||
<Layout.Element />
|
||||
</Layout>
|
||||
</section>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
@import "./breadcrumbs/Breadcrumbs";
|
||||
@import "./course-sequence/CourseSequence";
|
||||
@import "./add-component/AddComponent";
|
||||
|
||||
@@ -5,37 +5,42 @@ import {
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { getConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import {
|
||||
getCourseSectionVerticalApiUrl,
|
||||
getCourseUnitApiUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
postXBlockBaseApiUrl,
|
||||
} from './data/api';
|
||||
import {
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseUnitQuery,
|
||||
} from './data/thunk';
|
||||
import initializeStore from '../store';
|
||||
import {
|
||||
courseCreateXblockMock,
|
||||
courseSectionVerticalMock,
|
||||
courseUnitIndexMock,
|
||||
} from './__mocks__';
|
||||
import { executeThunk } from '../utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
import headerNavigationsMessages from './header-navigations/messages';
|
||||
import headerTitleMessages from './header-title/messages';
|
||||
import { getUnitPreviewPath, getUnitViewLivePath } from './utils';
|
||||
import messages from './add-component/messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseId = '123';
|
||||
const sectionId = 'graded_interactions';
|
||||
const subsectionId = '19a30717eff543078a5d94ae9d6c18a5';
|
||||
const blockId = '567890';
|
||||
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({ blockId }),
|
||||
useNavigate: () => mockedUsedNavigate,
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
@@ -63,6 +68,10 @@ describe('<CourseUnit />', () => {
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render CourseUnit component correctly', async () => {
|
||||
@@ -85,20 +94,21 @@ describe('<CourseUnit />', () => {
|
||||
const { open } = window;
|
||||
window.open = jest.fn();
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const {
|
||||
draft_preview_link: draftPreviewLink,
|
||||
published_preview_link: publishedPreviewLink,
|
||||
} = courseSectionVerticalMock;
|
||||
|
||||
await waitFor(() => {
|
||||
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||
userEvent.click(viewLiveButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
const VIEW_LIVE_LINK = getConfig().LMS_BASE_URL + getUnitViewLivePath(courseId, blockId);
|
||||
expect(window.open).toHaveBeenCalledWith(VIEW_LIVE_LINK, '_blank');
|
||||
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
|
||||
|
||||
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||
userEvent.click(previewButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
// eslint-disable-next-line max-len
|
||||
const PREVIEW_LINK = getConfig().PREVIEW_BASE_URL + getUnitPreviewPath(courseId, sectionId, subsectionId, blockId);
|
||||
expect(window.open).toHaveBeenCalledWith(PREVIEW_LINK, '_blank');
|
||||
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
|
||||
});
|
||||
|
||||
window.open = open;
|
||||
@@ -146,4 +156,57 @@ describe('<CourseUnit />', () => {
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
expect(await findByText(newDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t handle creating xblock and displays an error message', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.reply(500, {});
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const videoButton = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(videoButton);
|
||||
expect(mockedUsedNavigate).not.toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('handle creating Problem xblock and navigate to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const problemButton = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(problemButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles creating Video xblock and navigates to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const videoButton = getByRole('button', {
|
||||
name: new RegExp(`${messages.buttonText.defaultMessage} Video`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(videoButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
4
src/course-unit/__mocks__/courseCreateXblock.js
Normal file
4
src/course-unit/__mocks__/courseCreateXblock.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
locator: 'block-v1:edX+L153+3T2023+type@drag-and-drop-v2+block@dc52e3cf8e6145e39ba5c1ff4888db4b',
|
||||
courseKey: 'course-v1:edX+L153+3T2023',
|
||||
};
|
||||
1418
src/course-unit/__mocks__/courseSectionVertical.js
Normal file
1418
src/course-unit/__mocks__/courseSectionVertical.js
Normal file
@@ -0,0 +1,1418 @@
|
||||
module.exports = {
|
||||
language_code: 'en',
|
||||
action: 'view',
|
||||
xblock: {
|
||||
display_name: 'Getting Started',
|
||||
display_type: 'Unit',
|
||||
category: 'vertical',
|
||||
},
|
||||
is_unit_page: true,
|
||||
is_collapsible: false,
|
||||
position: 1,
|
||||
prev_url: '%2Fcontainer%2Fblock-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%40vertical_0270f6de40fc',
|
||||
next_url: '%2Fcontainer%2Fblock-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%404f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
new_unit_category: 'vertical',
|
||||
outline_url: '/course/course-v1:edX+DemoX+Demo_Course?format=concise',
|
||||
ancestor_xblocks: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40d8a6192ade314473a78242dfeedfbf5b',
|
||||
display_name: 'Introduction 2',
|
||||
},
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations',
|
||||
display_name: 'Example Week 1: Getting Started',
|
||||
},
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40graded_interactions',
|
||||
display_name: 'Example Week 2: Get Interactive',
|
||||
},
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40social_integration',
|
||||
display_name: 'Example Week 3: Be Social',
|
||||
},
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%401414ffd5143b4b508f739b563ab468b7',
|
||||
display_name: 'About Exams and Certificates',
|
||||
},
|
||||
],
|
||||
title: 'Example Week 1: Getting Started',
|
||||
is_last: false,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5',
|
||||
display_name: 'Lesson 1 - Getting Started',
|
||||
},
|
||||
{
|
||||
url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40basic_questions',
|
||||
display_name: 'Homework - Question Styles',
|
||||
},
|
||||
],
|
||||
title: 'Lesson 1 - Getting Started',
|
||||
is_last: true,
|
||||
},
|
||||
],
|
||||
component_templates: [
|
||||
{
|
||||
type: 'advanced',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Annotation',
|
||||
category: 'annotatable',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'common',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Video',
|
||||
category: 'videoalpha',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'common',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Advanced',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'discussion',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Discussion',
|
||||
category: 'discussion',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Discussion',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'library',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Randomized Content Block',
|
||||
category: 'library_content',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'common',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Library Content',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'html',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Text',
|
||||
category: 'html',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Raw HTML',
|
||||
category: 'html',
|
||||
boilerplate_name: 'raw.yaml',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Zooming Image Tool',
|
||||
category: 'html',
|
||||
boilerplate_name: 'zooming_image.yaml',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'IFrame Tool',
|
||||
category: 'html',
|
||||
boilerplate_name: 'iframe.yaml',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Anonymous User ID',
|
||||
category: 'html',
|
||||
boilerplate_name: 'anon_user_id.yaml',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Announcement',
|
||||
category: 'html',
|
||||
boilerplate_name: 'announcement.yaml',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Text',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'openassessment',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Peer Assessment Only',
|
||||
category: 'openassessment',
|
||||
boilerplate_name: 'peer-assessment',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Self Assessment Only',
|
||||
category: 'openassessment',
|
||||
boilerplate_name: 'self-assessment',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Staff Assessment Only',
|
||||
category: 'openassessment',
|
||||
boilerplate_name: 'staff-assessment',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Self Assessment to Peer Assessment',
|
||||
category: 'openassessment',
|
||||
boilerplate_name: 'self-to-peer',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
{
|
||||
display_name: 'Self Assessment to Staff Assessment',
|
||||
category: 'openassessment',
|
||||
boilerplate_name: 'self-to-staff',
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Open Response',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'problem',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Blank Common Problem',
|
||||
category: 'problem',
|
||||
boilerplate_name: 'blank_common.yaml',
|
||||
hinted: false,
|
||||
tab: 'common',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Problem',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'video',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Video',
|
||||
category: 'video',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Video',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'drag-and-drop-v2',
|
||||
templates: [
|
||||
{
|
||||
display_name: 'Drag and Drop',
|
||||
category: 'drag-and-drop-v2',
|
||||
boilerplate_name: null,
|
||||
hinted: false,
|
||||
tab: 'advanced',
|
||||
support_level: true,
|
||||
},
|
||||
],
|
||||
display_name: 'Drag and Drop',
|
||||
support_legend: {
|
||||
show_legend: false,
|
||||
allow_unsupported_xblocks: false,
|
||||
documentation_label: 'Your Platform Name Here Support Levels:',
|
||||
},
|
||||
},
|
||||
],
|
||||
xblock_info: {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
display_name: 'Getting Started',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 04, 2024 at 10:32 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: true,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_info: {
|
||||
ancestors: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
display_name: 'Lesson 1 - Getting Started',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 04, 2024 at 10:32 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: null,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
hide_after_due: false,
|
||||
is_proctored_exam: false,
|
||||
was_exam_ever_linked_with_external: false,
|
||||
online_proctoring_rules: '',
|
||||
is_practice_exam: false,
|
||||
is_onboarding_exam: false,
|
||||
is_time_limited: false,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: null,
|
||||
proctoring_exam_configuration_link: null,
|
||||
supports_onboarding: false,
|
||||
show_review_rules: true,
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
display_name: 'Getting Started',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 04, 2024 at 10:32 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: true,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
display_name: 'Working with Videos',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
display_name: 'Videos on edX',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
display_name: 'Video Demonstrations',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
display_name: 'Video Presentation Styles',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
display_name: 'Interactive Questions',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
||||
display_name: 'Exciting Labs and Tools',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
display_name: 'Reading Assignments',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
||||
display_name: 'When Are Your Exams? ',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
display_name: 'Example Week 1: Getting Started',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 04, 2024 at 10:32 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: null,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
highlights: [],
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
display_name: 'Demonstration Course',
|
||||
category: 'course',
|
||||
has_children: true,
|
||||
unit_level_discussions: false,
|
||||
edited_on: 'Jan 08, 2024 at 16:39 UTC',
|
||||
published: true,
|
||||
published_on: 'Jan 08, 2024 at 16:39 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: null,
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: null,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
highlights_enabled_for_messaging: false,
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
|
||||
enable_proctored_exams: false,
|
||||
create_zendesk_tickets: true,
|
||||
enable_timed_exams: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
edited_by: 'edx',
|
||||
published_by: null,
|
||||
currently_visible_to_students: true,
|
||||
has_partition_group_components: false,
|
||||
release_date_from: 'Section "Example Week 1: Getting Started"',
|
||||
staff_lock_from: null,
|
||||
},
|
||||
draft_preview_link: '//preview.localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
published_preview_link: '//localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
show_unit_tags: false,
|
||||
user_clipboard: {
|
||||
content: null,
|
||||
source_usage_key: '',
|
||||
source_context_title: '',
|
||||
source_edit_url: '',
|
||||
},
|
||||
is_fullwidth_content: false,
|
||||
assets_url: '/assets/course-v1:edX+DemoX+Demo_Course/',
|
||||
unit_block_id: '867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
subsection_location: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as courseUnitIndexMock } from './courseUnitIndex';
|
||||
export { default as courseSectionVerticalMock } from './courseSectionVertical';
|
||||
export { default as courseCreateXblockMock } from './courseCreateXblock';
|
||||
|
||||
64
src/course-unit/add-component/AddComponent.jsx
Normal file
64
src/course-unit/add-component/AddComponent.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import { COMPONENT_ICON_TYPES } from '../constants';
|
||||
import ComponentIcon from './ComponentIcon';
|
||||
import messages from './messages';
|
||||
|
||||
const AddComponent = ({ blockId, handleCreateNewCourseXblock }) => {
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const { componentTemplates } = useSelector(getCourseSectionVertical);
|
||||
|
||||
const handleCreateNewXblock = (type) => () => {
|
||||
switch (type) {
|
||||
case COMPONENT_ICON_TYPES.discussion:
|
||||
case COMPONENT_ICON_TYPES.dragAndDrop:
|
||||
handleCreateNewCourseXblock({ type, parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_ICON_TYPES.problem:
|
||||
case COMPONENT_ICON_TYPES.video:
|
||||
handleCreateNewCourseXblock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
if (!Object.keys(componentTemplates).length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
|
||||
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
|
||||
{Object.keys(componentTemplates).map((component) => (
|
||||
<li key={componentTemplates[component].type}>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
className="add-component-button flex-column rounded-sm"
|
||||
onClick={handleCreateNewXblock(componentTemplates[component].type)}
|
||||
>
|
||||
<ComponentIcon type={componentTemplates[component].type} />
|
||||
<span className="sr-only">{intl.formatMessage(messages.buttonText)}</span>
|
||||
<span className="small mt-2">{componentTemplates[component].displayName}</span>
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AddComponent.propTypes = {
|
||||
blockId: PropTypes.string.isRequired,
|
||||
handleCreateNewCourseXblock: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default AddComponent;
|
||||
12
src/course-unit/add-component/AddComponent.scss
Normal file
12
src/course-unit/add-component/AddComponent.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.course-unit {
|
||||
.new-component-type {
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.add-component-button {
|
||||
@include pgn-box-shadow(1, "down");
|
||||
|
||||
width: 11.63rem;
|
||||
height: 6.875rem;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user