Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6b75429
feat: unit settings tab in outline
navinkarkera Feb 3, 2026
c73592f
fix: lint issues
navinkarkera Mar 13, 2026
de603d0
fix: settings tab lost on resize
navinkarkera Mar 13, 2026
6589d41
refactor: create global context to show processing notification
navinkarkera Mar 16, 2026
282f413
refactor: create wrapper to show and hide notification
navinkarkera Mar 16, 2026
eb1d263
refactor: use single mutation for unit settings
navinkarkera Mar 16, 2026
9f8cf07
refactor: simplify and merge duplicate unit configure options
navinkarkera Mar 16, 2026
2f35b4f
feat: subsection settings
navinkarkera Mar 27, 2026
dc80c6b
fixup! feat: subsection settings
navinkarkera Mar 30, 2026
7437997
fixup! feat: subsection settings
navinkarkera Mar 30, 2026
a451570
refactor: improve response of unit settings
navinkarkera Mar 30, 2026
e4d319b
feat: section settings
navinkarkera Mar 31, 2026
f98c070
refactor: convert highlights modal component to typescript
navinkarkera Apr 1, 2026
cb84690
feat: highlights card in section sidebar
navinkarkera Apr 2, 2026
5e5494d
test: highlights modal
navinkarkera Apr 2, 2026
b1a76cd
fix: lint isssues
navinkarkera Apr 2, 2026
80fed93
fix: lint issues
navinkarkera Apr 2, 2026
0fc2392
fixup! fix: lint issues
navinkarkera Apr 2, 2026
7270d10
test: fix typing
navinkarkera Apr 2, 2026
d459c08
fix: oxlint warnings
navinkarkera Apr 2, 2026
66db837
test: section settings
navinkarkera Apr 2, 2026
f457cf7
test: subsection settings
navinkarkera Apr 2, 2026
ee779ee
fix: lint issues
navinkarkera Apr 2, 2026
9b79bf9
fix: unrelated lint issues
navinkarkera Apr 2, 2026
b20509e
test: fix
navinkarkera Apr 2, 2026
5f9442b
test: adjust UnitInfoSidebar to include PublishControls in Info tab f…
navinkarkera Apr 2, 2026
6fe15b5
test: fix some failing tests
navinkarkera Apr 2, 2026
06038c1
test: fix all tests
navinkarkera Apr 3, 2026
f6e9d97
fix: lint issues
navinkarkera Apr 3, 2026
6e4a438
feat: show confirmation modal if changing from staff to student visible
navinkarkera Apr 3, 2026
f9461a9
test: advancetab
navinkarkera Apr 3, 2026
a28b7ea
fix: failing test
navinkarkera Apr 3, 2026
df741fd
fix: lint issues
navinkarkera Apr 3, 2026
4a86b2f
test: improve coverage
navinkarkera Apr 3, 2026
cab1c5c
test: unit info sidebar
navinkarkera Apr 3, 2026
03993ab
refactor: rebase
navinkarkera Apr 6, 2026
d82b781
fix: lint issues
navinkarkera Apr 6, 2026
dbb2458
fix: rebase
navinkarkera Apr 9, 2026
cada6a7
fix: prerequisite setting in sidebar and partial updates
navinkarkera Apr 10, 2026
df98f8a
refactor: configure api and fix lint issues
navinkarkera Apr 10, 2026
b85ba7e
fix: tests
navinkarkera Apr 11, 2026
e919cf1
fix: tests
navinkarkera Apr 11, 2026
dd99aef
fix: Subsection settings data update
navinkarkera Apr 11, 2026
72edea9
fix: remove duplicate useEffect
navinkarkera Apr 11, 2026
421bd7b
fix: lint issues
navinkarkera Apr 11, 2026
d628cd1
fix: show results after due date checkbox
navinkarkera Apr 11, 2026
6080bbb
fix: typing
navinkarkera Apr 11, 2026
cc52c35
chore: add comment
navinkarkera Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/course-outline/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { PUBLISH_TYPES } from '@src/course-unit/constants';
import { XBlock } from '@src/data/types';
import {
CourseOutline,
Expand Down Expand Up @@ -257,7 +258,7 @@ export async function configureCourseSubsection(variables: ConfigureSubsectionDa
is_time_limited: variables.isTimeLimited,
is_proctored_enabled: variables.isProctoredExam || variables.isPracticeExam || variables.isOnboardingExam,
exam_review_rules: variables.examReviewRules,
default_time_limit_minutes: variables.defaultTimeLimitMin,
default_time_limit_minutes: variables.defaultTimeLimitMinutes,
is_onboarding_exam: variables.isOnboardingExam,
start: variables.releaseDate,
},
Expand All @@ -269,18 +270,23 @@ export async function configureCourseSubsection(variables: ConfigureSubsectionDa
* Configure course unit
*/
export async function configureCourseUnit(variables: ConfigureUnitData): Promise<object> {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseItemApiUrl(variables.unitId), {
publish: 'republish',
const body = {
publish: variables.groupAccess ? null : variables.type,
...(variables.type === PUBLISH_TYPES.republish ? {
metadata: {
// The backend expects metadata.visible_to_staff_only to either true or null
visible_to_staff_only: variables.isVisibleToStaffOnly ? true : null,
group_access: variables.groupAccess,
discussion_enabled: variables.discussionEnabled,
...(variables.discussionEnabled !== undefined && {
discussion_enabled: variables.discussionEnabled,
}),
...(variables.groupAccess != null && { group_access: variables.groupAccess }),
},
});
} : {}),
};
const url = getCourseItemApiUrl(variables.unitId);
const { data } = await getAuthenticatedHttpClient()
.post(url, body);

return data;
return camelCaseObject(data);
}

/**
Expand Down
93 changes: 19 additions & 74 deletions src/course-outline/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import {
ConfigureUnitData,
StaticFileNotices,
} from '@src/course-outline/data/types';
import { useToastContext } from '@src/generic/toast-context';
import { NOTIFICATION_MESSAGES } from '@src/constants';
import { getNotificationMessage } from '@src/course-unit/data/utils';
import { createGlobalState } from '@src/data/apiHooks';
import type { XBlockBase, XblockChildInfo } from '@src/data/types';
import { getBlockType, getCourseKey } from '@src/generic/key-utils';
import { useMutationWithProcessingNotification } from '@src/generic/processing-notification/data/apiHooks';
import { handleResponseErrors } from '@src/generic/saving-error-alert';
import { useToastContext } from '@src/generic/toast-context';
import { ParentIds } from '@src/generic/types';
import {
QueryClient,
Expand Down Expand Up @@ -85,7 +86,7 @@ export const useScrollState = createGlobalState<ScrollState>(courseOutlineQueryK
* 1. If sectionId exists, invalidate section data which also updates all children block data
* 2. Else If subsectionId exists, invalidate subsection data
*/
const invalidateParentQueries = async (queryClient: QueryClient, variables: ParentIds) => {
export const invalidateParentQueries = async (queryClient: QueryClient, variables: ParentIds) => {
if (variables.sectionId) {
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.sectionId) });
} else if (variables.subsectionId) {
Expand All @@ -108,20 +109,12 @@ export const useCreateCourseBlock = (
courseKey: string,
callback?: ((locator: string, parentLocator: string) => Promise<void>),
) => {
const {
showToast,
closeToast,
} = useToastContext();
const queryClient = useQueryClient();
const { setData } = useScrollState(courseKey);
const dispatch = useDispatch();
return useMutation({
return useMutationWithProcessingNotification({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean-up! 👍

mutationFn: (variables: CreateCourseXBlockMutationProps) => createCourseXblock(variables),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
},
onSuccess: async (data: { locator: string; }, variables) => {
closeToast();
await callback?.(data.locator, variables.parentLocator);
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(data.locator)),
Expand All @@ -136,9 +129,6 @@ export const useCreateCourseBlock = (
dispatch(addSection(newBlock));
}
},
onError: () => {
closeToast();
},
});
};

Expand Down Expand Up @@ -198,7 +188,7 @@ export const useCourseDetails = (courseId?: string, enabled: boolean = true) =>
*/
export const useUpdateCourseBlockName = (courseId: string) => {
const queryClient = useQueryClient();
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables:{
itemId: string;
displayName: string;
Expand All @@ -213,7 +203,7 @@ export const useUpdateCourseBlockName = (courseId: string) => {

export const usePublishCourseItem = () => {
const queryClient = useQueryClient();
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables:{
itemId: string;
} & ParentIds) => publishCourseItem(variables.itemId),
Expand All @@ -226,7 +216,7 @@ export const usePublishCourseItem = () => {

export const useDeleteCourseItem = () => {
const queryClient = useQueryClient();
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables:{
itemId: string;
} & ParentIds) => deleteCourseItem(variables.itemId),
Expand All @@ -239,17 +229,9 @@ export const useDeleteCourseItem = () => {

export const useConfigureSection = () => {
const queryClient = useQueryClient();
const {
showToast,
closeToast,
} = useToastContext();
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables: ConfigureSectionData & ParentIds) => configureCourseSection(variables),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
},
onSettled: (_data, _err, variables) => {
closeToast();
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.sectionId)),
});
Expand All @@ -260,17 +242,9 @@ export const useConfigureSection = () => {

export const useConfigureSubsection = () => {
const queryClient = useQueryClient();
const {
showToast,
closeToast,
} = useToastContext();
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables: ConfigureSubsectionData & ParentIds) => configureCourseSubsection(variables),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
},
onSettled: (_data, _err, variables) => {
closeToast();
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.itemId)) });
invalidateParentQueries(queryClient, variables).catch((e) => handleResponseErrors(e));
},
Expand All @@ -279,39 +253,30 @@ export const useConfigureSubsection = () => {

export const useConfigureUnit = () => {
const queryClient = useQueryClient();
const {
showToast,
closeToast,
} = useToastContext();
const { showToast, closeToast } = useToastContext();
return useMutation({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the useMutationWithProcessingNotification here? If not, could you please put a comment about it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we have special message to display here. i'll add a comment

mutationFn: (variables: ConfigureUnitData & ParentIds) => configureCourseUnit(variables),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
onMutate: (variables) => {
const msg = getNotificationMessage(variables.type, variables.isVisibleToStaffOnly, true);
// Show processing notification
showToast(msg, undefined, 15000);
},
onSettled: (_data, _err, variables) => {
closeToast();
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.unitId)) });
invalidateParentQueries(queryClient, variables).catch((e) => handleResponseErrors(e));
closeToast();
},
});
};

