feat: show sync button on section/subsections [FC-0097] (#2324)
- Adds the sync button on section/subsection cards
This commit is contained in:
@@ -19,7 +19,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Loop } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import previewChangesMessages from '../course-unit/preview-changes/messages';
|
||||
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
|
||||
import { invalidateLinksQuery, useEntityLinks } from './data/apiHooks';
|
||||
import {
|
||||
SearchContextProvider, SearchKeywordsField, useSearchContext, BlockTypeLabel, Highlight, SearchSortWidget,
|
||||
} from '../search-manager';
|
||||
@@ -189,7 +189,7 @@ const ItemReviewList = ({
|
||||
|
||||
const reloadLinks = useCallback((usageKey: string) => {
|
||||
const courseKey = outOfSyncItemsByKey[usageKey].downstreamContextKey;
|
||||
queryClient.invalidateQueries({ queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey) });
|
||||
invalidateLinksQuery(queryClient, courseKey);
|
||||
}, [outOfSyncItemsByKey]);
|
||||
|
||||
const postChange = (accept: boolean) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams-all/`;
|
||||
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`;
|
||||
export const getEntityLinksSummaryByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/${downstreamContextKey}/summary`;
|
||||
|
||||
export interface PaginatedData<T> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
type QueryClient,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { getEntityLinksSummaryByDownstreamContext, getEntityLinks } from './api';
|
||||
@@ -70,3 +71,12 @@ export const useEntityLinksSummaryByDownstreamContext = (courseId?: string) => (
|
||||
enabled: courseId !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Ivalidates the downstream links query for a course
|
||||
*/
|
||||
export const invalidateLinksQuery = (queryClient: QueryClient, courseId: string) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: courseLibrariesQueryKeys.courseLibraries(courseId),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import {
|
||||
act, fireEvent, initializeMocks, render, screen, within,
|
||||
act, fireEvent, initializeMocks, render, screen, waitFor, within,
|
||||
} from '@src/testUtils';
|
||||
import { XBlock } from '@src/data/types';
|
||||
import SectionCard from './SectionCard';
|
||||
|
||||
const mockPathname = '/foo-bar';
|
||||
const mockUseAcceptLibraryBlockChanges = jest.fn();
|
||||
const mockUseIgnoreLibraryBlockChanges = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
jest.mock('@src/course-unit/data/apiHooks', () => ({
|
||||
useAcceptLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseAcceptLibraryBlockChanges,
|
||||
}),
|
||||
useIgnoreLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseIgnoreLibraryBlockChanges,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -74,7 +77,7 @@ const section = {
|
||||
|
||||
const onEditSectionSubmit = jest.fn();
|
||||
|
||||
const renderComponent = (props?: object, entry = '/') => render(
|
||||
const renderComponent = (props?: object, entry = '/course/:courseId') => render(
|
||||
<SectionCard
|
||||
section={section}
|
||||
index={1}
|
||||
@@ -98,7 +101,8 @@ const renderComponent = (props?: object, entry = '/') => render(
|
||||
<span>children</span>
|
||||
</SectionCard>,
|
||||
{
|
||||
path: '/',
|
||||
path: '/course/:courseId',
|
||||
params: { courseId: '5' },
|
||||
routerProps: {
|
||||
initialEntries: [entry],
|
||||
},
|
||||
@@ -182,7 +186,7 @@ describe('<SectionCard />', () => {
|
||||
const collapsedSections = { ...section };
|
||||
// @ts-ignore-next-line
|
||||
collapsedSections.isSectionsExpanded = false;
|
||||
renderComponent(collapsedSections, `?show=${subsection.id}`);
|
||||
renderComponent(collapsedSections, `/course/:courseId?show=${subsection.id}`);
|
||||
|
||||
const cardSubsections = await screen.findByTestId('section-card__subsections');
|
||||
const newSubsectionButton = await screen.findByRole('button', { name: 'New subsection' });
|
||||
@@ -194,7 +198,7 @@ describe('<SectionCard />', () => {
|
||||
const collapsedSections = { ...section };
|
||||
// @ts-ignore-next-line
|
||||
collapsedSections.isSectionsExpanded = false;
|
||||
renderComponent(collapsedSections, `?show=${unit.id}`);
|
||||
renderComponent(collapsedSections, `/course/:courseId?show=${unit.id}`);
|
||||
|
||||
const cardSubsections = await screen.findByTestId('section-card__subsections');
|
||||
const newSubsectionButton = await screen.findByRole('button', { name: 'New subsection' });
|
||||
@@ -207,11 +211,58 @@ describe('<SectionCard />', () => {
|
||||
const collapsedSections = { ...section };
|
||||
// @ts-ignore-next-line
|
||||
collapsedSections.isSectionsExpanded = false;
|
||||
renderComponent(collapsedSections, `?show=${randomId}`);
|
||||
renderComponent(collapsedSections, `/course/:courseId?show=${randomId}`);
|
||||
|
||||
const cardSubsections = screen.queryByTestId('section-card__subsections');
|
||||
const newSubsectionButton = screen.queryByRole('button', { name: 'New subsection' });
|
||||
expect(cardSubsections).toBeNull();
|
||||
expect(newSubsectionButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should sync section changes from upstream', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByTestId('section-card-header')).toBeInTheDocument();
|
||||
|
||||
// Click on sync button
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
// Should open compare preview modal
|
||||
expect(screen.getByRole('heading', { name: /preview changes: section name/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
||||
|
||||
// Click on accept changes
|
||||
const acceptChangesButton = screen.getByText(/accept changes/i);
|
||||
fireEvent.click(acceptChangesButton);
|
||||
|
||||
await waitFor(() => expect(mockUseAcceptLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should decline sync section changes from upstream', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByTestId('section-card-header')).toBeInTheDocument();
|
||||
|
||||
// Click on sync button
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
// Should open compare preview modal
|
||||
expect(screen.getByRole('heading', { name: /preview changes: section name/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
||||
|
||||
// Click on ignore changes
|
||||
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
|
||||
fireEvent.click(ignoreChangesButton);
|
||||
|
||||
// Should open the confirmation modal
|
||||
expect(screen.getByRole('heading', { name: /ignore these changes\?/i })).toBeInTheDocument();
|
||||
|
||||
// Click on ignore button
|
||||
const ignoreButton = screen.getByRole('button', { name: /ignore/i });
|
||||
fireEvent.click(ignoreButton);
|
||||
|
||||
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
useContext, useEffect, useState, useRef, useCallback, ReactNode,
|
||||
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
|
||||
} from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Bubble, Button, StandardModal, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { setCurrentItem, setCurrentSection } from '@src/course-outline/data/slice';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
@@ -16,14 +17,17 @@ 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';
|
||||
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
|
||||
import { ContentType } from '@src/library-authoring/routes';
|
||||
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
|
||||
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
|
||||
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
|
||||
import type { XBlock } from '@src/data/types';
|
||||
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
|
||||
import messages from './messages';
|
||||
|
||||
interface SectionCardProps {
|
||||
@@ -79,6 +83,8 @@ const SectionCard = ({
|
||||
openAddLibrarySubsectionModal,
|
||||
closeAddLibrarySubsectionModal,
|
||||
] = useToggle(false);
|
||||
const { courseId } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Expand the section if a search result should be shown/scrolled to
|
||||
const containsSearchResult = () => {
|
||||
@@ -107,6 +113,7 @@ const SectionCard = ({
|
||||
};
|
||||
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || isSectionsExpanded);
|
||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
|
||||
const namePrefix = 'section';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -126,6 +133,19 @@ const SectionCard = ({
|
||||
upstreamInfo,
|
||||
} = section;
|
||||
|
||||
const blockSyncData = useMemo(() => {
|
||||
if (!upstreamInfo?.readyToSync) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
displayName,
|
||||
downstreamBlockId: id,
|
||||
upstreamBlockId: upstreamInfo.upstreamRef,
|
||||
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
|
||||
isContainer: true,
|
||||
};
|
||||
}, [upstreamInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeId === id && isExpanded) {
|
||||
setIsExpanded(false);
|
||||
@@ -149,6 +169,13 @@ const SectionCard = ({
|
||||
setIsExpanded((prevState) => containsSearchResult() || prevState);
|
||||
}, [locatorId, setIsExpanded]);
|
||||
|
||||
const handleOnPostChangeSync = useCallback(() => {
|
||||
dispatch(fetchCourseSectionQuery([section.id]));
|
||||
if (courseId) {
|
||||
invalidateLinksQuery(queryClient, courseId);
|
||||
}
|
||||
}, [dispatch, section, courseId, queryClient]);
|
||||
|
||||
// re-create actions object for customizations
|
||||
const actions = { ...sectionActions };
|
||||
// add actions to control display of move up & down menu buton.
|
||||
@@ -267,6 +294,7 @@ const SectionCard = ({
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSectionMoveUp}
|
||||
onClickMoveDown={handleSectionMoveDown}
|
||||
onClickSync={openSyncModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
@@ -275,6 +303,7 @@ const SectionCard = ({
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
readyToSync={upstreamInfo?.readyToSync}
|
||||
/>
|
||||
)}
|
||||
<div className="section-card__content" data-testid="section-card__content">
|
||||
@@ -330,6 +359,14 @@ const SectionCard = ({
|
||||
visibleTabs={[ContentType.subsections]}
|
||||
/>
|
||||
</StandardModal>
|
||||
{blockSyncData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
|
||||
import {
|
||||
act, fireEvent, initializeMocks, render, screen, within,
|
||||
act, fireEvent, initializeMocks, render, screen, waitFor, within,
|
||||
} from '@src/testUtils';
|
||||
import { XBlock } from '@src/data/types';
|
||||
import cardHeaderMessages from '../card-header/messages';
|
||||
import SubsectionCard from './SubsectionCard';
|
||||
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const containerKey = 'lct:org:lib:unit:1';
|
||||
const handleOnAddUnitFromLibrary = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
const mockUseAcceptLibraryBlockChanges = jest.fn();
|
||||
const mockUseIgnoreLibraryBlockChanges = jest.fn();
|
||||
|
||||
jest.mock('@src/course-unit/data/apiHooks', () => ({
|
||||
useAcceptLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseAcceptLibraryBlockChanges,
|
||||
}),
|
||||
useIgnoreLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseIgnoreLibraryBlockChanges,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -97,7 +101,7 @@ const section: XBlock = {
|
||||
|
||||
const onEditSubectionSubmit = jest.fn();
|
||||
|
||||
const renderComponent = (props?: object, entry = '/') => render(
|
||||
const renderComponent = (props?: object, entry = '/course/:courseId') => render(
|
||||
<SubsectionCard
|
||||
section={section}
|
||||
subsection={subsection}
|
||||
@@ -122,7 +126,8 @@ const renderComponent = (props?: object, entry = '/') => render(
|
||||
<span>children</span>
|
||||
</SubsectionCard>,
|
||||
{
|
||||
path: '/',
|
||||
path: '/course/:courseId',
|
||||
params: { courseId: '5' },
|
||||
routerProps: {
|
||||
initialEntries: [entry],
|
||||
},
|
||||
@@ -277,7 +282,7 @@ describe('<SubsectionCard />', () => {
|
||||
});
|
||||
|
||||
it('check extended subsection when URL "show" param in subsection', async () => {
|
||||
renderComponent(undefined, `?show=${unit.id}`);
|
||||
renderComponent(undefined, `/course/:courseId?show=${unit.id}`);
|
||||
|
||||
const cardUnits = await screen.findByTestId('subsection-card__units');
|
||||
const newUnitButton = await screen.findByRole('button', { name: 'New unit' });
|
||||
@@ -287,7 +292,7 @@ describe('<SubsectionCard />', () => {
|
||||
|
||||
it('check not extended subsection when URL "show" param not in subsection', async () => {
|
||||
const randomId = 'random-id';
|
||||
renderComponent(undefined, `?show=${randomId}`);
|
||||
renderComponent(undefined, `/course/:courseId?show=${randomId}`);
|
||||
|
||||
const cardUnits = screen.queryByTestId('subsection-card__units');
|
||||
const newUnitButton = screen.queryByRole('button', { name: 'New unit' });
|
||||
@@ -321,4 +326,51 @@ describe('<SubsectionCard />', () => {
|
||||
libraryContentKey: containerKey,
|
||||
});
|
||||
});
|
||||
|
||||
it('should sync subsection changes from upstream', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByTestId('subsection-card-header')).toBeInTheDocument();
|
||||
|
||||
// Click on sync button
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
// Should open compare preview modal
|
||||
expect(screen.getByRole('heading', { name: /preview changes: subsection name/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
||||
|
||||
// Click on accept changes
|
||||
const acceptChangesButton = screen.getByText(/accept changes/i);
|
||||
fireEvent.click(acceptChangesButton);
|
||||
|
||||
await waitFor(() => expect(mockUseAcceptLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should decline sync subsection changes from upstream', async () => {
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByTestId('subsection-card-header')).toBeInTheDocument();
|
||||
|
||||
// Click on sync button
|
||||
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
|
||||
fireEvent.click(syncButton);
|
||||
|
||||
// Should open compare preview modal
|
||||
expect(screen.getByRole('heading', { name: /preview changes: subsection name/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('Preview not available')).toBeInTheDocument();
|
||||
|
||||
// Click on ignore changes
|
||||
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
|
||||
fireEvent.click(ignoreChangesButton);
|
||||
|
||||
// Should open the confirmation modal
|
||||
expect(screen.getByRole('heading', { name: /ignore these changes\?/i })).toBeInTheDocument();
|
||||
|
||||
// Click on ignore button
|
||||
const ignoreButton = screen.getByRole('button', { name: /ignore/i });
|
||||
fireEvent.click(ignoreButton);
|
||||
|
||||
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, {
|
||||
useContext, useEffect, useState, useRef, useCallback, ReactNode,
|
||||
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
|
||||
} from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { StandardModal, useToggle } from '@openedx/paragon';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@@ -16,6 +17,7 @@ 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 { ComponentPicker, SelectedComponent } from '@src/library-authoring';
|
||||
@@ -24,7 +26,9 @@ import { ContainerType } from '@src/generic/key-utils';
|
||||
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
|
||||
import { ContentType } from '@src/library-authoring/routes';
|
||||
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
|
||||
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
|
||||
import type { XBlock } from '@src/data/types';
|
||||
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
|
||||
import messages from './messages';
|
||||
|
||||
interface SubsectionCardProps {
|
||||
@@ -86,6 +90,7 @@ const SubsectionCard = ({
|
||||
const locatorId = searchParams.get('show');
|
||||
const isScrolledToElement = locatorId === subsection.id;
|
||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
|
||||
const namePrefix = 'subsection';
|
||||
const { sharedClipboardData, showPasteUnit } = useClipboard();
|
||||
const [
|
||||
@@ -93,6 +98,8 @@ const SubsectionCard = ({
|
||||
openAddLibraryUnitModal,
|
||||
closeAddLibraryUnitModal,
|
||||
] = useToggle(false);
|
||||
const { courseId } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
id,
|
||||
@@ -108,6 +115,19 @@ const SubsectionCard = ({
|
||||
upstreamInfo,
|
||||
} = subsection;
|
||||
|
||||
const blockSyncData = useMemo(() => {
|
||||
if (!upstreamInfo?.readyToSync) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
displayName,
|
||||
downstreamBlockId: id,
|
||||
upstreamBlockId: upstreamInfo.upstreamRef,
|
||||
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
|
||||
isContainer: true,
|
||||
};
|
||||
}, [upstreamInfo]);
|
||||
|
||||
// re-create actions object for customizations
|
||||
const actions = { ...subsectionActions };
|
||||
// add actions to control display of move up & down menu button.
|
||||
@@ -148,6 +168,13 @@ const SubsectionCard = ({
|
||||
dispatch(setCurrentItem(subsection));
|
||||
};
|
||||
|
||||
const handleOnPostChangeSync = useCallback(() => {
|
||||
dispatch(fetchCourseSectionQuery([section.id]));
|
||||
if (courseId) {
|
||||
invalidateLinksQuery(queryClient, courseId);
|
||||
}
|
||||
}, [dispatch, section, queryClient, courseId]);
|
||||
|
||||
const handleEditSubmit = (titleValue: string) => {
|
||||
if (displayName !== titleValue) {
|
||||
onEditSubmit(id, section.id, titleValue);
|
||||
@@ -269,6 +296,7 @@ const SubsectionCard = ({
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
onClickSync={openSyncModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
@@ -280,6 +308,7 @@ const SubsectionCard = ({
|
||||
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
|
||||
isSequential
|
||||
extraActionsComponent={extraActionsComponent}
|
||||
readyToSync={upstreamInfo?.readyToSync}
|
||||
/>
|
||||
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
|
||||
<XBlockStatus
|
||||
@@ -332,6 +361,14 @@ const SubsectionCard = ({
|
||||
visibleTabs={[ContentType.units]}
|
||||
/>
|
||||
</StandardModal>
|
||||
{blockSyncData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -92,6 +92,10 @@ const renderComponent = (props?: object) => render(
|
||||
}}
|
||||
{...props}
|
||||
/>,
|
||||
{
|
||||
path: '/course/:courseId',
|
||||
params: { courseId: '5' },
|
||||
},
|
||||
);
|
||||
|
||||
describe('<UnitCard />', () => {
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import CourseOutlineUnitCardExtraActionsSlot from '@src/plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '@src/course-outline/data/slice';
|
||||
@@ -22,6 +23,7 @@ import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course
|
||||
import { useClipboard } from '@src/generic/clipboard';
|
||||
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
|
||||
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
|
||||
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
|
||||
import type { XBlock } from '@src/data/types';
|
||||
|
||||
interface UnitCardProps {
|
||||
@@ -74,6 +76,8 @@ const UnitCard = ({
|
||||
const namePrefix = 'unit';
|
||||
|
||||
const { copyToClipboard } = useClipboard();
|
||||
const { courseId } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
id,
|
||||
@@ -155,7 +159,10 @@ const UnitCard = ({
|
||||
|
||||
const handleOnPostChangeSync = useCallback(() => {
|
||||
dispatch(fetchCourseSectionQuery([section.id]));
|
||||
}, [dispatch, section]);
|
||||
if (courseId) {
|
||||
invalidateLinksQuery(queryClient, courseId);
|
||||
}
|
||||
}, [dispatch, section, queryClient, courseId]);
|
||||
|
||||
const titleComponent = (
|
||||
<TitleLink
|
||||
|
||||
Reference in New Issue
Block a user