refactor(course-outline): improve query cache handling and remove redux thunks (#2884)

- Centralizes and reuses query cache handling logic:
Introduces a `ParentIds` type (src/generic/types.ts) and standardizes its use across data/API hooks for updating or invalidating parent/child query caches.
- Ensures cache coherence using `cancelQueries` before updating query data:
Before calling `setQueryData` for any block, any inflight queries are cancelled to prevent race conditions and stale UI.
- Simplifies post-sync/invalidation flows:
Removes Redux thunk usages in favor of direct query invalidations using React Query APIs within course outline cards, sidebars, publish modal, and `unlinkmodal`.
- Refactors data types for clarity:
Splits XBlock into `XBlockBase` and derived interfaces so the presence of `childInfo` is explicit.
- Cleans up redundant code and props:
Removes unnecessary `memoization`, `useDispatch` imports, and duplicate logic in React components.
This commit is contained in:
Navin Karkera
2026-02-20 23:40:28 +05:30
committed by GitHub
parent 8f8c6d8dd2
commit ee6006e0e3
16 changed files with 141 additions and 105 deletions

View File

@@ -1,7 +1,8 @@
import { containerComparisonQueryKeys } from '@src/container-comparison/data/apiHooks';
import type { XBlock } from '@src/data/types';
import type { XBlockBase, XblockChildInfo } from '@src/data/types';
import { getCourseKey } from '@src/generic/key-utils';
import { handleResponseErrors } from '@src/generic/saving-error-alert';
import { ParentIds } from '@src/generic/types';
import {
QueryClient,
skipToken, useMutation, useQuery, useQueryClient,
@@ -41,22 +42,22 @@ export const courseOutlineQueryKeys = {
],
};
type ParentIds = {
/** This id will be used to invalidate data of parent subsection */
subsectionId?: string;
/** This id will be used to invalidate data of parent section */
sectionId?: string;
};
/**
* Invalidate parent Subsection and Section data.
*
* This function ensures that cached data for parent subsection and section is invalidated
* when child items are created, updated, or deleted.
*
* Priority:
* 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) => {
if (variables.subsectionId) {
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.subsectionId) });
}
if (variables.sectionId) {
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.sectionId) });
} else if (variables.subsectionId) {
// istanbul ignore next
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.subsectionId) });
}
};
@@ -66,6 +67,9 @@ type CreateCourseXBlockMutationProps = CreateCourseXBlockType & ParentIds;
* Hook to create an XBLOCK in a course .
* The `locator` is the ID of the parent block where this new XBLOCK should be created.
* Can also be used to import block from library by passing `libraryContentKey` in request body
*
* @param callback - Optional function called after successful creation to handle additional logic
* @returns Mutation object for creating course blocks
*/
export const useCreateCourseBlock = (
callback?: ((locator: string, parentLocator: string) => Promise<void>),
@@ -75,7 +79,6 @@ export const useCreateCourseBlock = (
mutationFn: (variables: CreateCourseXBlockMutationProps) => createCourseXblock(variables),
onSettled: async (data: { locator: string; }, _err, variables) => {
await callback?.(data.locator, variables.parentLocator);
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.parentLocator) });
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(data.locator)),
});
@@ -84,13 +87,34 @@ export const useCreateCourseBlock = (
});
};
export const useCourseItemData = <T = XBlock>(itemId?: string, initialData?: T, enabled: boolean = true) => (
useQuery({
export const useCourseItemData = <T extends XBlockBase>(itemId?: string, initialData?: T, enabled: boolean = true) => {
const queryClient = useQueryClient();
return useQuery<T>({
initialData,
queryKey: courseOutlineQueryKeys.courseItemId(itemId),
queryFn: enabled && itemId ? () => getCourseItem<T>(itemId!) : skipToken,
})
);
queryFn: enabled && itemId ? async () => {
const data = await getCourseItem<T>(itemId!);
// If the container has children blocks, update children react-query cache
// data without hitting the API as each xblock call returns its children information as well.
if ('childInfo' in data) {
// This could mean that data is of a section or subsection
(data.childInfo as XblockChildInfo).children.forEach(async (child) => {
await queryClient.cancelQueries({ queryKey: courseOutlineQueryKeys.courseItemId(child.id) });
queryClient.setQueryData(courseOutlineQueryKeys.courseItemId(child.id), child);
if ('childInfo' in child) {
// This means that the data is of section and so its children subsections also
// have children i.e. units
(child.childInfo as XblockChildInfo).children.forEach(async (grandChild) => {
await queryClient.cancelQueries({ queryKey: courseOutlineQueryKeys.courseItemId(grandChild.id) });
queryClient.setQueryData(courseOutlineQueryKeys.courseItemId(grandChild.id), grandChild);
});
}
});
}
return data;
} : skipToken,
});
};
export const useCourseDetails = (courseId?: string, enabled: boolean = true) => (
useQuery({
@@ -99,6 +123,15 @@ export const useCourseDetails = (courseId?: string, enabled: boolean = true) =>
})
);
/**
* Hook to update the display name of a course block.
*
* This mutation updates the display name of a course item and invalidates relevant cache queries
* to ensure the UI reflects the changes.
*
* @param courseId - The ID of the course containing the item
* @returns Mutation object for updating course block names
*/
export const useUpdateCourseBlockName = (courseId: string) => {
const queryClient = useQueryClient();
return useMutation({
@@ -107,10 +140,9 @@ export const useUpdateCourseBlockName = (courseId: string) => {
displayName: string;
} & ParentIds) => editItemDisplayName({ itemId: variables.itemId, displayName: variables.displayName }),
onSuccess: async (_data, variables) => {
await queryClient.invalidateQueries({ queryKey: containerComparisonQueryKeys.course(courseId) });
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(courseId) });
await queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.itemId) });
await invalidateParentQueries(queryClient, variables);
queryClient.invalidateQueries({ queryKey: containerComparisonQueryKeys.course(courseId) });
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(courseId) });
},
});
};
@@ -122,9 +154,8 @@ export const usePublishCourseItem = () => {
itemId: string;
} & ParentIds) => publishCourseItem(variables.itemId),
onSettled: (_data, _err, variables) => {
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.itemId) });
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.itemId)) });
invalidateParentQueries(queryClient, variables).catch((e) => handleResponseErrors(e));
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseDetails(getCourseKey(variables.itemId)) });
},
});
};

