Skip to content

Commit 806135e

Browse files
authored
feat: settings tab in sidebar (#2968)
1 parent 646d9ee commit 806135e

55 files changed

Lines changed: 3301 additions & 909 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/course-outline/data/api.ts

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
22
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
3+
import { PUBLISH_TYPES } from '@src/course-unit/constants';
34
import { XBlock } from '@src/data/types';
45
import {
56
CourseOutline,
@@ -13,6 +14,10 @@ import {
1314

1415
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
1516

17+
const pickDefined = <T extends Record<string, any>>(obj: T) => Object.fromEntries(
18+
Object.entries(obj).filter(([, value]) => value !== undefined),
19+
);
20+
1621
export const getCourseOutlineIndexApiUrl = (
1722
courseId: string,
1823
) => `${getApiBaseUrl()}/api/contentstore/v1/course_index/${courseId}`;
@@ -238,49 +243,89 @@ export async function configureCourseSection(variables: ConfigureSectionData): P
238243
/**
239244
* Configure course subsection
240245
*/
241-
export async function configureCourseSubsection(variables: ConfigureSubsectionData): Promise<object> {
246+
export async function configureCourseSubsection(
247+
variables: Partial<ConfigureSubsectionData> & Pick<ConfigureSubsectionData, 'itemId'>,
248+
): Promise<object> {
249+
const {
250+
itemId,
251+
isVisibleToStaffOnly,
252+
dueDate,
253+
hideAfterDue,
254+
showCorrectness,
255+
isPracticeExam,
256+
isTimeLimited,
257+
isProctoredExam,
258+
isOnboardingExam,
259+
examReviewRules,
260+
defaultTimeLimitMinutes,
261+
releaseDate,
262+
graderType,
263+
isPrereq,
264+
prereqUsageKey,
265+
prereqMinScore,
266+
prereqMinCompletion,
267+
} = variables;
268+
269+
const metadata = pickDefined({
270+
visible_to_staff_only: (() => {
271+
if (isVisibleToStaffOnly === undefined) {
272+
return undefined;
273+
}
274+
return isVisibleToStaffOnly ? true : null;
275+
})(),
276+
due: dueDate,
277+
hide_after_due: hideAfterDue,
278+
show_correctness: showCorrectness,
279+
is_practice_exam: isPracticeExam,
280+
is_time_limited: isTimeLimited,
281+
is_proctored_enabled: (
282+
isProctoredExam !== undefined || isPracticeExam !== undefined || isOnboardingExam !== undefined
283+
)
284+
? (isProctoredExam || isPracticeExam || isOnboardingExam)
285+
: undefined,
286+
exam_review_rules: examReviewRules,
287+
default_time_limit_minutes: defaultTimeLimitMinutes,
288+
is_onboarding_exam: isOnboardingExam,
289+
start: releaseDate,
290+
});
291+
292+
const body = pickDefined({
293+
publish: 'republish',
294+
graderType,
295+
isPrereq,
296+
prereqUsageKey,
297+
prereqMinScore,
298+
prereqMinCompletion,
299+
metadata,
300+
});
301+
242302
const { data } = await getAuthenticatedHttpClient()
243-
.post(getCourseItemApiUrl(variables.itemId), {
244-
publish: 'republish',
245-
graderType: variables.graderType,
246-
isPrereq: variables.isPrereq,
247-
prereqUsageKey: variables.prereqUsageKey,
248-
prereqMinScore: variables.prereqMinScore,
249-
prereqMinCompletion: variables.prereqMinCompletion,
250-
metadata: {
251-
// The backend expects metadata.visible_to_staff_only to either true or null
252-
visible_to_staff_only: variables.isVisibleToStaffOnly ? true : null,
253-
due: variables.dueDate,
254-
hide_after_due: variables.hideAfterDue,
255-
show_correctness: variables.showCorrectness,
256-
is_practice_exam: variables.isPracticeExam,
257-
is_time_limited: variables.isTimeLimited,
258-
is_proctored_enabled: variables.isProctoredExam || variables.isPracticeExam || variables.isOnboardingExam,
259-
exam_review_rules: variables.examReviewRules,
260-
default_time_limit_minutes: variables.defaultTimeLimitMin,
261-
is_onboarding_exam: variables.isOnboardingExam,
262-
start: variables.releaseDate,
263-
},
264-
});
303+
.post(getCourseItemApiUrl(itemId), body);
304+
265305
return data;
266306
}
267307

268308
/**
269309
* Configure course unit
270310
*/
271311
export async function configureCourseUnit(variables: ConfigureUnitData): Promise<object> {
272-
const { data } = await getAuthenticatedHttpClient()
273-
.post(getCourseItemApiUrl(variables.unitId), {
274-
publish: 'republish',
312+
const body = {
313+
publish: variables.groupAccess ? null : variables.type,
314+
...(variables.type === PUBLISH_TYPES.republish ? {
275315
metadata: {
276-
// The backend expects metadata.visible_to_staff_only to either true or null
277316
visible_to_staff_only: variables.isVisibleToStaffOnly ? true : null,
278-
group_access: variables.groupAccess,
279-
discussion_enabled: variables.discussionEnabled,
317+
...(variables.discussionEnabled !== undefined && {
318+
discussion_enabled: variables.discussionEnabled,
319+
}),
320+
...(variables.groupAccess != null && { group_access: variables.groupAccess }),
280321
},
281-
});
322+
} : {}),
323+
};
324+
const url = getCourseItemApiUrl(variables.unitId);
325+
const { data } = await getAuthenticatedHttpClient()
326+
.post(url, body);
282327

283-
return data;
328+
return camelCaseObject(data);
284329
}
285330

286331
/**

src/course-outline/data/apiHooks.ts

Lines changed: 45 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import {
66
ConfigureUnitData,
77
StaticFileNotices,
88
} from '@src/course-outline/data/types';
9-
import { useToastContext } from '@src/generic/toast-context';
10-
import { NOTIFICATION_MESSAGES } from '@src/constants';
9+
import { getNotificationMessage } from '@src/course-unit/data/utils';
1110
import { createGlobalState } from '@src/data/apiHooks';
1211
import type { XBlockBase, XblockChildInfo } from '@src/data/types';
13-
import { getBlockType, getCourseKey } from '@src/generic/key-utils';
12+
import {
13+
ContainerType, getBlockType, getCourseKey, normalizeContainerType,
14+
} from '@src/generic/key-utils';
15+
import { useMutationWithProcessingNotification } from '@src/generic/processing-notification/data/apiHooks';
1416
import { handleResponseErrors } from '@src/generic/saving-error-alert';
17+
import { useToastContext } from '@src/generic/toast-context';
1518
import { ParentIds } from '@src/generic/types';
1619
import {
1720
QueryClient,
@@ -85,7 +88,7 @@ export const useScrollState = createGlobalState<ScrollState>(courseOutlineQueryK
8588
* 1. If sectionId exists, invalidate section data which also updates all children block data
8689
* 2. Else If subsectionId exists, invalidate subsection data
8790
*/
88-
const invalidateParentQueries = async (queryClient: QueryClient, variables: ParentIds) => {
91+
export const invalidateParentQueries = async (queryClient: QueryClient, variables: ParentIds) => {
8992
if (variables.sectionId) {
9093
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.sectionId) });
9194
} else if (variables.subsectionId) {
@@ -108,20 +111,12 @@ export const useCreateCourseBlock = (
108111
courseKey: string,
109112
callback?: ((locator: string, parentLocator: string) => Promise<void>),
110113
) => {
111-
const {
112-
showToast,
113-
closeToast,
114-
} = useToastContext();
115114
const queryClient = useQueryClient();
116115
const { setData } = useScrollState(courseKey);
117116
const dispatch = useDispatch();
118-
return useMutation({
117+
return useMutationWithProcessingNotification({
119118
mutationFn: (variables: CreateCourseXBlockMutationProps) => createCourseXblock(variables),
120-
onMutate: () => {
121-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
122-
},
123119
onSuccess: async (data: { locator: string; }, variables) => {
124-
closeToast();
125120
await callback?.(data.locator, variables.parentLocator);
126121
queryClient.invalidateQueries({
127122
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(data.locator)),
@@ -136,9 +131,6 @@ export const useCreateCourseBlock = (
136131
dispatch(addSection(newBlock));
137132
}
138133
},
139-
onError: () => {
140-
closeToast();
141-
},
142134
});
143135
};
144136

@@ -198,7 +190,7 @@ export const useCourseDetails = (courseId?: string, enabled: boolean = true) =>
198190
*/
199191
export const useUpdateCourseBlockName = (courseId: string) => {
200192
const queryClient = useQueryClient();
201-
return useMutation({
193+
return useMutationWithProcessingNotification({
202194
mutationFn: (variables:{
203195
itemId: string;
204196
displayName: string;
@@ -213,7 +205,7 @@ export const useUpdateCourseBlockName = (courseId: string) => {
213205

214206
export const usePublishCourseItem = () => {
215207
const queryClient = useQueryClient();
216-
return useMutation({
208+
return useMutationWithProcessingNotification({
217209
mutationFn: (variables:{
218210
itemId: string;
219211
} & ParentIds) => publishCourseItem(variables.itemId),
@@ -226,7 +218,7 @@ export const usePublishCourseItem = () => {
226218

227219
export const useDeleteCourseItem = () => {
228220
const queryClient = useQueryClient();
229-
return useMutation({
221+
return useMutationWithProcessingNotification({
230222
mutationFn: (variables:{
231223
itemId: string;
232224
} & ParentIds) => deleteCourseItem(variables.itemId),
@@ -239,17 +231,9 @@ export const useDeleteCourseItem = () => {
239231

240232
export const useConfigureSection = () => {
241233
const queryClient = useQueryClient();
242-
const {
243-
showToast,
244-
closeToast,
245-
} = useToastContext();
246-
return useMutation({
234+
return useMutationWithProcessingNotification({
247235
mutationFn: (variables: ConfigureSectionData & ParentIds) => configureCourseSection(variables),
248-
onMutate: () => {
249-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
250-
},
251236
onSettled: (_data, _err, variables) => {
252-
closeToast();
253237
queryClient.invalidateQueries({
254238
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.sectionId)),
255239
});
@@ -260,58 +244,61 @@ export const useConfigureSection = () => {
260244

261245
export const useConfigureSubsection = () => {
262246
const queryClient = useQueryClient();
263-
const {
264-
showToast,
265-
closeToast,
266-
} = useToastContext();
267-
return useMutation({
268-
mutationFn: (variables: ConfigureSubsectionData & ParentIds) => configureCourseSubsection(variables),
269-
onMutate: () => {
270-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
271-
},
272-
onSettled: (_data, _err, variables) => {
273-
closeToast();
274-
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.itemId)) });
247+
return useMutationWithProcessingNotification({
248+
mutationFn: (
249+
variables: Partial<ConfigureSubsectionData> & Pick<ConfigureSubsectionData, 'itemId'> & ParentIds,
250+
) => configureCourseSubsection(variables),
251+
onSettled: async (_data, _err, variables) => {
252+
const courseKey = getCourseKey(variables.itemId);
253+
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(courseKey) });
275254
invalidateParentQueries(queryClient, variables).catch((e) => handleResponseErrors(e));
255+
if (variables.isPrereq !== undefined) {
256+
const subsectionItemQueries = queryClient.getQueryCache().findAll({
257+
predicate: (query) => {
258+
const { queryKey } = query;
259+
return Array.isArray(queryKey)
260+
&& queryKey.length >= 3
261+
&& queryKey[0] === courseOutlineQueryKeys.all[0]
262+
&& queryKey[1] === courseKey
263+
&& typeof queryKey[2] === 'string'
264+
&& normalizeContainerType(getBlockType(queryKey[2], 'empty')) === ContainerType.Subsection;
265+
},
266+
});
267+
await Promise.all(subsectionItemQueries.map((query) => queryClient.invalidateQueries({
268+
queryKey: query.queryKey,
269+
})));
270+
}
276271
},
277272
});
278273
};
279274

280275
export const useConfigureUnit = () => {
281276
const queryClient = useQueryClient();
282-
const {
283-
showToast,
284-
closeToast,
285-
} = useToastContext();
277+
const { showToast, closeToast } = useToastContext();
278+
// We are not using useMutationWithProcessingNotification to set custom processing notification message
286279
return useMutation({
287280
mutationFn: (variables: ConfigureUnitData & ParentIds) => configureCourseUnit(variables),
288-
onMutate: () => {
289-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
281+
onMutate: (variables) => {
282+
const msg = getNotificationMessage(variables.type, variables.isVisibleToStaffOnly, true);
283+
// Show processing notification
284+
showToast(msg, undefined, 15000);
290285
},
291286
onSettled: (_data, _err, variables) => {
292-
closeToast();
293287
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.unitId)) });
294288
invalidateParentQueries(queryClient, variables).catch((e) => handleResponseErrors(e));
289+
closeToast();
295290
},
296291
});
297292
};
298293

299294
export const useUpdateCourseSectionHighlights = () => {
300-
const {
301-
showToast,
302-
closeToast,
303-
} = useToastContext();
304295
const queryClient = useQueryClient();
305-
return useMutation({
296+
return useMutationWithProcessingNotification({
306297
mutationFn: (variables: {
307298
sectionId: string;
308299
highlights: string[];
309300
} & ParentIds) => updateCourseSectionHighlights(variables.sectionId, variables.highlights),
310-
onMutate: () => {
311-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
312-
},
313301
onSettled: (_data, _err, variables) => {
314-
closeToast();
315302
queryClient.invalidateQueries({
316303
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.sectionId)),
317304
});
@@ -321,21 +308,14 @@ export const useUpdateCourseSectionHighlights = () => {
321308
};
322309

323310
export const useDuplicateItem = (courseKey: string) => {
324-
const {
325-
showToast,
326-
closeToast,
327-
} = useToastContext();
328311
const queryClient = useQueryClient();
329312
const dispatch = useDispatch();
330313
const { setData } = useScrollState(courseKey);
331-
return useMutation({
314+
return useMutationWithProcessingNotification({
332315
mutationFn: (variables: {
333316
itemId: string;
334317
parentId: string;
335318
} & ParentIds) => duplicateCourseItem(variables.itemId, variables.parentId),
336-
onMutate: () => {
337-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
338-
},
339319
onSuccess: async (data, variables) => {
340320
await invalidateParentQueries(queryClient, variables);
341321
// add duplicated section to store, subsection and unit are handled by invalidateParentQueries
@@ -346,9 +326,6 @@ export const useDuplicateItem = (courseKey: string) => {
346326
// scroll to newly added block
347327
setData({ id: data.locator });
348328
},
349-
onSettled: () => {
350-
closeToast();
351-
},
352329
});
353330
};
354331

@@ -362,29 +339,19 @@ export const usePasteFileNotices = createGlobalState<StaticFileNotices>(
362339
);
363340

364341
export const usePasteItem = (courseId?: string) => {
365-
const {
366-
showToast,
367-
closeToast,
368-
} = useToastContext();
369342
const queryClient = useQueryClient();
370343
const { setData: setScrollState } = useScrollState(courseId);
371344
const { setData } = usePasteFileNotices(courseId);
372-
return useMutation({
345+
return useMutationWithProcessingNotification({
373346
mutationFn: (variables: {
374347
parentLocator: string;
375348
} & ParentIds) => pasteBlock(variables.parentLocator),
376-
onMutate: () => {
377-
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
378-
},
379349
onSuccess: async (data, variables) => {
380350
await invalidateParentQueries(queryClient, variables);
381351
// set pasteFileNotices
382352
setData(data.staticFileNotices);
383353
// scroll to pasted block
384354
setScrollState({ id: data.locator });
385355
},
386-
onSettled: () => {
387-
closeToast();
388-
},
389356
});
390357
};

0 commit comments

Comments
 (0)