export const useUpdateCourseSectionHighlights = () => {
const {
showToast,
closeToast,
} = useToastContext();
const queryClient = useQueryClient();
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables: {
sectionId: string;
highlights: string[];
} & ParentIds) => updateCourseSectionHighlights(variables.sectionId, variables.highlights),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
},
onSettled: (_data, _err, variables) => {
closeToast();
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.sectionId)),
});
Expand All @@ -321,21 +286,14 @@ export const useUpdateCourseSectionHighlights = () => {
};

export const useDuplicateItem = (courseKey: string) => {
const {
showToast,
closeToast,
} = useToastContext();
const queryClient = useQueryClient();
const dispatch = useDispatch();
const { setData } = useScrollState(courseKey);
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables: {
itemId: string;
parentId: string;
} & ParentIds) => duplicateCourseItem(variables.itemId, variables.parentId),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
},
onSuccess: async (data, variables) => {
await invalidateParentQueries(queryClient, variables);
// add duplicated section to store, subsection and unit are handled by invalidateParentQueries
Expand All @@ -346,9 +304,6 @@ export const useDuplicateItem = (courseKey: string) => {
// scroll to newly added block
setData({ id: data.locator });
},
onSettled: () => {
closeToast();
},
});
};

Expand All @@ -362,29 +317,19 @@ export const usePasteFileNotices = createGlobalState<StaticFileNotices>(
);