View File

@@ -169,24 +169,13 @@ const useCourseOutline = ({ courseId }) => {
return;
}
await unlinkDownstream(currentUnlinkModalData.value.id, {
await unlinkDownstream({
downstreamBlockId: currentUnlinkModalData.value.id,
sectionId: currentUnlinkModalData.sectionId,
subsectionId: currentUnlinkModalData.subsectionId,
}, {
onSuccess: () => {
closeUnlinkModal();
// istanbul ignore next
// refresh child block data
currentUnlinkModalData.value.childInfo?.children.forEach((block) => {
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(block.id) });
block.childInfo?.children.forEach(({ id: blockId }) => {
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(blockId) });
});
});
// refresh parent blocks data
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(currentUnlinkModalData?.sectionId),
});
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(currentUnlinkModalData?.subsectionId),
});
},
});
}, [currentUnlinkModalData, unlinkDownstream, closeUnlinkModal]);

View File

@@ -144,7 +144,7 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
setCurrentFlow(flow);
}, [setCurrentFlow, setCurrentPageKey]);
const { data: currentItemData } = useCourseItemData(selectedContainerState?.currentId);
const { data: currentItemData } = useCourseItemData<XBlock>(selectedContainerState?.currentId);
const sectionsList = useSelector(getSectionsList);
/** Stores last section that allows adding subsections inside it. */

View File

@@ -4,7 +4,6 @@ import { SchoolOutline, Tag } from '@openedx/paragon/icons';
import { ContentTagsDrawerSheet, ContentTagsSnippet } from '@src/content-tags-drawer';
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
import { courseOutlineQueryKeys, useCourseItemData } from '@src/course-outline/data/apiHooks';
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
import { useOutlineSidebarContext } from '@src/course-outline/outline-sidebar/OutlineSidebarContext';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { ComponentCountSnippet, getItemIcon } from '@src/generic/block-type-utils';
@@ -44,14 +43,14 @@ export const InfoSection = ({ itemId }: Props) => {
*/
// istanbul ignore next
const handleOnPostChangeSync = useCallback(() => {
// invalidating section data will update all children blocks as well.
if (selectedContainerState?.sectionId) {
dispatch(fetchCourseSectionQuery([selectedContainerState.sectionId]));
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(selectedContainerState?.sectionId),
});
}
if (courseId) {
invalidateLinksQuery(queryClient, courseId);
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.course(courseId),
});
}
}, [dispatch, selectedContainerState, queryClient, courseId]);

View File

