Skip to content

Commit 35e8675

Browse files
committed
fixup! feat: subsection settings
1 parent 38c28d5 commit 35e8675

6 files changed

Lines changed: 190 additions & 8 deletions

File tree

src/course-outline/data/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export interface ConfigureSubsectionData {
111111
examReviewRules?: string,
112112
defaultTimeLimitMin?: number,
113113
hideAfterDue: boolean,
114-
showCorrectness: string,
114+
showCorrectness: "always" | "never" | "past_due" | "never_but_include_grade",
115115
isPrereq?: boolean,
116116
prereqUsageKey?: string,
117117
prereqMinScore?: number,

src/course-outline/outline-sidebar/info-sidebar/SubsectionSettings.tsx

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { FormattedMessage, useIntl } from "@edx/frontend-platform/i18n";
22
import { Button, ButtonGroup, Form, Stack } from "@openedx/paragon";
33
import { useConfigureSubsection, useCourseDetails, useCourseItemData } from "@src/course-outline/data/apiHooks";
4+
import { getProctoredExamsFlag, getTimedExamsFlag } from "@src/course-outline/data/selectors";
45
import { ConfigureSubsectionData } from "@src/course-outline/data/types";
56
import { useOutlineSidebarContext } from "@src/course-outline/outline-sidebar/OutlineSidebarContext";
67
import { useCourseAuthoringContext } from "@src/CourseAuthoringContext";
78
import { VisibilityTypes } from "@src/data/constants";
9+
import AdvancedTab from "@src/generic/configure-modal/AdvancedTab";
810
import { DatepickerControl, DATEPICKER_TYPES } from "@src/generic/datepicker-control";
911
import { SidebarContent, SidebarSection } from "@src/generic/sidebar"
1012
import { useStateWithCallback } from "@src/hooks";
11-
import { FormEvent, useState } from "react";
13+
import { useState } from "react";
14+
import { useSelector } from "react-redux";
1215
import messages from './messages';
1316

1417
interface Props {
@@ -102,7 +105,7 @@ const GradingSection = ({ subsectionId, onChange }: SubProps) => {
102105
</Button>
103106
</ButtonGroup>
104107
{graded &&
105-
<Form.Group>
108+
<Form.Group className="mt-2">
106109
<Form.Label className="x-small">
107110
<FormattedMessage {...messages.subsectionGradingDropdownLabel} />
108111
</Form.Label>
@@ -120,7 +123,7 @@ const GradingSection = ({ subsectionId, onChange }: SubProps) => {
120123
</Form.Group>
121124
}
122125
{!courseDetails?.selfPaced && graded &&
123-
<Stack className="mt-3" direction="horizontal" gap={3}>
126+
<Stack direction="horizontal" gap={3}>
124127
<DatepickerControl
125128
type={DATEPICKER_TYPES.date}
126129
value={localState?.dueDate}
@@ -142,6 +145,144 @@ const GradingSection = ({ subsectionId, onChange }: SubProps) => {
142145
);
143146
}
144147

148+
const VisibilitySection = ({ subsectionId, onChange }: SubProps) => {
149+
const intl = useIntl();
150+
const { data: itemData } = useCourseItemData(subsectionId);
151+
const [localState, setLocalState] = useStateWithCallback<Partial<ConfigureSubsectionData>>(
152+
{
153+
isVisibleToStaffOnly: itemData?.visibilityState === VisibilityTypes.STAFF_ONLY,
154+
hideAfterDue: itemData?.hideAfterDue,
155+
},
156+
(val) => onChange(val || {})
157+
);
158+
159+
return (
160+
<SidebarSection
161+
title={intl.formatMessage(messages.subsectionGradingTitle)}
162+
>
163+
<ButtonGroup toggle>
164+
<Button
165+
variant={localState?.isVisibleToStaffOnly ? 'outline-primary' : 'primary'}
166+
onClick={() => setLocalState({ ...localState, isVisibleToStaffOnly: false })}
167+
>
168+
<FormattedMessage {...messages.subsectionVisibilityStudentVisible} />
169+
</Button>
170+
<Button
171+
variant={localState?.isVisibleToStaffOnly ? 'primary' : 'outline-primary'}
172+
onClick={() => setLocalState({
173+
...localState,
174+
isVisibleToStaffOnly: true,
175+
hideAfterDue: false,
176+
})}
177+
>
178+
<FormattedMessage {...messages.subsectionVisibilityStaffOnly} />
179+
</Button>
180+
</ButtonGroup>
181+
{!localState?.isVisibleToStaffOnly && <Form.Checkbox
182+
checked={localState?.hideAfterDue}
183+
className="mt-2"
184+
onChange={ (e) => setLocalState({
185+
...localState,
186+
hideAfterDue: e.target.checked,
187+
isVisibleToStaffOnly: false,
188+
}) }
189+
>
190+
<FormattedMessage {...messages.subsectionVisibilityHideAfterDueLabel} />
191+
</Form.Checkbox>}
192+
</SidebarSection>
193+
);
194+
}
195+
196+
const AssessmentResultVisibilitySection = ({ subsectionId, onChange }: SubProps) => {
197+
const intl = useIntl();
198+
const { data: itemData } = useCourseItemData(subsectionId);
199+
const [localState, setLocalState] = useStateWithCallback<Partial<ConfigureSubsectionData>>(
200+
{
201+
showCorrectness: itemData?.showCorrectness,
202+
},
203+
(val) => onChange(val || {})
204+
);
205+
206+
return (
207+
<SidebarSection
208+
title={intl.formatMessage(messages.subsectionAssessmentResultsTitle)}
209+
>
210+
<ButtonGroup toggle>
211+
<Button
212+
variant={localState?.showCorrectness === "always" ? 'primary' : 'outline-primary'}
213+
onClick={() => setLocalState({ showCorrectness: "always" })}
214+
>
215+
<FormattedMessage {...messages.subsectionAssessmentResultsShowBtn} />
216+
</Button>
217+
<Button
218+
variant={["never", "past_due"].includes(localState?.showCorrectness || "") ? 'primary' : 'outline-primary'}
219+
onClick={() => {
220+
if (localState?.showCorrectness === "always") {
221+
setLocalState({ showCorrectness: "never" })
222+
}
223+
}}
224+
>
225+
<FormattedMessage {...messages.subsectionAssessmentResultsHideBtn} />
226+
</Button>
227+
</ButtonGroup>
228+
<Form.Checkbox
229+
checked={localState?.showCorrectness === "past_due"}
230+
className="mt-2"
231+
onChange={() => setLocalState({ showCorrectness: "past_due" })}
232+
>
233+
<FormattedMessage {...messages.subsectionAssessmentResultsCheckbox} />
234+
</Form.Checkbox>
235+
</SidebarSection>
236+
);
237+
}
238+
239+
const SpecialExamSection = ({ subsectionId, onChange }: SubProps) => {
240+
const intl = useIntl();
241+
const { data: itemData } = useCourseItemData(subsectionId);
242+
const enableTimedExams = useSelector(getTimedExamsFlag);
243+
const enableProctoredExams = useSelector(getProctoredExamsFlag);
244+
const [localState, setLocalState] = useStateWithCallback<Partial<ConfigureSubsectionData>>(
245+
{
246+
showCorrectness: itemData?.showCorrectness,
247+
},
248+
(val) => onChange(val || {})
249+
);
250+
251+
const setFieldValue = (key: keyof ConfigureSubsectionData, value: any) => {
252+
setLocalState({
253+
...localState,
254+
[key]: value,
255+
})
256+
}
257+
258+
return (
259+
<SidebarSection
260+
title={intl.formatMessage(messages.subsectionSpecialExamTitle)}
261+
>
262+
<AdvancedTab
263+
values={{
264+
isProctoredExam: itemData?.isProctoredExam,
265+
isTimeLimited: itemData?.isTimeLimited,
266+
isOnboardingExam: itemData?.isOnboardingExam,
267+
isPracticeExam: itemData?.isPracticeExam,
268+
defaultTimeLimitMinutes: itemData?.defaultTimeLimitMinutes,
269+
examReviewRules: itemData?.examReviewRules,
270+
}}
271+
setFieldValue={setFieldValue}
272+
prereqs={itemData?.prereqs}
273+
releasedToStudents={itemData?.releasedToStudents}
274+
wasExamEverLinkedWithExternal={itemData?.wasExamEverLinkedWithExternal}
275+
enableProctoredExams={enableProctoredExams}
276+
enableTimedExams={enableTimedExams}
277+
supportsOnboarding={itemData?.supportsOnboarding}
278+
showReviewRules={itemData?.showReviewRules}
279+
wasProctoredExam={itemData?.isProctoredExam}
280+
onlineProctoringRules={itemData?.onlineProctoringRules}
281+
/>
282+
</SidebarSection>
283+
);
284+
}
285+
145286
export const SubsectionSettings = ({ subsectionId }: Props) => {
146287
const { courseId } = useCourseAuthoringContext();
147288
const { data: courseDetails } = useCourseDetails(courseId);
@@ -186,6 +327,18 @@ export const SubsectionSettings = ({ subsectionId }: Props) => {
186327
subsectionId={subsectionId}
187328
onChange={onChange}
188329
/>
330+
<VisibilitySection
331+
subsectionId={subsectionId}
332+
onChange={onChange}
333+
/>
334+
<AssessmentResultVisibilitySection
335+
subsectionId={subsectionId}
336+
onChange={onChange}
337+
/>
338+
<SpecialExamSection
339+
subsectionId={subsectionId}
340+
onChange={onChange}
341+
/>
189342
</SidebarContent>
190343
)
191344
}

src/course-outline/outline-sidebar/info-sidebar/messages.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,31 @@ const messages = defineMessages({
6666
defaultMessage: 'Staff Only',
6767
description: 'Visibility option for staff only in subsection settings sidebar',
6868
},
69+
subsectionVisibilityHideAfterDueLabel: {
70+
id: 'course-authoring.course-outline.sidebar.library.subsection-settings.visibility.hideAfterDue',
71+
defaultMessage: 'Hide content after due date',
72+
description: 'Hide content after due date Checkbox label',
73+
},
6974
subsectionAssessmentResultsTitle: {
7075
id: 'course-authoring.course-outline.sidebar.library.subsection-settings.assessment-results.title',
7176
defaultMessage: 'Assessment Results Visibility',
7277
description: 'Subsection Assessment Results Visibility section title in subsection settings sidebar',
7378
},
79+
subsectionAssessmentResultsShowBtn: {
80+
id: 'course-authoring.course-outline.sidebar.library.subsection-settings.assessment-results.show-btn',
81+
defaultMessage: 'Show',
82+
description: 'Subsection Assessment Results Visibility section show button text in subsection settings sidebar',
83+
},
84+
subsectionAssessmentResultsHideBtn: {
85+
id: 'course-authoring.course-outline.sidebar.library.subsection-settings.assessment-results.hide-btn',
86+
defaultMessage: 'Hide',
87+
description: 'Subsection Assessment Results Visibility section hide button text in subsection settings sidebar',
88+
},
89+
subsectionAssessmentResultsCheckbox: {
90+
id: 'course-authoring.course-outline.sidebar.library.subsection-settings.assessment-results.checkbox',
91+
defaultMessage: 'Only show results after due date',
92+
description: 'Subsection Assessment Results Visibility section checkbox text in subsection settings sidebar',
93+
},
7494
subsectionSpecialExamTitle: {
7595
id: 'course-authoring.course-outline.sidebar.library.subsection-settings.special-exam.title',
7696
defaultMessage: 'Set as Special Exam',

src/data/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export interface XBlockBase {
9797
actions: XBlockActions;
9898
explanatoryMessage?: string;
9999
userPartitions: UserPartitionTypes[];
100-
showCorrectness: string;
100+
showCorrectness: "always" | "never" | "past_due" | "never_but_include_grade",
101101
highlights: string[];
102102
highlightsEnabled: boolean;
103103
highlightsPreviewOnly: boolean;

src/generic/configure-modal/AdvancedTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import messages from './messages';
1414
import PrereqSettings from './PrereqSettings';
1515

1616
interface ValuesProps {
17-
isTimeLimited: boolean;
17+
isTimeLimited?: boolean;
1818
defaultTimeLimitMinutes?: number;
1919
isPrereq?: boolean;
2020
prereqUsageKey?: string;

src/hooks.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from 'react';
1010
import { history } from '@edx/frontend-platform';
1111
import { useLocation, useSearchParams } from 'react-router-dom';
12+
import { isEqual } from 'lodash';
1213

1314
export const useScrollToHashElement = ({ isLoading }: { isLoading: boolean }) => {
1415
const [elementWithHash, setElementWithHash] = useState<string | null>(null);
@@ -241,8 +242,16 @@ export function useStateWithCallback<T>(
241242
): [T | undefined, (val?: T) => void] {
242243
const [value, setValue] = useState<T | undefined>(defaultValue);
243244
const setValueWithCallback = useCallback((val?: T) => {
244-
setValue(val);
245-
callback?.(val)
245+
let hasChanged = false;
246+
setValue((prev) => {
247+
if (!isEqual(val, prev)) {
248+
hasChanged = true;
249+
}
250+
return val;
251+
});
252+
if (hasChanged) {
253+
callback?.(val)
254+
}
246255
}, [callback]);
247256
return [value, setValueWithCallback];
248257
}

0 commit comments

Comments
 (0)