export const usePasteItem = (courseId?: string) => {
const {
showToast,
closeToast,
} = useToastContext();
const queryClient = useQueryClient();
const { setData: setScrollState } = useScrollState(courseId);
const { setData } = usePasteFileNotices(courseId);
return useMutation({
return useMutationWithProcessingNotification({
mutationFn: (variables: {
parentLocator: string;
} & ParentIds) => pasteBlock(variables.parentLocator),
onMutate: () => {
showToast(NOTIFICATION_MESSAGES.saving, undefined, 15000);
},
onSuccess: async (data, variables) => {
await invalidateParentQueries(queryClient, variables);
// set pasteFileNotices
setData(data.staticFileNotices);
// scroll to pasted block
setScrollState({ id: data.locator });
},
onSettled: () => {
closeToast();
},
});
};
33 changes: 18 additions & 15 deletions src/course-outline/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { XBlock, XBlockActions } from '@src/data/types';
import { PUBLISH_TYPES } from '@src/course-unit/constants';

export interface CourseStructure {
highlightsEnabledForMessaging: boolean,
Expand Down Expand Up @@ -34,6 +35,7 @@ export interface CourseDetails {
org: string;
description?: string;
hasChanges: boolean;
selfPaced: boolean;
}

export interface ChecklistType {
Expand Down Expand Up @@ -102,25 +104,26 @@ export interface ConfigureSubsectionData {
releaseDate: string,
graderType: string,
dueDate: string,
isTimeLimited: boolean,
isProctoredExam: boolean,
isOnboardingExam: boolean,
isPracticeExam: boolean,
examReviewRules: string,
defaultTimeLimitMin: number,
hideAfterDue: string,
showCorrectness: string,
isPrereq: boolean,
prereqUsageKey: string,
prereqMinScore: number,
isTimeLimited?: boolean,
isProctoredExam?: boolean,
isOnboardingExam?: boolean,
isPracticeExam?: boolean,
examReviewRules?: string,
defaultTimeLimitMinutes?: number,
hideAfterDue: boolean,
showCorrectness: 'always' | 'never' | 'past_due' | 'never_but_include_grade',
isPrereq?: boolean,
prereqUsageKey?: string,
prereqMinScore?: number,
prereqMinCompletion: number,
}

export interface ConfigureUnitData {
unitId: string,
isVisibleToStaffOnly: boolean,
groupAccess: object,
discussionEnabled: boolean,
unitId: string;
isVisibleToStaffOnly: boolean;
type: typeof PUBLISH_TYPES[keyof typeof PUBLISH_TYPES];
groupAccess: Record<string, any> | null,
discussionEnabled?: boolean;
}

export type StaticFileNotices = {
Expand Down
Loading