@@ -1,16 +1,15 @@
/* eslint-disable import/named */
import React, { useMemo } from 'react';
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ModalDialog,
ActionRow,
} from '@openedx/paragon';
import { courseOutlineQueryKeys, usePublishCourseItem } from '@src/course-outline/data/apiHooks';
import { usePublishCourseItem } from '@src/course-outline/data/apiHooks';
import type { UnitXBlock, XBlock } from '@src/data/types';
import LoadingButton from '@src/generic/loading-button';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useQueryClient } from '@tanstack/react-query';
import messages from './messages';
import { COURSE_BLOCK_NAMES } from '../constants';
@@ -26,22 +25,6 @@ const PublishModal = () => {
: undefined;
const children: Array<XBlock | UnitXBlock> | undefined = childInfo?.children;
const publishMutation = usePublishCourseItem();
const queryClient = useQueryClient();
const childrenIds = useMemo(() => children?.reduce((
result: string[],
current: XBlock | UnitXBlock,
): string[] => {
let temp = [...result];
if ('childInfo' in current) {
const grandChildren = current.childInfo.children.filter((child) => child.hasChanges);
temp = [...temp, ...grandChildren.map((child) => child.id)];
}
if (current.hasChanges) {
temp.push(current.id);
}
return temp;
}, []), [children]);
const onPublishSubmit = async () => {
if (id) {
@@ -52,10 +35,6 @@ const PublishModal = () => {
}, {
onSettled: () => {
closePublishModal();
// Update query client to refresh the data of all children blocks
childrenIds?.forEach((blockId) => {
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(blockId) });
});
},
});
}

View File

@@ -1,7 +1,6 @@
import {
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
} from 'react';
import { useDispatch } from 'react-redux';
import {
Bubble, Button, useToggle,
} from '@openedx/paragon';
@@ -14,7 +13,6 @@ import SortableItem from '@src/course-outline/drag-helper/SortableItem';
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
import TitleButton from '@src/course-outline/card-header/TitleButton';
import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
import { ContainerType } from '@src/generic/key-utils';
@@ -60,7 +58,6 @@ const SectionCard = ({
resetScrollState,
}: SectionCardProps) => {
const currentRef = useRef(null);
const dispatch = useDispatch();
const { activeId, overId } = useContext(DragContext);
const { selectedContainerState, openContainerInfoSidebar, setSelectedContainerState } = useOutlineSidebarContext();
const [searchParams] = useSearchParams();
@@ -111,6 +108,10 @@ const SectionCard = ({
useEffect(() => {
// istanbul ignore if
if (moment(initialData.editedOnRaw).isAfter(moment(section.editedOnRaw))) {
queryClient.cancelQueries({
queryKey: courseOutlineQueryKeys.courseItemId(initialData.id),
// eslint-disable-next-line no-console
}).catch((error) => console.error('Error cancelling query:', error));
queryClient.setQueryData(courseOutlineQueryKeys.courseItemId(initialData.id), initialData);
}
}, [initialData, section]);
@@ -167,11 +168,13 @@ const SectionCard = ({
}, [locatorId, setIsExpanded]);
const handleOnPostChangeSync = useCallback(() => {
dispatch(fetchCourseSectionQuery([section.id]));
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(section.id),
});
if (courseId) {
invalidateLinksQuery(queryClient, courseId);
}
}, [dispatch, section, courseId, queryClient]);
}, [section, courseId, queryClient]);
// re-create actions object for customizations
const actions = { ...sectionActions };

View File

