feat: [FC-0044] Unit page - Manage access modal (unit & xblocks) (#901)
* feat: [FC-0044] Unit page - Manage access modal (unit & xblocks) * fix: add message description
This commit is contained in:
@@ -49,3 +49,10 @@ export const DECODED_ROUTES = {
|
||||
'/container/:blockId',
|
||||
],
|
||||
};
|
||||
|
||||
export const COURSE_BLOCK_NAMES = ({
|
||||
chapter: { id: 'chapter', name: 'Section' },
|
||||
sequential: { id: 'sequential', name: 'Subsection' },
|
||||
vertical: { id: 'vertical', name: 'Unit' },
|
||||
component: { id: 'component', name: 'Component' },
|
||||
});
|
||||
|
||||
@@ -29,9 +29,10 @@ import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import DeleteModal from '../generic/delete-modal/DeleteModal';
|
||||
import ConfigureModal from '../generic/configure-modal/ConfigureModal';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { getCurrentItem } from './data/selectors';
|
||||
import { getCurrentItem, getProctoredExamsFlag } from './data/selectors';
|
||||
import { COURSE_BLOCK_NAMES } from './constants';
|
||||
import HeaderNavigations from './header-navigations/HeaderNavigations';
|
||||
import OutlineSideBar from './outline-sidebar/OutlineSidebar';
|
||||
@@ -43,7 +44,6 @@ import UnitCard from './unit-card/UnitCard';
|
||||
import HighlightsModal from './highlights-modal/HighlightsModal';
|
||||
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
|
||||
import PublishModal from './publish-modal/PublishModal';
|
||||
import ConfigureModal from './configure-modal/ConfigureModal';
|
||||
import PageAlerts from './page-alerts/PageAlerts';
|
||||
import DraggableList from './drag-helper/DraggableList';
|
||||
import {
|
||||
@@ -129,8 +129,10 @@ const CourseOutline = ({ courseId }) => {
|
||||
title: processingNotificationTitle,
|
||||
} = useSelector(getProcessingNotification);
|
||||
|
||||
const { category } = useSelector(getCurrentItem);
|
||||
const deleteCategory = COURSE_BLOCK_NAMES[category]?.name.toLowerCase();
|
||||
const currentItemData = useSelector(getCurrentItem);
|
||||
const deleteCategory = COURSE_BLOCK_NAMES[currentItemData.category]?.name.toLowerCase();
|
||||
|
||||
const enableProctoredExams = useSelector(getProctoredExamsFlag);
|
||||
|
||||
/**
|
||||
* Move section to new index
|
||||
@@ -431,6 +433,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
isOpen={isConfigureModalOpen}
|
||||
onClose={handleConfigureModalClose}
|
||||
onConfigureSubmit={handleConfigureItemSubmit}
|
||||
currentItemData={currentItemData}
|
||||
enableProctoredExams={enableProctoredExams}
|
||||
/>
|
||||
<DeleteModal
|
||||
category={deleteCategory}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
@import "./empty-placeholder/EmptyPlaceholder";
|
||||
@import "./highlights-modal/HighlightsModal";
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/SortableItem";
|
||||
@import "./xblock-status/XBlockStatus";
|
||||
@import "./paste-button/PasteButton";
|
||||
|
||||
@@ -40,12 +40,12 @@ import {
|
||||
import { executeThunk } from '../utils';
|
||||
import { COURSE_BLOCK_NAMES, VIDEO_SHARING_OPTIONS } from './constants';
|
||||
import CourseOutline from './CourseOutline';
|
||||
import messages from './messages';
|
||||
|
||||
import configureModalMessages from '../generic/configure-modal/messages';
|
||||
import headerMessages from './header-navigations/messages';
|
||||
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';
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
moveSubsection,
|
||||
moveUnit,
|
||||
} from './drag-helper/utils';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
@@ -48,6 +48,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
handleTitleEdit,
|
||||
handleInternetConnectionFailed,
|
||||
handleCreateNewCourseXBlock,
|
||||
handleConfigureSubmit,
|
||||
courseVerticalChildren,
|
||||
} = useCourseUnit({ courseId, blockId });
|
||||
|
||||
@@ -85,6 +86,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
isTitleEditFormOpen={isTitleEditFormOpen}
|
||||
handleTitleEdit={handleTitleEdit}
|
||||
handleTitleEditSubmit={handleTitleEditSubmit}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
)}
|
||||
breadcrumbs={(
|
||||
@@ -119,16 +121,20 @@ const CourseUnit = ({ courseId }) => {
|
||||
)}
|
||||
<Stack gap={4} className="mb-4">
|
||||
{courseVerticalChildren.children.map(({
|
||||
name, blockId: id, blockType: type, shouldScroll,
|
||||
name, blockId: id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
|
||||
}) => (
|
||||
<CourseXBlock
|
||||
id={id}
|
||||
key={id}
|
||||
title={name}
|
||||
type={type}
|
||||
blockId={blockId}
|
||||
validationMessages={validationMessages}
|
||||
shouldScroll={shouldScroll}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
data-testid="course-xblock"
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
@import "./add-component/AddComponent";
|
||||
@import "./course-xblock/CourseXBlock";
|
||||
@import "./sidebar/Sidebar";
|
||||
@import "./header-title/HeaderTitle";
|
||||
|
||||
@@ -38,13 +38,14 @@ import courseSequenceMessages from './course-sequence/messages';
|
||||
import sidebarMessages from './sidebar/messages';
|
||||
import { extractCourseUnitId } from './sidebar/utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
import messages from './messages';
|
||||
|
||||
import deleteModalMessages from '../generic/delete-modal/messages';
|
||||
import configureModalMessages from '../generic/configure-modal/messages';
|
||||
import courseXBlockMessages from './course-xblock/messages';
|
||||
import addComponentMessages from './add-component/messages';
|
||||
import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
|
||||
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -571,6 +572,7 @@ describe('<CourseUnit />', () => {
|
||||
name: 'New Cloned XBlock',
|
||||
block_id: '1234567890',
|
||||
block_type: 'drag-and-drop-v2',
|
||||
user_partition_info: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -594,7 +596,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle visibility and update course unit state accordingly', async () => {
|
||||
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let draftUnpublishedChangesHeading;
|
||||
@@ -617,7 +619,7 @@ describe('<CourseUnit />', () => {
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: true },
|
||||
metadata: { visible_to_staff_only: true, group_access: null },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
@@ -654,7 +656,7 @@ describe('<CourseUnit />', () => {
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: null },
|
||||
metadata: { visible_to_staff_only: null, group_access: null },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
@@ -942,4 +944,73 @@ describe('<CourseUnit />', () => {
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let sidebarVisibilityCheckbox;
|
||||
let modalVisibilityCheckbox;
|
||||
let configureModal;
|
||||
let restrictAccessSelect;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
sidebarVisibilityCheckbox = within(courseUnitSidebar)
|
||||
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
expect(sidebarVisibilityCheckbox).not.toBeChecked();
|
||||
|
||||
const headerConfigureBtn = getByRole('button', { name: /settings/i });
|
||||
expect(headerConfigureBtn).toBeInTheDocument();
|
||||
|
||||
userEvent.click(headerConfigureBtn);
|
||||
configureModal = getByTestId('configure-modal');
|
||||
restrictAccessSelect = within(configureModal)
|
||||
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
|
||||
expect(within(configureModal)
|
||||
.getByText(configureModalMessages.unitVisibility.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(configureModal)
|
||||
.getByText(configureModalMessages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||
expect(restrictAccessSelect).toBeInTheDocument();
|
||||
expect(restrictAccessSelect).toHaveValue('-1');
|
||||
|
||||
modalVisibilityCheckbox = within(configureModal)
|
||||
.getByRole('checkbox', { name: configureModalMessages.hideFromLearners.defaultMessage });
|
||||
expect(modalVisibilityCheckbox).not.toBeChecked();
|
||||
|
||||
userEvent.click(modalVisibilityCheckbox);
|
||||
expect(modalVisibilityCheckbox).toBeChecked();
|
||||
|
||||
userEvent.selectOptions(restrictAccessSelect, '0');
|
||||
const [, group1Checkbox] = within(configureModal).getAllByRole('checkbox');
|
||||
|
||||
userEvent.click(group1Checkbox);
|
||||
expect(group1Checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
|
||||
publish: null,
|
||||
metadata: { visible_to_staff_only: true, group_access: { 50: [2] } },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.replyOnce(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
});
|
||||
|
||||
const modalSaveBtn = within(configureModal)
|
||||
.getByRole('button', { name: configureModalMessages.saveButton.defaultMessage });
|
||||
userEvent.click(modalSaveBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sidebarVisibilityCheckbox).toBeChecked();
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleVisibleToStaffOnly.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.visibilityStaffOnlyTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,146 @@ module.exports = {
|
||||
children: [
|
||||
{
|
||||
name: 'Discussion',
|
||||
block_id: 'block-v1:OpenedX+L153+3T2023+type@discussion+block@fecd20842dd24f50bdc06643e791b013',
|
||||
block_id: 'block-v1:OpenedX+L153+3T2023+type@discussion+block@5a28279f24344723a96b1268d3b7cfc0',
|
||||
block_type: 'discussion',
|
||||
actions: {
|
||||
can_copy: true,
|
||||
can_duplicate: true,
|
||||
can_move: true,
|
||||
can_manage_access: true,
|
||||
can_delete: true,
|
||||
},
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 970807507,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1959537066,
|
||||
name: 'Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 108068059,
|
||||
name: 'Group 2',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 970807507,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1959537066,
|
||||
name: 'Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 108068059,
|
||||
name: 'Group 2',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Drag and Drop',
|
||||
block_id: 'block-v1:OpenedX+L153+3T2023+type@drag-and-drop-v2+block@b33cf1f6df4c41639659bc91132eeb02',
|
||||
block_type: 'drag-and-drop-v2',
|
||||
actions: {
|
||||
can_copy: true,
|
||||
can_duplicate: true,
|
||||
can_move: true,
|
||||
can_manage_access: true,
|
||||
can_delete: true,
|
||||
},
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 970807507,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1959537066,
|
||||
name: 'Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 108068059,
|
||||
name: 'Group 2',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 970807507,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1959537066,
|
||||
name: 'Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 108068059,
|
||||
name: 'Group 2',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
is_published: false,
|
||||
isPublished: false,
|
||||
};
|
||||
|
||||
@@ -9,21 +9,37 @@ import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
||||
import { scrollToElement } from '../../course-outline/utils';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import { getCourseId } from '../data/selectors';
|
||||
import { COMPONENT_TYPES } from '../constants';
|
||||
import XBlockMessages from './xblock-messages/XBlockMessages';
|
||||
import messages from './messages';
|
||||
|
||||
const CourseXBlock = ({
|
||||
id, title, type, unitXBlockActions, shouldScroll, ...props
|
||||
id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo,
|
||||
handleConfigureSubmit, validationMessages, ...props
|
||||
}) => {
|
||||
const courseXBlockElementRef = useRef(null);
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const navigate = useNavigate();
|
||||
const courseId = useSelector(getCourseId);
|
||||
const intl = useIntl();
|
||||
|
||||
const onXBlockDelete = () => {
|
||||
const visibilityMessage = userPartitionInfo.selectedGroupsLabel
|
||||
? intl.formatMessage(messages.visibilityMessage, { selectedGroupsLabel: userPartitionInfo.selectedGroupsLabel })
|
||||
: null;
|
||||
|
||||
const currentItemData = {
|
||||
category: COURSE_BLOCK_NAMES.component.id,
|
||||
displayName: title,
|
||||
userPartitionInfo,
|
||||
showCorrectness: 'always',
|
||||
};
|
||||
|
||||
const onDeleteSubmit = () => {
|
||||
unitXBlockActions.handleDelete(id);
|
||||
closeDeleteModal();
|
||||
};
|
||||
@@ -39,6 +55,10 @@ const CourseXBlock = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onConfigureSubmit = (...arg) => {
|
||||
handleConfigureSubmit(id, ...arg, closeConfigureModal);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// if this item has been newly added, scroll to it.
|
||||
if (courseXBlockElementRef.current && shouldScroll) {
|
||||
@@ -51,6 +71,7 @@ const CourseXBlock = ({
|
||||
<Card className="mb-1">
|
||||
<Card.Header
|
||||
title={title}
|
||||
subtitle={visibilityMessage}
|
||||
actions={(
|
||||
<ActionRow>
|
||||
<IconButton
|
||||
@@ -78,7 +99,7 @@ const CourseXBlock = ({
|
||||
<Dropdown.Item>
|
||||
{intl.formatMessage(messages.blockLabelButtonMove)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item>
|
||||
<Dropdown.Item onClick={openConfigureModal}>
|
||||
{intl.formatMessage(messages.blockLabelButtonManageAccess)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={openDeleteModal}>
|
||||
@@ -90,13 +111,21 @@ const CourseXBlock = ({
|
||||
category="component"
|
||||
isOpen={isDeleteModalOpen}
|
||||
close={closeDeleteModal}
|
||||
onDeleteSubmit={onXBlockDelete}
|
||||
onDeleteSubmit={onDeleteSubmit}
|
||||
/>
|
||||
<ConfigureModal
|
||||
isXBlockComponent
|
||||
isOpen={isConfigureModalOpen}
|
||||
onClose={closeConfigureModal}
|
||||
onConfigureSubmit={onConfigureSubmit}
|
||||
currentItemData={currentItemData}
|
||||
/>
|
||||
</ActionRow>
|
||||
)}
|
||||
size="md"
|
||||
/>
|
||||
<Card.Section>
|
||||
<XBlockMessages validationMessages={validationMessages} />
|
||||
<div className="w-100 bg-gray-100" style={{ height: 200 }} data-block-id={id} />
|
||||
</Card.Section>
|
||||
</Card>
|
||||
@@ -105,6 +134,7 @@ const CourseXBlock = ({
|
||||
};
|
||||
|
||||
CourseXBlock.defaultProps = {
|
||||
validationMessages: [],
|
||||
shouldScroll: false,
|
||||
};
|
||||
|
||||
@@ -113,10 +143,30 @@ CourseXBlock.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
shouldScroll: PropTypes.bool,
|
||||
validationMessages: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
})),
|
||||
unitXBlockActions: PropTypes.shape({
|
||||
handleDelete: PropTypes.func,
|
||||
handleDuplicate: PropTypes.func,
|
||||
}).isRequired,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
|
||||
groups: PropTypes.arrayOf(PropTypes.shape({
|
||||
deleted: PropTypes.bool,
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
selected: PropTypes.bool,
|
||||
})),
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
scheme: PropTypes.string,
|
||||
})),
|
||||
selectedPartitionIndex: PropTypes.number,
|
||||
selectedGroupsLabel: PropTypes.string,
|
||||
}).isRequired,
|
||||
handleConfigureSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CourseXBlock;
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
.course-unit {
|
||||
.pgn__card .pgn__card-header {
|
||||
border-bottom: 1px solid $light-400;
|
||||
padding-bottom: map-get($spacers, 2);
|
||||
.course-unit__xblocks {
|
||||
.pgn__card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $light-400;
|
||||
padding-bottom: map-get($spacers, 2);
|
||||
|
||||
.pgn__card-header-content {
|
||||
margin-top: map-get($spacers, 3\.5);
|
||||
&:not(:has(.pgn__card-header-subtitle-md)) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon .btn-icon__icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
.pgn__card-header-subtitle-md {
|
||||
margin-top: 0;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.pgn__card-header-title-md {
|
||||
font: 700 1.375rem/1.75rem $font-family-sans-serif;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.pgn__card-section {
|
||||
padding: map-get($spacers, 3\.5) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-iframe__wrapper .alert-danger {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
render, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { getCourseId } from '../data/selectors';
|
||||
import { COMPONENT_TYPES } from '../constants';
|
||||
import { courseVerticalChildrenMock } from '../__mocks__';
|
||||
import CourseXBlock from './CourseXBlock';
|
||||
|
||||
import configureModalMessages from '../../generic/configure-modal/messages';
|
||||
import deleteModalMessages from '../../generic/delete-modal/messages';
|
||||
import initializeStore from '../../store';
|
||||
import { getCourseSectionVerticalApiUrl, getXBlockBaseApiUrl } from '../data/api';
|
||||
import { fetchCourseSectionVerticalData } from '../data/thunk';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseId } from '../data/selectors';
|
||||
import { PUBLISH_TYPES, COMPONENT_TYPES } from '../constants';
|
||||
import { courseSectionVerticalMock, courseVerticalChildrenMock } from '../__mocks__';
|
||||
import CourseXBlock from './CourseXBlock';
|
||||
import messages from './messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const courseId = '1234';
|
||||
const blockId = '567890';
|
||||
@@ -26,6 +35,7 @@ const {
|
||||
block_type: type,
|
||||
user_partition_info: userPartitionInfo,
|
||||
} = courseVerticalChildrenMock.children[0];
|
||||
const userPartitionInfoFormatted = camelCaseObject(userPartitionInfo);
|
||||
const unitXBlockActionsMock = {
|
||||
handleDelete: handleDeleteMock,
|
||||
handleDuplicate: handleDuplicateMock,
|
||||
@@ -50,7 +60,7 @@ const renderComponent = (props) => render(
|
||||
type={type}
|
||||
blockId={blockId}
|
||||
unitXBlockActions={unitXBlockActionsMock}
|
||||
userPartitionInfo={camelCaseObject(userPartitionInfo)}
|
||||
userPartitionInfo={userPartitionInfoFormatted}
|
||||
shouldScroll={false}
|
||||
handleConfigureSubmit={handleConfigureSubmitMock}
|
||||
{...props}
|
||||
@@ -76,6 +86,13 @@ describe('<CourseXBlock />', () => {
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render CourseXBlock component correctly', async () => {
|
||||
@@ -93,7 +110,6 @@ describe('<CourseXBlock />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage));
|
||||
expect(getByRole('button', { name: messages.blockLabelButtonCopy.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.blockLabelButtonDuplicate.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.blockLabelButtonMove.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.blockLabelButtonManageAccess.defaultMessage })).toBeInTheDocument();
|
||||
@@ -181,6 +197,117 @@ describe('<CourseXBlock />', () => {
|
||||
userEvent.click(editButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/editor/problem/${id}`);
|
||||
expect(handleDeleteMock).toHaveBeenCalledWith(id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('restrict access', () => {
|
||||
it('opens restrict access modal successfully', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByLabelText,
|
||||
findByTestId,
|
||||
} = renderComponent();
|
||||
|
||||
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
|
||||
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
|
||||
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
|
||||
|
||||
userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage));
|
||||
const accessBtn = getByText(messages.blockLabelButtonManageAccess.defaultMessage);
|
||||
|
||||
userEvent.click(accessBtn);
|
||||
const configureModal = await findByTestId('configure-modal');
|
||||
|
||||
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
||||
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
|
||||
expect(within(configureModal).getByRole('button', { name: modalSaveBtnText })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes restrict access modal when cancel button is clicked', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByLabelText,
|
||||
findByTestId,
|
||||
} = renderComponent();
|
||||
|
||||
userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage));
|
||||
const accessBtn = getByText(messages.blockLabelButtonManageAccess.defaultMessage);
|
||||
|
||||
userEvent.click(accessBtn);
|
||||
const configureModal = await findByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
|
||||
userEvent.click(within(configureModal).getByRole('button', { name: configureModalMessages.saveButton.defaultMessage }));
|
||||
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles submit restrict access data when save button is clicked', async () => {
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(id), {
|
||||
publish: PUBLISH_TYPES.republish,
|
||||
metadata: { visible_to_staff_only: false, group_access: { 970807507: [1959537066] } },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const {
|
||||
getByText,
|
||||
getByLabelText,
|
||||
findByTestId,
|
||||
getByRole,
|
||||
} = renderComponent();
|
||||
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
|
||||
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
|
||||
|
||||
userEvent.click(getByLabelText(messages.blockActionsDropdownAlt.defaultMessage));
|
||||
const accessBtn = getByText(messages.blockLabelButtonManageAccess.defaultMessage);
|
||||
|
||||
userEvent.click(accessBtn);
|
||||
const configureModal = await findByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
||||
|
||||
const restrictAccessSelect = getByRole('combobox', {
|
||||
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
||||
});
|
||||
userEvent.selectOptions(restrictAccessSelect, '0');
|
||||
|
||||
// eslint-disable-next-line array-callback-return
|
||||
userPartitionInfoFormatted.selectablePartitions[0].groups.map((group) => {
|
||||
expect(within(configureModal).getByRole('checkbox', { name: group.name })).not.toBeChecked();
|
||||
expect(within(configureModal).queryByText(group.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 });
|
||||
userEvent.click(group1Checkbox);
|
||||
expect(group1Checkbox).toBeChecked();
|
||||
|
||||
const saveModalBtnText = within(configureModal).getByRole('button', {
|
||||
name: configureModalMessages.saveButton.defaultMessage,
|
||||
});
|
||||
expect(saveModalBtnText).toBeInTheDocument();
|
||||
userEvent.click(saveModalBtnText);
|
||||
await waitFor(() => {
|
||||
expect(handleConfigureSubmitMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a visibility message if item has accessible restrictions', async () => {
|
||||
const { getByText } = renderComponent(
|
||||
{
|
||||
userPartitionInfo: {
|
||||
...userPartitionInfoFormatted,
|
||||
selectedGroupsLabel: 'Visibility group 1',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const visibilityMessage = messages.visibilityMessage.defaultMessage
|
||||
.replace('{selectedGroupsLabel}', 'Visibility group 1');
|
||||
expect(getByText(visibilityMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
5
src/course-unit/course-xblock/constants.js
Normal file
5
src/course-unit/course-xblock/constants.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const MESSAGE_ERROR_TYPES = {
|
||||
error: 'error',
|
||||
warning: 'warning',
|
||||
};
|
||||
@@ -4,30 +4,47 @@ const messages = defineMessages({
|
||||
blockAltButtonEdit: {
|
||||
id: 'course-authoring.course-unit.xblock.button.edit.alt',
|
||||
defaultMessage: 'Edit',
|
||||
description: 'The xblock edit button text',
|
||||
},
|
||||
blockActionsDropdownAlt: {
|
||||
id: 'course-authoring.course-unit.xblock.button.actions.alt',
|
||||
defaultMessage: 'Actions',
|
||||
description: 'The xblock three dots dropdown alt text',
|
||||
},
|
||||
blockLabelButtonCopy: {
|
||||
id: 'course-authoring.course-unit.xblock.button.copy.label',
|
||||
defaultMessage: 'Copy',
|
||||
description: 'The xblock copy button text',
|
||||
},
|
||||
blockLabelButtonDuplicate: {
|
||||
id: 'course-authoring.course-unit.xblock.button.duplicate.label',
|
||||
defaultMessage: 'Duplicate',
|
||||
description: 'The xblock duplicate button text',
|
||||
},
|
||||
blockLabelButtonMove: {
|
||||
id: 'course-authoring.course-unit.xblock.button.move.label',
|
||||
defaultMessage: 'Move',
|
||||
description: 'The xblock move button text',
|
||||
},
|
||||
blockLabelButtonManageAccess: {
|
||||
id: 'course-authoring.course-unit.xblock.button.manageAccess.label',
|
||||
defaultMessage: 'Manage access',
|
||||
description: 'The xblock manage access button text',
|
||||
},
|
||||
blockLabelButtonDelete: {
|
||||
id: 'course-authoring.course-unit.xblock.button.delete.label',
|
||||
defaultMessage: 'Delete',
|
||||
description: 'The xblock delete button text',
|
||||
},
|
||||
visibilityMessage: {
|
||||
id: 'course-authoring.course-unit.xblock.visibility.message',
|
||||
defaultMessage: 'Access restricted to: {selectedGroupsLabel}',
|
||||
description: 'Group visibility accessibility text for xblock',
|
||||
},
|
||||
validationSummary: {
|
||||
id: 'course-authoring.course-unit.xblock.validation.summary',
|
||||
defaultMessage: 'This component has validation issues.',
|
||||
description: 'The alert text of the visibility validation issues',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Info as InfoIcon, WarningFilled as WarningIcon } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from '../messages';
|
||||
import { MESSAGE_ERROR_TYPES } from '../constants';
|
||||
import { getMessagesBlockType } from './utils';
|
||||
|
||||
const XBlockMessages = ({ validationMessages }) => {
|
||||
const intl = useIntl();
|
||||
const type = getMessagesBlockType(validationMessages);
|
||||
const { warning } = MESSAGE_ERROR_TYPES;
|
||||
const alertVariant = type === warning ? 'warning' : 'danger';
|
||||
const alertIcon = type === warning ? WarningIcon : InfoIcon;
|
||||
|
||||
if (!validationMessages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
variant={alertVariant}
|
||||
icon={alertIcon}
|
||||
>
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages.validationSummary)}
|
||||
</Alert.Heading>
|
||||
<ul>
|
||||
{validationMessages.map(({ text }) => (
|
||||
<li key={text}>{text}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
XBlockMessages.defaultProps = {
|
||||
validationMessages: [],
|
||||
};
|
||||
|
||||
XBlockMessages.propTypes = {
|
||||
validationMessages: PropTypes.arrayOf(PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
text: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
export default XBlockMessages;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
import XBlockMessages from './XBlockMessages';
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<IntlProvider locale="en">
|
||||
<XBlockMessages
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<XBlockMessages />', () => {
|
||||
it('renders without errors', () => {
|
||||
renderComponent({ validationMessages: [] });
|
||||
});
|
||||
|
||||
it('does not render anything when there are no errors', () => {
|
||||
const { container } = renderComponent({ validationMessages: [] });
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders a warning Alert when there are warning errors', () => {
|
||||
const validationMessages = [{ type: 'warning', text: 'This is a warning' }];
|
||||
const { getByText } = renderComponent({ validationMessages });
|
||||
|
||||
expect(getByText('This is a warning')).toBeInTheDocument();
|
||||
expect(getByText(messages.validationSummary.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a danger Alert when there are danger errors', () => {
|
||||
const validationMessages = [{ type: 'danger', text: 'This is a danger' }];
|
||||
const { getByText } = renderComponent({ validationMessages });
|
||||
|
||||
expect(getByText('This is a danger')).toBeInTheDocument();
|
||||
expect(getByText(messages.validationSummary.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders multiple error messages in a list', () => {
|
||||
const validationMessages = [
|
||||
{ type: 'warning', text: 'Warning 1' },
|
||||
{ type: 'danger', text: 'Danger 1' },
|
||||
{ type: 'danger', text: 'Danger 2' },
|
||||
];
|
||||
const { getByText } = renderComponent({ validationMessages });
|
||||
|
||||
expect(getByText('Warning 1')).toBeInTheDocument();
|
||||
expect(getByText('Danger 1')).toBeInTheDocument();
|
||||
expect(getByText('Danger 2')).toBeInTheDocument();
|
||||
expect(getByText(messages.validationSummary.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
16
src/course-unit/course-xblock/xblock-messages/utils.js
Normal file
16
src/course-unit/course-xblock/xblock-messages/utils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MESSAGE_ERROR_TYPES } from '../constants';
|
||||
|
||||
/**
|
||||
* Determines the block type based on the types of messages in the given array.
|
||||
* @param {Array} messages - An array of message objects.
|
||||
* @param {Object[]} messages.type - The type of each message (e.g., MESSAGE_ERROR_TYPES.error).
|
||||
* @returns {string} - The block type determined by the messages (e.g., 'warning' or 'error').
|
||||
*/
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getMessagesBlockType = (messages) => {
|
||||
let type = MESSAGE_ERROR_TYPES.warning;
|
||||
if (messages.some((message) => message.type === MESSAGE_ERROR_TYPES.error)) {
|
||||
type = MESSAGE_ERROR_TYPES.error;
|
||||
}
|
||||
return type;
|
||||
};
|
||||
44
src/course-unit/course-xblock/xblock-messages/utils.test.js
Normal file
44
src/course-unit/course-xblock/xblock-messages/utils.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { MESSAGE_ERROR_TYPES } from '../constants';
|
||||
import { getMessagesBlockType } from './utils';
|
||||
|
||||
describe('xblock-messages utils', () => {
|
||||
describe('getMessagesBlockType', () => {
|
||||
it('returns "warning" when there are no error messages', () => {
|
||||
const messages = [
|
||||
{ type: MESSAGE_ERROR_TYPES.warning, text: 'This is a warning' },
|
||||
{ type: MESSAGE_ERROR_TYPES.warning, text: 'Another warning' },
|
||||
];
|
||||
const result = getMessagesBlockType(messages);
|
||||
|
||||
expect(result).toBe(MESSAGE_ERROR_TYPES.warning);
|
||||
});
|
||||
|
||||
it('returns "error" when there is at least one error message', () => {
|
||||
const messages = [
|
||||
{ type: MESSAGE_ERROR_TYPES.warning, text: 'This is a warning' },
|
||||
{ type: MESSAGE_ERROR_TYPES.error, text: 'This is an error' },
|
||||
{ type: MESSAGE_ERROR_TYPES.warning, text: 'Another warning' },
|
||||
];
|
||||
const result = getMessagesBlockType(messages);
|
||||
|
||||
expect(result).toBe(MESSAGE_ERROR_TYPES.error);
|
||||
});
|
||||
|
||||
it('returns "error" when there are only error messages', () => {
|
||||
const messages = [
|
||||
{ type: MESSAGE_ERROR_TYPES.error, text: 'This is an error' },
|
||||
{ type: MESSAGE_ERROR_TYPES.error, text: 'Another error' },
|
||||
];
|
||||
const result = getMessagesBlockType(messages);
|
||||
|
||||
expect(result).toBe(MESSAGE_ERROR_TYPES.error);
|
||||
});
|
||||
|
||||
it('returns "warning" when there are no messages', () => {
|
||||
const messages = [];
|
||||
const result = getMessagesBlockType(messages);
|
||||
|
||||
expect(result).toBe(MESSAGE_ERROR_TYPES.warning);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -88,14 +88,16 @@ export async function createCourseXblock({
|
||||
* @param {string} unitId - The ID of the course unit.
|
||||
* @param {string} type - The action type (e.g., PUBLISH_TYPES.discardChanges).
|
||||
* @param {boolean} isVisible - The visibility status for students.
|
||||
* @param {boolean} groupAccess - Access group key set.
|
||||
* @returns {Promise<any>} A promise that resolves with the response data.
|
||||
*/
|
||||
export async function handleCourseUnitVisibilityAndData(unitId, type, isVisible) {
|
||||
export async function handleCourseUnitVisibilityAndData(unitId, type, isVisible, groupAccess) {
|
||||
const body = {
|
||||
publish: type,
|
||||
publish: groupAccess ? null : type,
|
||||
...(type === PUBLISH_TYPES.republish ? {
|
||||
metadata: {
|
||||
visible_to_staff_only: isVisible,
|
||||
visible_to_staff_only: isVisible ? true : null,
|
||||
group_access: groupAccess || null,
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const slice = createSlice({
|
||||
},
|
||||
unit: {},
|
||||
courseSectionVertical: {},
|
||||
courseVerticalChildren: [],
|
||||
courseVerticalChildren: {},
|
||||
},
|
||||
reducers: {
|
||||
fetchCourseItemSuccess: (state, { payload }) => {
|
||||
|
||||
@@ -111,19 +111,19 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function editCourseUnitVisibilityAndData(itemId, type, isVisible) {
|
||||
export function editCourseUnitVisibilityAndData(itemId, type, isVisible, groupAccess, isModalView, blockId = itemId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(updateQueryPendingStatus(true));
|
||||
const notificationMessage = getNotificationMessage(type, isVisible);
|
||||
dispatch(showProcessingNotification(notificationMessage));
|
||||
const notification = getNotificationMessage(type, isVisible, isModalView);
|
||||
dispatch(showProcessingNotification(notification));
|
||||
|
||||
try {
|
||||
await handleCourseUnitVisibilityAndData(itemId, type, isVisible).then(async (result) => {
|
||||
await handleCourseUnitVisibilityAndData(itemId, type, isVisible, groupAccess).then(async (result) => {
|
||||
if (result) {
|
||||
const courseUnit = await getCourseUnitData(itemId);
|
||||
const courseUnit = await getCourseUnitData(blockId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
|
||||
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
|
||||
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
|
||||
@@ -30,15 +30,18 @@ export function normalizeCourseSectionVerticalData(metadata) {
|
||||
* Get the notification message based on the publishing type and visibility.
|
||||
* @param {string} type - The publishing type.
|
||||
* @param {boolean} isVisible - The visibility status.
|
||||
* @param {boolean} isModalView - The modal view status.
|
||||
* @returns {string} The corresponding notification message.
|
||||
*/
|
||||
export const getNotificationMessage = (type, isVisible) => {
|
||||
export const getNotificationMessage = (type, isVisible, isModalView) => {
|
||||
let notificationMessage;
|
||||
|
||||
if (type === PUBLISH_TYPES.discardChanges) {
|
||||
notificationMessage = NOTIFICATION_MESSAGES.discardChanges;
|
||||
} else if (type === PUBLISH_TYPES.makePublic) {
|
||||
notificationMessage = NOTIFICATION_MESSAGES.publishing;
|
||||
} else if (type === PUBLISH_TYPES.republish && isModalView) {
|
||||
notificationMessage = NOTIFICATION_MESSAGES.saving;
|
||||
} else if (type === PUBLISH_TYPES.republish && !isVisible) {
|
||||
notificationMessage = NOTIFICATION_MESSAGES.makingVisibleToStudents;
|
||||
} else if (type === PUBLISH_TYPES.republish && isVisible) {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, IconButton } from '@openedx/paragon';
|
||||
import { Form, IconButton, useToggle } from '@openedx/paragon';
|
||||
import {
|
||||
EditOutline as EditIcon,
|
||||
Settings as SettingsIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
||||
import { getCourseUnitData } from '../data/selectors';
|
||||
import { updateQueryPendingStatus } from '../data/slice';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -16,10 +18,30 @@ const HeaderTitle = ({
|
||||
isTitleEditFormOpen,
|
||||
handleTitleEdit,
|
||||
handleTitleEditSubmit,
|
||||
handleConfigureSubmit,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const [titleValue, setTitleValue] = useState(unitTitle);
|
||||
const currentItemData = useSelector(getCourseUnitData);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const { selectedPartitionIndex, selectedGroupsLabel } = currentItemData.userPartitionInfo;
|
||||
|
||||
const onConfigureSubmit = (...arg) => {
|
||||
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
|
||||
};
|
||||
|
||||
const getVisibilityMessage = () => {
|
||||
let message;
|
||||
|
||||
if (selectedPartitionIndex !== -1 && !Number.isNaN(selectedPartitionIndex) && selectedGroupsLabel) {
|
||||
message = intl.formatMessage(messages.definedVisibilityMessage, { selectedGroupsLabel });
|
||||
} else if (currentItemData.hasPartitionGroupComponents) {
|
||||
message = intl.formatMessage(messages.commonVisibilityMessage);
|
||||
}
|
||||
|
||||
return message ? (<p className="header-title__visibility-message mb-0">{message}</p>) : null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTitleValue(unitTitle);
|
||||
@@ -27,38 +49,46 @@ const HeaderTitle = ({
|
||||
}, [unitTitle]);
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center lead" data-testid="unit-header-title">
|
||||
{isTitleEditFormOpen ? (
|
||||
<Form.Group className="m-0">
|
||||
<Form.Control
|
||||
ref={(e) => e && e.focus()}
|
||||
value={titleValue}
|
||||
name="displayName"
|
||||
onChange={(e) => setTitleValue(e.target.value)}
|
||||
aria-label={intl.formatMessage(messages.ariaLabelButtonEdit)}
|
||||
onBlur={() => handleTitleEditSubmit(titleValue)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleTitleEditSubmit(titleValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
) : unitTitle}
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
||||
className="ml-1 flex-shrink-0"
|
||||
iconAs={EditIcon}
|
||||
onClick={handleTitleEdit}
|
||||
/>
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.altButtonSettings)}
|
||||
className="flex-shrink-0"
|
||||
iconAs={SettingsIcon}
|
||||
onClick={() => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="d-flex align-items-center lead" data-testid="unit-header-title">
|
||||
{isTitleEditFormOpen ? (
|
||||
<Form.Group className="m-0">
|
||||
<Form.Control
|
||||
ref={(e) => e && e.focus()}
|
||||
value={titleValue}
|
||||
name="displayName"
|
||||
onChange={(e) => setTitleValue(e.target.value)}
|
||||
aria-label={intl.formatMessage(messages.ariaLabelButtonEdit)}
|
||||
onBlur={() => handleTitleEditSubmit(titleValue)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleTitleEditSubmit(titleValue);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
) : unitTitle}
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
||||
className="ml-1 flex-shrink-0"
|
||||
iconAs={EditIcon}
|
||||
onClick={handleTitleEdit}
|
||||
/>
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.altButtonSettings)}
|
||||
className="flex-shrink-0"
|
||||
iconAs={SettingsIcon}
|
||||
onClick={openConfigureModal}
|
||||
/>
|
||||
<ConfigureModal
|
||||
isOpen={isConfigureModalOpen}
|
||||
onClose={closeConfigureModal}
|
||||
onConfigureSubmit={onConfigureSubmit}
|
||||
currentItemData={currentItemData}
|
||||
/>
|
||||
</div>
|
||||
{getVisibilityMessage()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,6 +97,7 @@ HeaderTitle.propTypes = {
|
||||
isTitleEditFormOpen: PropTypes.bool.isRequired,
|
||||
handleTitleEdit: PropTypes.func.isRequired,
|
||||
handleTitleEditSubmit: PropTypes.func.isRequired,
|
||||
handleConfigureSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default HeaderTitle;
|
||||
|
||||
4
src/course-unit/header-title/HeaderTitle.scss
Normal file
4
src/course-unit/header-title/HeaderTitle.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.header-title__visibility-message {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
@@ -5,14 +7,21 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseUnitApiUrl } from '../data/api';
|
||||
import { fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseUnitIndexMock } from '../__mocks__';
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import messages from './messages';
|
||||
|
||||
const blockId = '123';
|
||||
const unitTitle = 'Getting Started';
|
||||
const isTitleEditFormOpen = false;
|
||||
const handleTitleEdit = jest.fn();
|
||||
const handleTitleEditSubmit = jest.fn();
|
||||
const handleConfigureSubmit = jest.fn();
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
@@ -22,6 +31,7 @@ const renderComponent = (props) => render(
|
||||
isTitleEditFormOpen={isTitleEditFormOpen}
|
||||
handleTitleEdit={handleTitleEdit}
|
||||
handleTitleEditSubmit={handleTitleEditSubmit}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
@@ -40,6 +50,11 @@ describe('<HeaderTitle />', () => {
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render HeaderTitle component correctly', () => {
|
||||
@@ -85,4 +100,36 @@ describe('<HeaderTitle />', () => {
|
||||
expect(titleField).toHaveValue(`${unitTitle} 1 2`);
|
||||
expect(handleTitleEditSubmit).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('displays a visibility message with the selected groups for the unit', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
user_partition_info: {
|
||||
...courseUnitIndexMock.user_partition_info,
|
||||
selected_partition_index: '1',
|
||||
selected_groups_label: 'Visibility group 1',
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
||||
const { getByText } = renderComponent();
|
||||
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
|
||||
.replace('{selectedGroupsLabel}', 'Visibility group 1');
|
||||
|
||||
expect(getByText(visibilityMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays a visibility message with the selected groups for some of xblock', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
has_partition_group_components: true,
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
expect(getByText(messages.commonVisibilityMessage.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,14 +4,27 @@ const messages = defineMessages({
|
||||
altButtonEdit: {
|
||||
id: 'course-authoring.course-unit.heading.button.edit.alt',
|
||||
defaultMessage: 'Edit',
|
||||
description: 'The unit edit button text',
|
||||
},
|
||||
ariaLabelButtonEdit: {
|
||||
id: 'course-authoring.course-unit.heading.button.edit.aria-label',
|
||||
defaultMessage: 'Edit field',
|
||||
description: 'The unit edit button aria label',
|
||||
},
|
||||
altButtonSettings: {
|
||||
id: 'course-authoring.course-unit.heading.button.settings.alt',
|
||||
defaultMessage: 'Settings',
|
||||
description: 'The unit settings button text',
|
||||
},
|
||||
definedVisibilityMessage: {
|
||||
id: 'course-authoring.course-unit.heading.visibility.defined.message',
|
||||
defaultMessage: 'Access to this unit is restricted to: {selectedGroupsLabel}',
|
||||
description: 'Group visibility accessibility text for Unit',
|
||||
},
|
||||
commonVisibilityMessage: {
|
||||
id: 'course-authoring.course-unit.heading.visibility.common.message',
|
||||
defaultMessage: 'Access to some content in this unit is restricted to specific groups of learners.',
|
||||
description: 'The label text of some content restriction in this unit',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
fetchCourseVerticalChildrenData,
|
||||
deleteUnitItemQuery,
|
||||
duplicateUnitItemQuery,
|
||||
editCourseUnitVisibilityAndData,
|
||||
} from './data/thunk';
|
||||
import {
|
||||
getCourseSectionVertical,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
getSequenceStatus,
|
||||
} from './data/selectors';
|
||||
import { changeEditTitleFormOpen, updateQueryPendingStatus } from './data/slice';
|
||||
import { PUBLISH_TYPES } from './constants';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
@@ -59,6 +61,11 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
dispatch(changeEditTitleFormOpen(!isTitleEditFormOpen));
|
||||
};
|
||||
|
||||
const handleConfigureSubmit = (id, isVisible, groupAccess, closeModalFn) => {
|
||||
dispatch(editCourseUnitVisibilityAndData(id, PUBLISH_TYPES.republish, isVisible, groupAccess, true, blockId));
|
||||
closeModalFn();
|
||||
};
|
||||
|
||||
const handleTitleEditSubmit = (displayName) => {
|
||||
if (unitTitle !== displayName) {
|
||||
dispatch(editCourseItemQuery(blockId, displayName, sequenceId));
|
||||
@@ -121,6 +128,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
handleTitleEdit,
|
||||
handleTitleEditSubmit,
|
||||
handleCreateNewCourseXBlock,
|
||||
handleConfigureSubmit,
|
||||
courseVerticalChildren,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Stack, Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { DatepickerControl, DATEPICKER_TYPES } from '../datepicker-control';
|
||||
import messages from './messages';
|
||||
import { DatepickerControl, DATEPICKER_TYPES } from '../../generic/datepicker-control';
|
||||
|
||||
const BasicTab = ({
|
||||
values,
|
||||
@@ -11,12 +11,10 @@ import {
|
||||
Tab,
|
||||
Tabs,
|
||||
} 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, getProctoredExamsFlag } from '../data/selectors';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import messages from './messages';
|
||||
import BasicTab from './BasicTab';
|
||||
import VisibilityTab from './VisibilityTab';
|
||||
@@ -27,6 +25,9 @@ const ConfigureModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfigureSubmit,
|
||||
currentItemData,
|
||||
enableProctoredExams,
|
||||
isXBlockComponent,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
@@ -57,8 +58,7 @@ const ConfigureModal = ({
|
||||
supportsOnboarding,
|
||||
showReviewRules,
|
||||
onlineProctoringRules,
|
||||
} = useSelector(getCurrentItem);
|
||||
const enableProctoredExams = useSelector(getProctoredExamsFlag);
|
||||
} = currentItemData;
|
||||
|
||||
const getSelectedGroups = () => {
|
||||
if (userPartitionInfo?.selectedPartitionIndex >= 0) {
|
||||
@@ -81,7 +81,6 @@ const ConfigureModal = ({
|
||||
const initialValues = {
|
||||
releaseDate: sectionStartDate,
|
||||
isVisibleToStaffOnly: visibilityState === VisibilityTypes.STAFF_ONLY,
|
||||
saveButtonDisabled: true,
|
||||
graderType: format == null ? 'notgraded' : format,
|
||||
dueDate: due == null ? '' : due,
|
||||
isTimeLimited,
|
||||
@@ -132,6 +131,10 @@ const ConfigureModal = ({
|
||||
|
||||
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
|
||||
|
||||
const dialogTitle = isXBlockComponent
|
||||
? intl.formatMessage(messages.componentTitle, { title: displayName })
|
||||
: intl.formatMessage(messages.title, { title: displayName });
|
||||
|
||||
const handleSave = (data) => {
|
||||
const groupAccess = {};
|
||||
switch (category) {
|
||||
@@ -159,6 +162,7 @@ const ConfigureModal = ({
|
||||
);
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
|
||||
if (data.selectedPartitionIndex >= 0) {
|
||||
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
||||
@@ -232,8 +236,10 @@ const ConfigureModal = ({
|
||||
</Tabs>
|
||||
);
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
return (
|
||||
<UnitTab
|
||||
isXBlockComponent={COURSE_BLOCK_NAMES.component.id === category}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY && !ancestorHasStaffLock}
|
||||
@@ -257,7 +263,7 @@ const ConfigureModal = ({
|
||||
<div data-testid="configure-modal">
|
||||
<ModalDialog.Header className="configure-modal__header">
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages.title, { title: displayName })}
|
||||
{dialogTitle}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<Formik
|
||||
@@ -268,7 +274,7 @@ const ConfigureModal = ({
|
||||
validateOnChange
|
||||
>
|
||||
{({
|
||||
values, handleSubmit, dirty, isValid, setFieldValue,
|
||||
values, handleSubmit, setFieldValue,
|
||||
}) => (
|
||||
<>
|
||||
<ModalDialog.Body className="configure-modal__body">
|
||||
@@ -281,7 +287,10 @@ const ConfigureModal = ({
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button data-testid="configure-save-button" onClick={handleSubmit} disabled={!(dirty && isValid)}>
|
||||
<Button
|
||||
data-testid="configure-save-button"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{intl.formatMessage(messages.saveButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
@@ -294,10 +303,63 @@ const ConfigureModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
ConfigureModal.defaultProps = {
|
||||
isXBlockComponent: false,
|
||||
enableProctoredExams: false,
|
||||
};
|
||||
|
||||
ConfigureModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfigureSubmit: PropTypes.func.isRequired,
|
||||
enableProctoredExams: PropTypes.bool,
|
||||
currentItemData: PropTypes.shape({
|
||||
displayName: PropTypes.string,
|
||||
start: PropTypes.string,
|
||||
visibilityState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
due: PropTypes.string,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
defaultTimeLimitMinutes: PropTypes.number,
|
||||
hideAfterDue: PropTypes.bool,
|
||||
showCorrectness: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string),
|
||||
category: PropTypes.string,
|
||||
format: PropTypes.string,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
|
||||
groups: PropTypes.arrayOf(PropTypes.shape({
|
||||
deleted: PropTypes.bool,
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
selected: PropTypes.bool,
|
||||
})),
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
scheme: PropTypes.string,
|
||||
})),
|
||||
selectedPartitionIndex: PropTypes.number,
|
||||
selectedGroupsLabel: PropTypes.string,
|
||||
}),
|
||||
ancestorHasStaffLock: PropTypes.bool,
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqs: PropTypes.arrayOf({
|
||||
blockDisplayName: PropTypes.string,
|
||||
blockUsageKey: PropTypes.string,
|
||||
}),
|
||||
prereq: PropTypes.number,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
releasedToStudents: PropTypes.bool,
|
||||
wasExamEverLinkedWithExternal: PropTypes.bool,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
examReviewRules: PropTypes.string,
|
||||
supportsOnboarding: PropTypes.bool,
|
||||
showReviewRules: PropTypes.bool,
|
||||
onlineProctoringRules: PropTypes.string,
|
||||
}).isRequired,
|
||||
isXBlockComponent: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ConfigureModal;
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
@@ -9,6 +8,12 @@ import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import ConfigureModal from './ConfigureModal';
|
||||
import {
|
||||
currentSectionMock,
|
||||
currentSubsectionMock,
|
||||
currentUnitMock,
|
||||
currentXBlockMock,
|
||||
} from './__mocks__';
|
||||
import messages from './messages';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -28,79 +33,6 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const currentSectionMock = {
|
||||
displayName: 'Section1',
|
||||
category: 'chapter',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
format: 'Not Graded',
|
||||
childInfo: {
|
||||
displayName: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 2',
|
||||
id: 2,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
displayName: 'Subsection_2 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 3',
|
||||
id: 3,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const onCloseMock = jest.fn();
|
||||
const onConfigureSubmitMock = jest.fn();
|
||||
|
||||
@@ -111,6 +43,7 @@ const renderComponent = () => render(
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentSectionMock}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
@@ -129,12 +62,11 @@ describe('<ConfigureModal /> for Section', () => {
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
useSelector.mockReturnValue(currentSectionMock);
|
||||
});
|
||||
|
||||
it('renders ConfigureModal component correctly', () => {
|
||||
const { getByText, getByRole } = renderComponent();
|
||||
expect(getByText(`${currentSectionMock.displayName} Settings`)).toBeInTheDocument();
|
||||
expect(getByText(`${currentSectionMock.displayName} settings`)).toBeInTheDocument();
|
||||
expect(getByText(messages.basicTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.visibilityTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.releaseDate.defaultMessage)).toBeInTheDocument();
|
||||
@@ -147,55 +79,12 @@ describe('<ConfigureModal /> for Section', () => {
|
||||
const { getByRole, getByText } = renderComponent();
|
||||
|
||||
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
expect(getByText('Section Visibility')).toBeInTheDocument();
|
||||
userEvent.click(visibilityTab);
|
||||
expect(getByText('Section visibility')).toBeInTheDocument();
|
||||
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the Save button and enables it if there is a change', () => {
|
||||
const { getByRole, getByPlaceholderText, getByTestId } = renderComponent();
|
||||
|
||||
const saveButton = getByRole('button', { name: messages.saveButton.defaultMessage });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const input = getByPlaceholderText('MM/DD/YYYY');
|
||||
fireEvent.change(input, { target: { value: '12/15/2023' } });
|
||||
|
||||
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
const checkbox = getByTestId('visibility-checkbox');
|
||||
fireEvent.click(checkbox);
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
const currentSubsectionMock = {
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
displayName: 'Subsection_1 Unit 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const renderSubsectionComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
@@ -203,6 +92,7 @@ const renderSubsectionComponent = () => render(
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentSubsectionMock}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
@@ -221,12 +111,11 @@ describe('<ConfigureModal /> for Subsection', () => {
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
useSelector.mockReturnValue(currentSubsectionMock);
|
||||
});
|
||||
|
||||
it('renders subsection ConfigureModal component correctly', () => {
|
||||
const { getByText, getByRole } = renderSubsectionComponent();
|
||||
expect(getByText(`${currentSubsectionMock.displayName} Settings`)).toBeInTheDocument();
|
||||
expect(getByText(`${currentSubsectionMock.displayName} settings`)).toBeInTheDocument();
|
||||
expect(getByText(messages.basicTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.visibilityTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.advancedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -244,8 +133,8 @@ describe('<ConfigureModal /> for Subsection', () => {
|
||||
const { getByRole, getByText } = renderSubsectionComponent();
|
||||
|
||||
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
expect(getByText('Subsection Visibility')).toBeInTheDocument();
|
||||
userEvent.click(visibilityTab);
|
||||
expect(getByText('Subsection visibility')).toBeInTheDocument();
|
||||
expect(getByText(messages.showEntireSubsection.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.showEntireSubsectionDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideContentAfterDue.defaultMessage)).toBeInTheDocument();
|
||||
@@ -265,82 +154,23 @@ describe('<ConfigureModal /> for Subsection', () => {
|
||||
const { getByRole, getByText } = renderSubsectionComponent();
|
||||
|
||||
const advancedTab = getByRole('tab', { name: messages.advancedTabTitle.defaultMessage });
|
||||
fireEvent.click(advancedTab);
|
||||
userEvent.click(advancedTab);
|
||||
expect(getByText(messages.setSpecialExam.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.none.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.timed.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.timedDescription.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the Save button and enables it if there is a change', () => {
|
||||
const { getByRole, getByTestId } = renderSubsectionComponent();
|
||||
|
||||
const saveButton = getByRole('button', { name: messages.saveButton.defaultMessage });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const input = getByTestId('grader-type-select');
|
||||
fireEvent.change(input, { target: { value: 'Exam' } });
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
const currentUnitMock = {
|
||||
displayName: 'Unit 1',
|
||||
id: 1,
|
||||
category: 'vertical',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 6,
|
||||
name: 'Honor',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1508065533,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1224170703,
|
||||
name: 'Content Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
};
|
||||
|
||||
const renderUnitComponent = () => render(
|
||||
const renderUnitComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentUnitMock}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
@@ -359,14 +189,13 @@ describe('<ConfigureModal /> for Unit', () => {
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
useSelector.mockReturnValue(currentUnitMock);
|
||||
});
|
||||
|
||||
it('renders unit ConfigureModal component correctly', () => {
|
||||
const {
|
||||
getByText, queryByText, getByRole, getByTestId,
|
||||
} = renderUnitComponent();
|
||||
expect(getByText(`${currentUnitMock.displayName} Settings`)).toBeInTheDocument();
|
||||
expect(getByText(`${currentUnitMock.displayName} settings`)).toBeInTheDocument();
|
||||
expect(getByText(messages.unitVisibility.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||
@@ -375,8 +204,8 @@ describe('<ConfigureModal /> for Unit', () => {
|
||||
expect(queryByText(messages.unitSelectGroup.defaultMessage)).not.toBeInTheDocument();
|
||||
const input = getByTestId('group-type-select');
|
||||
|
||||
[0, 1].forEach(groupeTypeIndex => {
|
||||
fireEvent.change(input, { target: { value: groupeTypeIndex } });
|
||||
['0', '1'].forEach(groupeTypeIndex => {
|
||||
userEvent.selectOptions(input, groupeTypeIndex);
|
||||
|
||||
expect(getByText(messages.unitSelectGroup.defaultMessage)).toBeInTheDocument();
|
||||
currentUnitMock
|
||||
@@ -388,32 +217,62 @@ describe('<ConfigureModal /> for Unit', () => {
|
||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the Save button and enables it if there is a change', () => {
|
||||
useSelector.mockReturnValue(
|
||||
{
|
||||
...currentUnitMock,
|
||||
userPartitionInfo: {
|
||||
...currentUnitMock.userPartitionInfo,
|
||||
selectedPartitionIndex: 0,
|
||||
},
|
||||
const renderXBlockComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
isXBlockComponent
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<ConfigureModal /> for XBlock', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
);
|
||||
const { getByRole, getByTestId } = renderUnitComponent();
|
||||
});
|
||||
|
||||
const saveButton = getByRole('button', { name: messages.saveButton.defaultMessage });
|
||||
expect(saveButton).toBeDisabled();
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders unit ConfigureModal component correctly', () => {
|
||||
const {
|
||||
getByText, queryByText, getByRole, getByTestId,
|
||||
} = renderXBlockComponent();
|
||||
expect(getByText(`Editing access for: ${currentUnitMock.displayName}`)).toBeInTheDocument();
|
||||
expect(queryByText(messages.unitVisibility.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(queryByText(messages.hideFromLearners.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(queryByText(messages.unitSelectGroup.defaultMessage)).not.toBeInTheDocument();
|
||||
const input = getByTestId('group-type-select');
|
||||
// unrestrict access
|
||||
fireEvent.change(input, { target: { value: -1 } });
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.change(input, { target: { value: 0 } });
|
||||
expect(saveButton).toBeDisabled();
|
||||
['0', '1'].forEach(groupeTypeIndex => {
|
||||
userEvent.selectOptions(input, groupeTypeIndex);
|
||||
|
||||
const checkbox = getByTestId('unit-visibility-checkbox');
|
||||
fireEvent.click(checkbox);
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
expect(getByText(messages.unitSelectGroup.defaultMessage)).toBeInTheDocument();
|
||||
currentUnitMock
|
||||
.userPartitionInfo
|
||||
.selectablePartitions[groupeTypeIndex].groups
|
||||
.forEach(g => expect(getByText(g.name)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
import FormikControl from '../../generic/FormikControl';
|
||||
import FormikControl from '../FormikControl';
|
||||
|
||||
const PrereqSettings = ({
|
||||
values,
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
FormattedMessage, injectIntl, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Field } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const UnitTab = ({
|
||||
isXBlockComponent,
|
||||
values,
|
||||
setFieldValue,
|
||||
showWarning,
|
||||
@@ -18,6 +20,7 @@ const UnitTab = ({
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
selectedPartitionIndex,
|
||||
selectedGroups,
|
||||
} = values;
|
||||
|
||||
const handleChange = (e) => {
|
||||
@@ -26,21 +29,32 @@ const UnitTab = ({
|
||||
|
||||
const handleSelect = (e) => {
|
||||
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
||||
setFieldValue('selectedGroups', []);
|
||||
};
|
||||
|
||||
const checkIsDeletedGroup = (group) => {
|
||||
const isGroupSelected = selectedGroups.includes(group.id.toString());
|
||||
|
||||
return group.deleted && isGroupSelected;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="unit-visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||
</Alert>
|
||||
{!isXBlockComponent && (
|
||||
<>
|
||||
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="unit-visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<hr />
|
||||
<Form.Group controlId="groupSelect">
|
||||
<Form.Label as="legend" className="font-weight-bold">
|
||||
<FormattedMessage {...messages.restrictAccessTo} />
|
||||
@@ -89,9 +103,19 @@ const UnitTab = ({
|
||||
value={`${group.id}`}
|
||||
name="selectedGroups"
|
||||
/>
|
||||
<Form.Label isInline>
|
||||
{group.name}
|
||||
</Form.Label>
|
||||
<div>
|
||||
<Form.Label
|
||||
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
|
||||
isInline
|
||||
>
|
||||
{group.name}
|
||||
</Form.Label>
|
||||
{group.deleted && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
{intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
))}
|
||||
</div>
|
||||
@@ -103,13 +127,21 @@ const UnitTab = ({
|
||||
);
|
||||
};
|
||||
|
||||
UnitTab.defaultProps = {
|
||||
isXBlockComponent: false,
|
||||
};
|
||||
|
||||
UnitTab.propTypes = {
|
||||
isXBlockComponent: PropTypes.bool,
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
selectedPartitionIndex: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
selectedGroups: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
]),
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
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';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
|
||||
const VisibilityTab = ({
|
||||
values,
|
||||
199
src/generic/configure-modal/__mocks__/index.js
Normal file
199
src/generic/configure-modal/__mocks__/index.js
Normal file
@@ -0,0 +1,199 @@
|
||||
export const currentSectionMock = {
|
||||
displayName: 'Section1',
|
||||
category: 'chapter',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
format: 'Not Graded',
|
||||
childInfo: {
|
||||
displayName: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 2',
|
||||
id: 2,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
displayName: 'Subsection_2 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 3',
|
||||
id: 3,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const currentSubsectionMock = {
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
displayName: 'Subsection_1 Unit 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const currentUnitMock = {
|
||||
displayName: 'Unit 1',
|
||||
id: 1,
|
||||
category: 'vertical',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 6,
|
||||
name: 'Honor',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1508065533,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1224170703,
|
||||
name: 'Content Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const currentXBlockMock = {
|
||||
displayName: 'Unit 1',
|
||||
id: 1,
|
||||
category: 'component',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 6,
|
||||
name: 'Honor',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1508065533,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1224170703,
|
||||
name: 'Content Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,12 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'course-authoring.course-outline.configure-modal.title',
|
||||
defaultMessage: '{title} Settings',
|
||||
defaultMessage: '{title} settings',
|
||||
},
|
||||
componentTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.component.title',
|
||||
defaultMessage: 'Editing access for: {title}',
|
||||
description: 'The visibility modal title for unit',
|
||||
},
|
||||
basicTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.title',
|
||||
@@ -15,15 +20,15 @@ const messages = defineMessages({
|
||||
},
|
||||
releaseDateAndTime: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date-and-time',
|
||||
defaultMessage: 'Release Date and Time',
|
||||
defaultMessage: 'Release date and time',
|
||||
},
|
||||
releaseDate: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date',
|
||||
defaultMessage: 'Release Date:',
|
||||
defaultMessage: 'Release date:',
|
||||
},
|
||||
releaseTimeUTC: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-time-UTC',
|
||||
defaultMessage: 'Release Time in UTC:',
|
||||
defaultMessage: 'Release time in UTC:',
|
||||
},
|
||||
visibilityTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.title',
|
||||
@@ -31,11 +36,11 @@ const messages = defineMessages({
|
||||
},
|
||||
visibilitySectionTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility',
|
||||
defaultMessage: '{visibilityTitle} Visibility',
|
||||
defaultMessage: '{visibilityTitle} visibility',
|
||||
},
|
||||
unitVisibility: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-visibility',
|
||||
defaultMessage: 'Unit Visibility',
|
||||
defaultMessage: 'Unit visibility',
|
||||
},
|
||||
hideFromLearners: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility.hide-from-learners',
|
||||
@@ -65,6 +70,11 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group-type',
|
||||
defaultMessage: 'Select a group type',
|
||||
},
|
||||
unitSelectDeletedGroupErrorMessage: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group-deleted-error-message',
|
||||
defaultMessage: 'This group no longer exists. Choose another group or remove the access restriction.',
|
||||
description: 'The alert text of no longer available group',
|
||||
},
|
||||
unitAllLearnersAndStaff: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-all-learners-staff',
|
||||
defaultMessage: 'All Learners and Staff',
|
||||
@@ -87,15 +97,15 @@ const messages = defineMessages({
|
||||
},
|
||||
dueDate: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.due-date',
|
||||
defaultMessage: 'Due Date:',
|
||||
defaultMessage: 'Due date:',
|
||||
},
|
||||
dueTimeUTC: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.due-time-UTC',
|
||||
defaultMessage: 'Due Time in UTC:',
|
||||
defaultMessage: 'Due time in UTC:',
|
||||
},
|
||||
subsectionVisibility: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.subsection-visibility',
|
||||
defaultMessage: 'Subsection Visibility',
|
||||
defaultMessage: 'Subsection visibility',
|
||||
},
|
||||
showEntireSubsection: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-entire-subsection',
|
||||
@@ -151,7 +161,7 @@ const messages = defineMessages({
|
||||
},
|
||||
setSpecialExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.set-special-exam',
|
||||
defaultMessage: 'Set as a Special Exam',
|
||||
defaultMessage: 'Set as a special exam',
|
||||
},
|
||||
none: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.none',
|
||||
@@ -195,7 +205,7 @@ const messages = defineMessages({
|
||||
},
|
||||
timeAllotted: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-allotted',
|
||||
defaultMessage: 'Time Allotted (HH:MM):',
|
||||
defaultMessage: 'Time allotted (HH:MM):',
|
||||
},
|
||||
timeLimitDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description',
|
||||
@@ -8,3 +8,4 @@
|
||||
@import "./course-stepper/CouseStepper";
|
||||
@import "./tag-count/TagCount";
|
||||
@import "./modal-dropzone/ModalDropzone";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
|
||||
Reference in New Issue
Block a user