feat: discussion setting and release & due date setting (#976)

* fix: hide release and due dates config in self paced courses

* feat: discussion enable setting for unit in outline

* refactor: message text

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>

* fix: modal dialog overflow

---------

Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
This commit is contained in:
Navin Karkera
2024-05-13 20:51:18 +05:30
committed by GitHub
parent 65132eead2
commit d882f2f856
11 changed files with 188 additions and 114 deletions

View File

@@ -438,6 +438,7 @@ const CourseOutline = ({ courseId }) => {
onConfigureSubmit={handleConfigureItemSubmit}
currentItemData={currentItemData}
enableProctoredExams={enableProctoredExams}
isSelfPaced={statusBarData.isSelfPaced}
/>
<DeleteModal
category={deleteCategory}

View File

@@ -1405,6 +1405,7 @@ describe('<CourseOutline />', () => {
publish: 'republish',
metadata: {
visible_to_staff_only: isVisibleToStaffOnly,
discussion_enabled: false,
group_access: newGroupAccess,
},
})
@@ -1423,6 +1424,7 @@ describe('<CourseOutline />', () => {
// after configuraiton response
unit.visibilityState = 'staff_only';
unit.discussion_enabled = false;
unit.userPartitionInfo = {
selectablePartitions: [
{
@@ -1465,6 +1467,11 @@ describe('<CourseOutline />', () => {
)).toBeInTheDocument();
let visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
await act(async () => fireEvent.click(visibilityCheckbox));
let discussionCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
);
expect(discussionCheckbox).toBeChecked();
await act(async () => fireEvent.click(discussionCheckbox));
let groupeType = await within(configureModal).findByTestId('group-type-select');
fireEvent.change(groupeType, { target: { value: '0' } });
@@ -1481,6 +1488,10 @@ describe('<CourseOutline />', () => {
configureModal = await findByTestId('configure-modal');
visibilityCheckbox = await within(configureModal).findByTestId('unit-visibility-checkbox');
expect(visibilityCheckbox).toBeChecked();
discussionCheckbox = await within(configureModal).findByLabelText(
configureModalMessages.discussionEnabledCheckbox.defaultMessage,
);
expect(discussionCheckbox).not.toBeChecked();
groupeType = await within(configureModal).findByTestId('group-type-select');
expect(groupeType).toHaveValue('0');

View File

@@ -310,7 +310,7 @@ export async function configureCourseSubsection(
* @param {object} groupAccess
* @returns {Promise<Object>}
*/
export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess) {
export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess, discussionEnabled) {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(unitId), {
publish: 'republish',
@@ -318,6 +318,7 @@ export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAcc
// The backend expects metadata.visible_to_staff_only to either true or null
visible_to_staff_only: isVisibleToStaffOnly ? true : null,
group_access: groupAccess,
discussion_enabled: discussionEnabled,
},
});

View File

@@ -334,11 +334,11 @@ export function configureCourseSubsectionQuery(
};
}
export function configureCourseUnitQuery(itemId, sectionId, isVisibleToStaffOnly, groupAccess) {
export function configureCourseUnitQuery(itemId, sectionId, isVisibleToStaffOnly, groupAccess, discussionEnabled) {
return async (dispatch) => {
dispatch(configureCourseItemQuery(
sectionId,
async () => configureCourseUnit(itemId, isVisibleToStaffOnly, groupAccess),
async () => configureCourseUnit(itemId, isVisibleToStaffOnly, groupAccess, discussionEnabled),
));
};
}

View File

@@ -38,6 +38,7 @@ const HighlightsModal = ({
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
isOverflowVisible={false}
>
<ModalDialog.Header className="highlights-modal__header">
<ModalDialog.Title>

View File

@@ -30,6 +30,7 @@ const PublishModal = ({
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
isOverflowVisible={false}
>
<ModalDialog.Header className="publish-modal__header">
<ModalDialog.Title>

View File

@@ -10,6 +10,7 @@ const BasicTab = ({
setFieldValue,
courseGraders,
isSubsection,
isSelfPaced,
}) => {
const intl = useIntl();
@@ -27,26 +28,30 @@ const BasicTab = ({
return (
<>
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.releaseDateAndTime} /></h5>
<hr />
<div data-testid="release-date-stack">
<Stack className="mt-3" direction="horizontal" gap={5}>
<DatepickerControl
type={DATEPICKER_TYPES.date}
value={releaseDate}
label={intl.formatMessage(messages.releaseDate)}
controlName="state-date"
onChange={(val) => setFieldValue('releaseDate', val)}
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={releaseDate}
label={intl.formatMessage(messages.releaseTimeUTC)}
controlName="start-time"
onChange={(val) => setFieldValue('releaseDate', val)}
/>
</Stack>
</div>
{!isSelfPaced && (
<>
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.releaseDateAndTime} /></h5>
<hr />
<div data-testid="release-date-stack">
<Stack className="mt-3" direction="horizontal" gap={5}>
<DatepickerControl
type={DATEPICKER_TYPES.date}
value={releaseDate}
label={intl.formatMessage(messages.releaseDate)}
controlName="state-date"
onChange={(val) => setFieldValue('releaseDate', val)}
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={releaseDate}
label={intl.formatMessage(messages.releaseTimeUTC)}
controlName="start-time"
onChange={(val) => setFieldValue('releaseDate', val)}
/>
</Stack>
</div>
</>
)}
{
isSubsection && (
<div>
@@ -66,25 +71,27 @@ const BasicTab = ({
{createOptions()}
</Form.Control>
</Form.Group>
<div data-testid="due-date-stack">
<Stack className="mt-3" direction="horizontal" gap={5}>
<DatepickerControl
type={DATEPICKER_TYPES.date}
value={dueDate}
label={intl.formatMessage(messages.dueDate)}
controlName="state-date"
onChange={(val) => setFieldValue('dueDate', val)}
data-testid="due-date-picker"
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={dueDate}
label={intl.formatMessage(messages.dueTimeUTC)}
controlName="start-time"
onChange={(val) => setFieldValue('dueDate', val)}
/>
</Stack>
</div>
{!isSelfPaced && (
<div data-testid="due-date-stack">
<Stack className="mt-3" direction="horizontal" gap={5}>
<DatepickerControl
type={DATEPICKER_TYPES.date}
value={dueDate}
label={intl.formatMessage(messages.dueDate)}
controlName="state-date"
onChange={(val) => setFieldValue('dueDate', val)}
data-testid="due-date-picker"
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={dueDate}
label={intl.formatMessage(messages.dueTimeUTC)}
controlName="start-time"
onChange={(val) => setFieldValue('dueDate', val)}
/>
</Stack>
</div>
)}
</div>
)
}
@@ -101,6 +108,7 @@ BasicTab.propTypes = {
}).isRequired,
courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
setFieldValue: PropTypes.func.isRequired,
isSelfPaced: PropTypes.bool.isRequired,
};
export default injectIntl(BasicTab);

View File

@@ -28,6 +28,7 @@ const ConfigureModal = ({
currentItemData,
enableProctoredExams,
isXBlockComponent,
isSelfPaced,
}) => {
const intl = useIntl();
const {
@@ -58,6 +59,7 @@ const ConfigureModal = ({
supportsOnboarding,
showReviewRules,
onlineProctoringRules,
discussionEnabled,
} = currentItemData;
const getSelectedGroups = () => {
@@ -98,6 +100,7 @@ const ConfigureModal = ({
// by default it is -1 i.e. accessible to all learners & staff
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
selectedGroups: getSelectedGroups(),
discussionEnabled,
};
const validationSchema = Yup.object().shape({
@@ -127,6 +130,7 @@ const ConfigureModal = ({
).nullable(true),
selectedPartitionIndex: Yup.number().integer(),
selectedGroups: Yup.array().of(Yup.string()),
discussionEnabled: Yup.boolean(),
});
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
@@ -168,7 +172,7 @@ const ConfigureModal = ({
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
}
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess);
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess, data.discussionEnabled);
break;
default:
break;
@@ -186,6 +190,7 @@ const ConfigureModal = ({
setFieldValue={setFieldValue}
isSubsection={isSubsection}
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
isSelfPaced={isSelfPaced}
/>
</Tab>
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
@@ -208,6 +213,7 @@ const ConfigureModal = ({
setFieldValue={setFieldValue}
isSubsection={isSubsection}
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
isSelfPaced={isSelfPaced}
/>
</Tab>
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
@@ -259,6 +265,7 @@ const ConfigureModal = ({
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
isOverflowVisible={false}
>
<div data-testid="configure-modal">
<ModalDialog.Header className="configure-modal__header">
@@ -358,8 +365,10 @@ ConfigureModal.propTypes = {
supportsOnboarding: PropTypes.bool,
showReviewRules: PropTypes.bool,
onlineProctoringRules: PropTypes.string,
discussionEnabled: PropTypes.bool.isRequired,
}).isRequired,
isXBlockComponent: PropTypes.bool,
isSelfPaced: PropTypes.bool.isRequired,
};
export default ConfigureModal;

View File

@@ -44,6 +44,7 @@ const renderComponent = () => render(
onClose={onCloseMock}
onConfigureSubmit={onConfigureSubmitMock}
currentItemData={currentSectionMock}
isSelfPaced={false}
/>
</IntlProvider>,
</AppProvider>,
@@ -85,7 +86,7 @@ describe('<ConfigureModal /> for Section', () => {
});
});
const renderSubsectionComponent = () => render(
const renderSubsectionComponent = (props) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<ConfigureModal
@@ -93,6 +94,8 @@ const renderSubsectionComponent = () => render(
onClose={onCloseMock}
onConfigureSubmit={onConfigureSubmitMock}
currentItemData={currentSubsectionMock}
isSelfPaced={false}
{...props}
/>
</IntlProvider>,
</AppProvider>,
@@ -129,6 +132,14 @@ describe('<ConfigureModal /> for Subsection', () => {
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
});
it('hides release and due dates for self paced courses', () => {
const { queryByText } = renderSubsectionComponent({ isSelfPaced: true });
expect(queryByText(messages.releaseDate.defaultMessage)).not.toBeInTheDocument();
expect(queryByText(messages.releaseTimeUTC.defaultMessage)).not.toBeInTheDocument();
expect(queryByText(messages.dueDate.defaultMessage)).not.toBeInTheDocument();
expect(queryByText(messages.dueTimeUTC.defaultMessage)).not.toBeInTheDocument();
});
it('switches to the subsection Visibility tab and renders correctly', () => {
const { getByRole, getByText } = renderSubsectionComponent();
@@ -198,6 +209,7 @@ describe('<ConfigureModal /> for Unit', () => {
expect(getByText(`${currentUnitMock.displayName} settings`)).toBeInTheDocument();
expect(getByText(messages.unitVisibility.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.discussionEnabledCheckbox.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();

View File

@@ -21,12 +21,17 @@ const UnitTab = ({
isVisibleToStaffOnly,
selectedPartitionIndex,
selectedGroups,
discussionEnabled,
} = values;
const handleChange = (e) => {
const handleVisibilityChange = (e) => {
setFieldValue('isVisibleToStaffOnly', e.target.checked);
};
const handleDiscussionChange = (e) => {
setFieldValue('discussionEnabled', e.target.checked);
};
const handleSelect = (e) => {
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
setFieldValue('selectedGroups', []);
@@ -42,9 +47,9 @@ const UnitTab = ({
<>
{!isXBlockComponent && (
<>
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
<h4 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h4>
<hr />
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="unit-visibility-checkbox">
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleVisibilityChange} data-testid="unit-visibility-checkbox">
<FormattedMessage {...messages.hideFromLearners} />
</Form.Checkbox>
{showWarning && (
@@ -52,77 +57,85 @@ const UnitTab = ({
<FormattedMessage {...messages.unitVisibilityWarning} />
</Alert>
)}
<hr />
</>
)}
<Form.Group controlId="groupSelect">
<Form.Label as="legend" className="font-weight-bold">
<FormattedMessage {...messages.restrictAccessTo} />
</Form.Label>
<Form.Control
as="select"
name="groupSelect"
value={selectedPartitionIndex}
onChange={handleSelect}
data-testid="group-type-select"
>
<option value="-1" key="-1">
{userPartitionInfo.selectedPartitionIndex === -1
? intl.formatMessage(messages.unitSelectGroupType)
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
</option>
{userPartitionInfo.selectablePartitions.map((partition, index) => (
<option
key={partition.id}
value={index}
>
{partition.name}
{userPartitionInfo.selectablePartitions.length > 0 && (
<Form.Group controlId="groupSelect">
<h4 className="mt-3"><FormattedMessage {...messages.unitAccess} /></h4>
<hr />
<Form.Label as="legend" className="font-weight-bold">
<FormattedMessage {...messages.restrictAccessTo} />
</Form.Label>
<Form.Control
as="select"
name="groupSelect"
value={selectedPartitionIndex}
onChange={handleSelect}
data-testid="group-type-select"
>
<option value="-1" key="-1">
{userPartitionInfo.selectedPartitionIndex === -1
? intl.formatMessage(messages.unitSelectGroupType)
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
</option>
))}
</Form.Control>
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
<Form.Group controlId="select-groups-checkboxes">
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
<div
role="group"
className="d-flex flex-column"
data-testid="group-checkboxes"
aria-labelledby="select-groups-checkboxes"
>
{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"
/>
<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>
</Form.Group>
)}
</Form.Group>
{userPartitionInfo.selectablePartitions.map((partition, index) => (
<option
key={partition.id}
value={index}
>
{partition.name}
</option>
))}
</Form.Control>
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
<Form.Group controlId="select-groups-checkboxes">
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
<div
role="group"
className="d-flex flex-column"
data-testid="group-checkboxes"
aria-labelledby="select-groups-checkboxes"
>
{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"
/>
<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>
</Form.Group>
)}
</Form.Group>
)}
<h4 className="mt-4"><FormattedMessage {...messages.discussionEnabledSectionTitle} /></h4>
<hr />
<Form.Checkbox checked={discussionEnabled} onChange={handleDiscussionChange}>
<FormattedMessage {...messages.discussionEnabledCheckbox} />
</Form.Checkbox>
<p className="x-small font-weight-bold"><FormattedMessage {...messages.discussionEnabledDescription} /></p>
</>
);
};
@@ -135,6 +148,7 @@ UnitTab.propTypes = {
isXBlockComponent: PropTypes.bool,
values: PropTypes.shape({
isVisibleToStaffOnly: PropTypes.bool.isRequired,
discussionEnabled: PropTypes.bool.isRequired,
selectedPartitionIndex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,

View File

@@ -42,6 +42,22 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-visibility',
defaultMessage: 'Unit visibility',
},
unitAccess: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-access',
defaultMessage: 'Unit access',
},
discussionEnabledSectionTitle: {
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.section-title',
defaultMessage: 'Discussion',
},
discussionEnabledCheckbox: {
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.checkbox',
defaultMessage: 'Enable discussion',
},
discussionEnabledDescription: {
id: 'course-authoring.course-outline.configure-modal.discussion-enabled.description',
defaultMessage: 'Topics for unpublished units will not be created',
},
hideFromLearners: {
id: 'course-authoring.course-outline.configure-modal.visibility.hide-from-learners',
defaultMessage: 'Hide from learners',