@@ -1,7 +1,6 @@
import {
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
} from 'react';
import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle } from '@openedx/paragon';
@@ -15,7 +14,6 @@ import SortableItem from '@src/course-outline/drag-helper/SortableItem';
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
import { useClipboard, PasteComponent } from '@src/generic/clipboard';
import TitleButton from '@src/course-outline/card-header/TitleButton';
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
import { ContainerType } from '@src/generic/key-utils';
@@ -65,7 +63,6 @@ const SubsectionCard = ({
}: SubsectionCardProps) => {
const currentRef = useRef(null);
const intl = useIntl();
const dispatch = useDispatch();
const { activeId, overId } = useContext(DragContext);
const { selectedContainerState, openContainerInfoSidebar, setSelectedContainerState } = useOutlineSidebarContext();
const [searchParams] = useSearchParams();
@@ -146,6 +143,10 @@ const SubsectionCard = ({
useEffect(() => {
// istanbul ignore if
if (moment(initialData.editedOnRaw).isAfter(moment(subsection.editedOnRaw))) {
queryClient.cancelQueries({
queryKey: courseOutlineQueryKeys.courseItemId(initialData.id),
// eslint-disable-next-line no-console
}).catch((error) => console.error('Error cancelling query:', error));
queryClient.setQueryData(courseOutlineQueryKeys.courseItemId(initialData.id), initialData);
}
}, [initialData, subsection]);
@@ -171,11 +172,13 @@ const SubsectionCard = ({
};
const handleOnPostChangeSync = useCallback(() => {
dispatch(fetchCourseSectionQuery([section.id]));
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(section.id),
});
if (courseId) {
invalidateLinksQuery(queryClient, courseId);
}
}, [dispatch, section, queryClient, courseId]);
}, [section, queryClient, courseId]);
const handleSubsectionMoveUp = () => {
onOrderChange(section, moveUpDetails);

View File

@@ -5,14 +5,12 @@ import {
useRef,
} from 'react';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import { useToggle } from '@openedx/paragon';
import { isEmpty } from 'lodash';
import { useSearchParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import CourseOutlineUnitCardExtraActionsSlot from '@src/plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
import CardHeader from '@src/course-outline/card-header/CardHeader';
import SortableItem from '@src/course-outline/drag-helper/SortableItem';
import TitleLink from '@src/course-outline/card-header/TitleLink';
@@ -61,7 +59,6 @@ const UnitCard = ({
discussionsSettings,
}: UnitCardProps) => {
const currentRef = useRef(null);
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const { selectedContainerState, openContainerInfoSidebar, setSelectedContainerState } = useOutlineSidebarContext();
const locatorId = searchParams.get('show');
@@ -162,11 +159,13 @@ const UnitCard = ({
};
const handleOnPostChangeSync = useCallback(() => {
dispatch(fetchCourseSectionQuery([section.id]));
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(section.id),
});
if (courseId) {
invalidateLinksQuery(queryClient, courseId);
}
}, [dispatch, section, queryClient, courseId]);
}, [section, queryClient, courseId]);
const onClickCard = useCallback((e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
@@ -202,6 +201,10 @@ const UnitCard = ({
useEffect(() => {
// istanbul ignore if
if (moment(initialData.editedOnRaw).isAfter(moment(unit.editedOnRaw))) {
queryClient.cancelQueries({
queryKey: courseOutlineQueryKeys.courseItemId(initialData.id),
// eslint-disable-next-line no-console
}).catch((error) => console.error('Error cancelling query:', error));
queryClient.setQueryData(courseOutlineQueryKeys.courseItemId(initialData.id), initialData);
}
}, [initialData, unit]);

View File

@@ -14,7 +14,7 @@ const expectedCourseItemDataWithUnit = {
childInfo: {
children: [
{
id: 'unitId',
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1',
},
],
},
@@ -64,7 +64,12 @@ describe('SubsectionUnitRedirect', () => {
// Confirm redirection by checking the final URL
const mockNavigate = screen.getByTestId('mock-navigate');
expect(mockNavigate).toBeInTheDocument();
expect(mockNavigate).toHaveAttribute('data-to', `/course/${courseId}/container/unitId`);
expect(mockNavigate).toHaveAttribute(
'data-to',
`/course/${courseId}/container/${encodeURIComponent(
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1',
)}`,
);
});
});

View File

@@ -2,14 +2,15 @@ import { LoadingSpinner } from '@src/generic/Loading';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { useParams, Navigate } from 'react-router-dom';
import { useCourseItemData } from '../course-outline/data/apiHooks';
import { useCourseItemData } from '@src/course-outline/data/apiHooks';
import { XBlock } from '@src/data/types';
const SubsectionUnitRedirect = () => {
const { courseId } = useCourseAuthoringContext();
let { subsectionId } = useParams();
// if the call is made via the click on breadcrumbs the re won't be courseId available
// in such cases the page should redirect to the 1st unit of he subsection
const { data: courseItemData, isLoading } = useCourseItemData(subsectionId);
const { data: courseItemData, isLoading } = useCourseItemData<XBlock>(subsectionId);
let firstUnitId = courseItemData?.childInfo?.children?.[0]?.id;
if (isLoading) {

View File

@@ -73,7 +73,8 @@ export const useCourseUnit = ({
const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useClipboard(canEdit);
const { canPasteComponent } = courseVerticalChildren;
const { displayName: unitTitle, category: unitCategory } = xblockInfo;
const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id;
const sequenceId = courseUnit.ancestorInfo?.ancestors[0]?.id;
const sectionId = courseUnit.ancestorInfo?.ancestors[1]?.id;
const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
const isUnitLegacyLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
const isSplitTestType = unitCategory === COURSE_BLOCK_NAMES.splitTest.id;
@@ -139,19 +140,26 @@ export const useCourseUnit = ({
const { mutateAsync: unlinkDownstream } = useUnlinkDownstream();
const unitXBlockActions = {
handleDelete: async (XBlockId) => {
handleDelete: async (XBlockId: string) => {
// oxlint-disable-next-line typescript-eslint(await-thenable)
await dispatch(deleteUnitItemQuery(blockId, XBlockId, sendMessageToIframe));
},
handleDuplicate: (XBlockId) => {
handleDuplicate: (XBlockId: string) => {
dispatch(duplicateUnitItemQuery(
blockId,
XBlockId,
(courseKey, locator) => sendMessageToIframe(messageTypes.completeXBlockDuplicating, { courseKey, locator }),
(courseKey: string, locator: string) => sendMessageToIframe(
messageTypes.completeXBlockDuplicating,
{ courseKey, locator },
),
));
},
handleUnlink: async (XBlockId) => {
await unlinkDownstream(XBlockId);
handleUnlink: async (XBlockId: string) => {
await unlinkDownstream({
downstreamBlockId: XBlockId,
subsectionId: sequenceId,
sectionId,
});
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
},
};

View File

@@ -53,7 +53,7 @@ export const ComponentInfoSidebar = () => {
const handlePostChange = () => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(selectedComponentId),
queryKey: courseOutlineQueryKeys.courseItemId(sectionId),
});
};

View File

@@ -70,7 +70,7 @@ export interface UpstreamInfo {
isReadyToSyncIndividually?: boolean,
}
export interface XBlock {
export interface XBlockBase {
id: string;
locator: string;
usageKey: string;
@@ -102,7 +102,6 @@ export interface XBlock {
highlightsEnabled: boolean;
highlightsPreviewOnly: boolean;
highlightsDocUrl: string;
childInfo: XblockChildInfo;
ancestorHasStaffLock: boolean;
staffOnlyMessage: boolean;
hasPartitionGroupComponents: boolean;
@@ -127,7 +126,11 @@ export interface XBlock {
upstreamInfo?: UpstreamInfo;
}
export type UnitXBlock = Omit<XBlock, 'childInfo'>;
export interface XBlock extends XBlockBase {
childInfo: XblockChildInfo;
}
export interface UnitXBlock extends XBlockBase {}
interface OutlineError {
data?: string;

View File

@@ -6,16 +6,16 @@ import { Cached, LinkOff, Newsstand } from '@openedx/paragon/icons';
import { useCourseItemData } from '@src/course-outline/data/apiHooks';
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
import { XBlock } from '@src/data/types';
import type { XBlock, XBlockBase } from '@src/data/types';
import { ContainerType, getBlockType, normalizeContainerType } from '@src/generic/key-utils';
import { useToggleWithValue } from '@src/hooks';
import { useMemo } from 'react';
import messages from './messages';
interface SubProps {
blockData: XBlock;
blockData: XBlockBase;
displayName: string;
openSyncModal: (val: XBlock) => void;
openSyncModal: (val: XBlockBase) => void;
sectionId?: string;
}

View File

@@ -14,3 +14,10 @@ export interface UseIFrameBehaviorReturnTypes {
showError: boolean;
hasLoaded: boolean;
}
export type ParentIds = {
/** This id will be used to invalidate data of parent subsection */
subsectionId?: string;
/** This id will be used to invalidate data of parent section */
sectionId?: string;
};

View File

@@ -3,23 +3,28 @@ import { courseLibrariesQueryKeys } from '@src/course-libraries';
import { getCourseKey } from '@src/generic/key-utils';
import { courseOutlineQueryKeys } from '@src/course-outline/data/apiHooks';
import { ParentIds } from '@src/generic/types';
import { unlinkDownstream } from './api';
export const useUnlinkDownstream = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: unlinkDownstream,
onSuccess: (_, contentId: string) => {
const courseKey = getCourseKey(contentId);
mutationFn: (variables: {
downstreamBlockId: string;
} & ParentIds) => unlinkDownstream(variables.downstreamBlockId),
onSuccess: (_, variables) => {
const courseKey = getCourseKey(variables.downstreamBlockId);
queryClient.invalidateQueries({
queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey),
});
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseItemId(contentId),
});
queryClient.invalidateQueries({
queryKey: courseOutlineQueryKeys.courseDetails(courseKey),
});
if (variables.sectionId) {
// This will handle updating all children block cache data as section data contains
// xblock data of all of its children
queryClient.invalidateQueries({ queryKey: courseOutlineQueryKeys.courseItemId(variables.sectionId) });
}
},
});
};