Skip to content

Commit 0c88fd6

Browse files
authored
feat: show sync button on section/subsections [FC-0097] (#2324)
- Adds the sync button on section/subsection cards
1 parent 8e680dc commit 0c88fd6

File tree

9 files changed

+228
-30
lines changed

9 files changed

+228
-30
lines changed

src/course-libraries/ReviewTabContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useQueryClient } from '@tanstack/react-query';
1919
import { Loop } from '@openedx/paragon/icons';
2020
import messages from './messages';
2121
import previewChangesMessages from '../course-unit/preview-changes/messages';
22-
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
22+
import { invalidateLinksQuery, useEntityLinks } from './data/apiHooks';
2323
import {
2424
SearchContextProvider, SearchKeywordsField, useSearchContext, BlockTypeLabel, Highlight, SearchSortWidget,
2525
} from '../search-manager';
@@ -189,7 +189,7 @@ const ItemReviewList = ({
189189

190190
const reloadLinks = useCallback((usageKey: string) => {
191191
const courseKey = outOfSyncItemsByKey[usageKey].downstreamContextKey;
192-
queryClient.invalidateQueries({ queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey) });
192+
invalidateLinksQuery(queryClient, courseKey);
193193
}, [outOfSyncItemsByKey]);
194194

195195
const postChange = (accept: boolean) => {

src/course-libraries/data/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
33

44
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
55

6-
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams-all/`;
6+
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`;
77
export const getEntityLinksSummaryByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/${downstreamContextKey}/summary`;
88

99
export interface PaginatedData<T> {

src/course-libraries/data/apiHooks.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
type QueryClient,
23
useQuery,
34
} from '@tanstack/react-query';
45
import { getEntityLinksSummaryByDownstreamContext, getEntityLinks } from './api';
@@ -70,3 +71,12 @@ export const useEntityLinksSummaryByDownstreamContext = (courseId?: string) => (
7071
enabled: courseId !== undefined,
7172
})
7273
);
74+
75+
/**
76+
* Ivalidates the downstream links query for a course
77+
*/
78+
export const invalidateLinksQuery = (queryClient: QueryClient, courseId: string) => {
79+
queryClient.invalidateQueries({
80+
queryKey: courseLibrariesQueryKeys.courseLibraries(courseId),
81+
});
82+
};

src/course-outline/section-card/SectionCard.test.tsx

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import {
2-
act, fireEvent, initializeMocks, render, screen, within,
2+
act, fireEvent, initializeMocks, render, screen, waitFor, within,
33
} from '@src/testUtils';
44
import { XBlock } from '@src/data/types';
55
import SectionCard from './SectionCard';
66

7-
const mockPathname = '/foo-bar';
7+
const mockUseAcceptLibraryBlockChanges = jest.fn();
8+
const mockUseIgnoreLibraryBlockChanges = jest.fn();
89

9-
jest.mock('react-router-dom', () => ({
10-
...jest.requireActual('react-router-dom'),
11-
useLocation: () => ({
12-
pathname: mockPathname,
10+
jest.mock('@src/course-unit/data/apiHooks', () => ({
11+
useAcceptLibraryBlockChanges: () => ({
12+
mutateAsync: mockUseAcceptLibraryBlockChanges,
13+
}),
14+
useIgnoreLibraryBlockChanges: () => ({
15+
mutateAsync: mockUseIgnoreLibraryBlockChanges,
1316
}),
1417
}));
1518

@@ -74,7 +77,7 @@ const section = {
7477

7578
const onEditSectionSubmit = jest.fn();
7679

77-
const renderComponent = (props?: object, entry = '/') => render(
80+
const renderComponent = (props?: object, entry = '/course/:courseId') => render(
7881
<SectionCard
7982
section={section}
8083
index={1}
@@ -98,7 +101,8 @@ const renderComponent = (props?: object, entry = '/') => render(
98101
<span>children</span>
99102
</SectionCard>,
100103
{
101-
path: '/',
104+
path: '/course/:courseId',
105+
params: { courseId: '5' },
102106
routerProps: {
103107
initialEntries: [entry],
104108
},
@@ -182,7 +186,7 @@ describe('<SectionCard />', () => {
182186
const collapsedSections = { ...section };
183187
// @ts-ignore-next-line
184188
collapsedSections.isSectionsExpanded = false;
185-
renderComponent(collapsedSections, `?show=${subsection.id}`);
189+
renderComponent(collapsedSections, `/course/:courseId?show=${subsection.id}`);
186190

187191
const cardSubsections = await screen.findByTestId('section-card__subsections');
188192
const newSubsectionButton = await screen.findByRole('button', { name: 'New subsection' });
@@ -194,7 +198,7 @@ describe('<SectionCard />', () => {
194198
const collapsedSections = { ...section };
195199
// @ts-ignore-next-line
196200
collapsedSections.isSectionsExpanded = false;
197-
renderComponent(collapsedSections, `?show=${unit.id}`);
201+
renderComponent(collapsedSections, `/course/:courseId?show=${unit.id}`);
198202

199203
const cardSubsections = await screen.findByTestId('section-card__subsections');
200204
const newSubsectionButton = await screen.findByRole('button', { name: 'New subsection' });
@@ -207,11 +211,58 @@ describe('<SectionCard />', () => {
207211
const collapsedSections = { ...section };
208212
// @ts-ignore-next-line
209213
collapsedSections.isSectionsExpanded = false;
210-
renderComponent(collapsedSections, `?show=${randomId}`);
214+
renderComponent(collapsedSections, `/course/:courseId?show=${randomId}`);
211215

212216
const cardSubsections = screen.queryByTestId('section-card__subsections');
213217
const newSubsectionButton = screen.queryByRole('button', { name: 'New subsection' });
214218
expect(cardSubsections).toBeNull();
215219
expect(newSubsectionButton).toBeNull();
216220
});
221+
222+
it('should sync section changes from upstream', async () => {
223+
renderComponent();
224+
225+
expect(await screen.findByTestId('section-card-header')).toBeInTheDocument();
226+
227+
// Click on sync button
228+
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
229+
fireEvent.click(syncButton);
230+
231+
// Should open compare preview modal
232+
expect(screen.getByRole('heading', { name: /preview changes: section name/i })).toBeInTheDocument();
233+
expect(screen.getByText('Preview not available')).toBeInTheDocument();
234+
235+
// Click on accept changes
236+
const acceptChangesButton = screen.getByText(/accept changes/i);
237+
fireEvent.click(acceptChangesButton);
238+
239+
await waitFor(() => expect(mockUseAcceptLibraryBlockChanges).toHaveBeenCalled());
240+
});
241+
242+
it('should decline sync section changes from upstream', async () => {
243+
renderComponent();
244+
245+
expect(await screen.findByTestId('section-card-header')).toBeInTheDocument();
246+
247+
// Click on sync button
248+
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
249+
fireEvent.click(syncButton);
250+
251+
// Should open compare preview modal
252+
expect(screen.getByRole('heading', { name: /preview changes: section name/i })).toBeInTheDocument();
253+
expect(screen.getByText('Preview not available')).toBeInTheDocument();
254+
255+
// Click on ignore changes
256+
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
257+
fireEvent.click(ignoreChangesButton);
258+
259+
// Should open the confirmation modal
260+
expect(screen.getByRole('heading', { name: /ignore these changes\?/i })).toBeInTheDocument();
261+
262+
// Click on ignore button
263+
const ignoreButton = screen.getByRole('button', { name: /ignore/i });
264+
fireEvent.click(ignoreButton);
265+
266+
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
267+
});
217268
});

src/course-outline/section-card/SectionCard.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {
2-
useContext, useEffect, useState, useRef, useCallback, ReactNode,
2+
useContext, useEffect, useState, useRef, useCallback, ReactNode, useMemo,
33
} from 'react';
44
import { useDispatch } from 'react-redux';
55
import { useIntl } from '@edx/frontend-platform/i18n';
66
import {
77
Bubble, Button, StandardModal, useToggle,
88
} from '@openedx/paragon';
9-
import { useSearchParams } from 'react-router-dom';
9+
import { useParams, useSearchParams } from 'react-router-dom';
1010
import classNames from 'classnames';
11+
import { useQueryClient } from '@tanstack/react-query';
1112

1213
import { setCurrentItem, setCurrentSection } from '@src/course-outline/data/slice';
1314
import { RequestStatus } from '@src/data/constants';
@@ -16,14 +17,17 @@ import SortableItem from '@src/course-outline/drag-helper/SortableItem';
1617
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
1718
import TitleButton from '@src/course-outline/card-header/TitleButton';
1819
import XBlockStatus from '@src/course-outline/xblock-status/XBlockStatus';
20+
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
1921
import { getItemStatus, getItemStatusBorder, scrollToElement } from '@src/course-outline/utils';
2022
import OutlineAddChildButtons from '@src/course-outline/OutlineAddChildButtons';
2123
import { ContainerType } from '@src/generic/key-utils';
2224
import { ComponentPicker, SelectedComponent } from '@src/library-authoring';
2325
import { ContentType } from '@src/library-authoring/routes';
2426
import { COMPONENT_TYPES } from '@src/generic/block-type-utils/constants';
27+
import { PreviewLibraryXBlockChanges } from '@src/course-unit/preview-changes';
2528
import { UpstreamInfoIcon } from '@src/generic/upstream-info-icon';
2629
import type { XBlock } from '@src/data/types';
30+
import { invalidateLinksQuery } from '@src/course-libraries/data/apiHooks';
2731
import messages from './messages';
2832

2933
interface SectionCardProps {
@@ -79,6 +83,8 @@ const SectionCard = ({
7983
openAddLibrarySubsectionModal,
8084
closeAddLibrarySubsectionModal,
8185
] = useToggle(false);
86+
const { courseId } = useParams();
87+
const queryClient = useQueryClient();
8288

8389
// Expand the section if a search result should be shown/scrolled to
8490
const containsSearchResult = () => {
@@ -107,6 +113,7 @@ const SectionCard = ({
107113
};
108114
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || isSectionsExpanded);
109115
const [isFormOpen, openForm, closeForm] = useToggle(false);
116+
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
110117
const namePrefix = 'section';
111118

112119
useEffect(() => {
@@ -126,6 +133,19 @@ const SectionCard = ({
126133
upstreamInfo,
127134
} = section;
128135

136+
const blockSyncData = useMemo(() => {
137+
if (!upstreamInfo?.readyToSync) {
138+
return undefined;
139+
}
140+
return {
141+
displayName,
142+
downstreamBlockId: id,
143+
upstreamBlockId: upstreamInfo.upstreamRef,
144+
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
145+
isContainer: true,
146+
};
147+
}, [upstreamInfo]);
148+
129149
useEffect(() => {
130150
if (activeId === id && isExpanded) {
131151
setIsExpanded(false);
@@ -149,6 +169,13 @@ const SectionCard = ({
149169
setIsExpanded((prevState) => containsSearchResult() || prevState);
150170
}, [locatorId, setIsExpanded]);
151171

172+
const handleOnPostChangeSync = useCallback(() => {
173+
dispatch(fetchCourseSectionQuery([section.id]));
174+
if (courseId) {
175+
invalidateLinksQuery(queryClient, courseId);
176+
}
177+
}, [dispatch, section, courseId, queryClient]);
178+
152179
// re-create actions object for customizations
153180
const actions = { ...sectionActions };
154181
// add actions to control display of move up & down menu buton.
@@ -267,6 +294,7 @@ const SectionCard = ({
267294
onClickDelete={onOpenDeleteModal}
268295
onClickMoveUp={handleSectionMoveUp}
269296
onClickMoveDown={handleSectionMoveDown}
297+
onClickSync={openSyncModal}
270298
isFormOpen={isFormOpen}
271299
closeForm={closeForm}
272300
onEditSubmit={handleEditSubmit}
@@ -275,6 +303,7 @@ const SectionCard = ({
275303
titleComponent={titleComponent}
276304
namePrefix={namePrefix}
277305
actions={actions}
306+
readyToSync={upstreamInfo?.readyToSync}
278307
/>
279308
)}
280309
<div className="section-card__content" data-testid="section-card__content">
@@ -330,6 +359,14 @@ const SectionCard = ({
330359
visibleTabs={[ContentType.subsections]}
331360
/>
332361
</StandardModal>
362+
{blockSyncData && (
363+
<PreviewLibraryXBlockChanges
364+
blockData={blockSyncData}
365+
isModalOpen={isSyncModalOpen}
366+
closeModal={closeSyncModal}
367+
postChange={handleOnPostChangeSync}
368+
/>
369+
)}
333370
</>
334371
);
335372
};

0 commit comments

Comments
 (0)