Skip to content

Commit a98f89e

Browse files
Merge branch 'master' into romaniuk/fix/load-sequences
2 parents 598cbf6 + d8eda24 commit a98f89e

Some content is hidden

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

53 files changed

+645
-3011
lines changed

package-lock.json

Lines changed: 8 additions & 2565 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
"meilisearch": "^0.41.0",
8080
"moment": "2.30.1",
8181
"moment-shortformat": "^2.1.0",
82-
"npm": "^10.8.1",
8382
"prop-types": "^15.8.1",
8483
"react": "^18.3.1",
8584
"react-datepicker": "^4.13.0",
@@ -98,7 +97,6 @@
9897
"redux-logger": "^3.0.6",
9998
"redux-thunk": "^2.4.1",
10099
"reselect": "^4.1.5",
101-
"start": "^5.1.0",
102100
"tinymce": "^5.10.4",
103101
"universal-cookie": "^4.0.4",
104102
"uuid": "^3.4.0",

src/accessibility-page/AccessibilityPage.jsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,30 @@ import messages from './messages';
99
import AccessibilityBody from './AccessibilityBody';
1010
import AccessibilityForm from './AccessibilityForm';
1111

12+
import { COMMUNITY_ACCESSIBILITY_LINK, ACCESSIBILITY_EMAIL } from './constants';
13+
1214
const AccessibilityPage = ({
1315
// injected
1416
intl,
15-
}) => {
16-
const communityAccessibilityLink = 'https://www.edx.org/accessibility';
17-
const email = '[email protected]';
18-
return (
19-
<>
20-
<Helmet>
21-
<title>
22-
{intl.formatMessage(messages.pageTitle, {
23-
siteName: process.env.SITE_NAME,
24-
})}
25-
</title>
26-
</Helmet>
27-
<Header isHiddenMainMenu />
28-
<Container size="xl" classNamae="px-4">
29-
<AccessibilityBody {...{ email, communityAccessibilityLink }} />
30-
<AccessibilityForm accessibilityEmail={email} />
31-
</Container>
32-
<StudioFooterSlot />
33-
</>
34-
);
35-
};
17+
}) => (
18+
<>
19+
<Helmet>
20+
<title>
21+
{intl.formatMessage(messages.pageTitle, {
22+
siteName: process.env.SITE_NAME,
23+
})}
24+
</title>
25+
</Helmet>
26+
<Header isHiddenMainMenu />
27+
<Container size="xl" classNamae="px-4">
28+
<AccessibilityBody
29+
{...{ email: ACCESSIBILITY_EMAIL, communityAccessibilityLink: COMMUNITY_ACCESSIBILITY_LINK }}
30+
/>
31+
<AccessibilityForm accessibilityEmail={ACCESSIBILITY_EMAIL} />
32+
</Container>
33+
<StudioFooterSlot />
34+
</>
35+
);
3636

3737
AccessibilityPage.propTypes = {
3838
// injected

src/accessibility-page/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const COMMUNITY_ACCESSIBILITY_LINK = 'https://www.edx.org/accessibility';
2+
export const ACCESSIBILITY_EMAIL = '[email protected]';

src/course-libraries/CourseLibraries.test.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,46 @@ describe('<CourseLibraries />', () => {
118118
userEvent.click(reviewActionBtn);
119119
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
120120
});
121+
122+
it('show alert if max lastPublishedDate is greated than the local storage value', async () => {
123+
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
124+
localStorage.setItem(
125+
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
126+
String(lastPublishedDate.getTime() - 1000),
127+
);
128+
129+
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
130+
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
131+
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
132+
// review tab should be open by default as outOfSyncCount is greater than 0
133+
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
134+
135+
userEvent.click(allTab);
136+
const alert = await screen.findByRole('alert');
137+
expect(await within(alert).findByText(
138+
'5 library components are out of sync. Review updates to accept or ignore changes',
139+
)).toBeInTheDocument();
140+
});
141+
142+
it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => {
143+
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
144+
localStorage.setItem(
145+
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
146+
String(lastPublishedDate.getTime() + 1000),
147+
);
148+
149+
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
150+
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
151+
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
152+
// review tab should be open by default as outOfSyncCount is greater than 0
153+
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
154+
userEvent.click(allTab);
155+
expect(allTab).toHaveAttribute('aria-selected', 'true');
156+
157+
screen.logTestingPlaygroundURL();
158+
159+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
160+
});
121161
});
122162

123163
describe('<CourseLibraries ReviewTab />', () => {
@@ -160,7 +200,7 @@ describe('<CourseLibraries ReviewTab />', () => {
160200

161201
it('update changes works', async () => {
162202
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
163-
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
203+
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
164204
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
165205
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
166206
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
@@ -176,7 +216,7 @@ describe('<CourseLibraries ReviewTab />', () => {
176216

177217
it('update changes works in preview modal', async () => {
178218
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
179-
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
219+
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
180220
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
181221
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
182222
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
@@ -195,7 +235,7 @@ describe('<CourseLibraries ReviewTab />', () => {
195235

196236
it('ignore change works', async () => {
197237
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
198-
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
238+
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
199239
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
200240
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
201241
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
@@ -218,7 +258,7 @@ describe('<CourseLibraries ReviewTab />', () => {
218258

219259
it('ignore change works in preview', async () => {
220260
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
221-
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
261+
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
222262
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
223263
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
224264
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });

src/course-libraries/CourseLibraries.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
164164
if (tabKey !== CourseLibraryTabs.review) {
165165
return null;
166166
}
167-
if (!outOfSyncCount || outOfSyncCount === 0) {
167+
if (!outOfSyncCount) {
168168
return (
169169
<Stack direction="horizontal" gap={2}>
170170
<Icon src={CheckCircle} size="xs" />

src/course-libraries/OutOfSyncAlert.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@ interface OutOfSyncAlertProps {
1818
* in course can be updated. Following are the conditions for displaying the alert.
1919
*
2020
* * The alert is displayed if components are out of sync.
21-
* * If the user clicks on dismiss button, the state is stored in localstorage of user
22-
* in this format: outOfSyncCountAlert-${courseId} = <number of out of sync components>.
23-
* * If the number of sync components don't change for the course and the user opens outline
21+
* * If the user clicks on dismiss button, the state of dismiss is stored in localstorage of user
22+
* in this format: outOfSyncCountAlert-${courseId} = <datetime value in milliseconds>.
23+
* * If there are not new published components for the course and the user opens outline
2424
* in the same browser, they don't see the alert again.
25-
* * If the number changes, i.e., if a new component is out of sync or the user updates or ignores
26-
* a component, the alert is displayed again.
25+
* * If there is a new published component upstream, the alert is displayed again.
2726
*/
2827
export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
2928
showAlert,
@@ -35,6 +34,8 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
3534
const intl = useIntl();
3635
const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
3736
const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0);
37+
const lastPublishedDate = data?.map(lib => new Date(lib.lastPublishedAt || 0).getTime())
38+
.reduce((acc, lastPublished) => Math.max(lastPublished, acc), 0);
3839
const alertKey = `outOfSyncCountAlert-${courseId}`;
3940

4041
useEffect(() => {
@@ -46,13 +47,14 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
4647
setShowAlert(false);
4748
return;
4849
}
49-
const dismissedAlert = localStorage.getItem(alertKey);
50-
setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount);
51-
}, [outOfSyncCount, isLoading, data]);
50+
const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10);
51+
52+
setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate);
53+
}, [outOfSyncCount, lastPublishedDate, isLoading, data]);
5254

5355
const dismissAlert = () => {
5456
setShowAlert(false);
55-
localStorage.setItem(alertKey, String(outOfSyncCount));
57+
localStorage.setItem(alertKey, Date.now().toString());
5658
onDismiss?.();
5759
};
5860

src/course-libraries/ReviewTabContent.tsx

Lines changed: 22 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {
2-
useCallback, useContext, useEffect, useMemo, useState,
2+
useCallback, useContext, useMemo, useState,
33
} from 'react';
44
import { getConfig } from '@edx/frontend-platform';
55
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -16,7 +16,7 @@ import {
1616

1717
import { tail, keyBy } from 'lodash';
1818
import { useQueryClient } from '@tanstack/react-query';
19-
import { Loop, Warning } from '@openedx/paragon/icons';
19+
import { Loop } from '@openedx/paragon/icons';
2020
import messages from './messages';
2121
import previewChangesMessages from '../course-unit/preview-changes/messages';
2222
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
@@ -35,7 +35,6 @@ import { useLoadOnScroll } from '../hooks';
3535
import DeleteModal from '../generic/delete-modal/DeleteModal';
3636
import { PublishableEntityLink } from './data/api';
3737
import AlertError from '../generic/alert-error';
38-
import AlertMessage from '../generic/alert-message';
3938

4039
interface Props {
4140
courseId: string;
@@ -100,34 +99,24 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions }) => {
10099

101100
const ComponentReviewList = ({
102101
outOfSyncComponents,
103-
onSearchUpdate,
104102
}: {
105103
outOfSyncComponents: PublishableEntityLink[];
106-
onSearchUpdate: () => void;
107104
}) => {
108105
const intl = useIntl();
109106
const { showToast } = useContext(ToastContext);
110107
const [blockData, setBlockData] = useState<LibraryChangesMessageData | undefined>(undefined);
111108
// ignore changes confirmation modal toggle.
112109
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
113110
const {
114-
hits: downstreamInfo,
111+
hits,
115112
isLoading: isIndexDataLoading,
116-
searchKeywords,
117113
hasError,
118114
hasNextPage,
119115
isFetchingNextPage,
120116
fetchNextPage,
121-
} = useSearchContext() as {
122-
hits: ContentHit[];
123-
isLoading: boolean;
124-
searchKeywords: string;
125-
searchSortOrder: SearchSortOption;
126-
hasError: boolean;
127-
hasNextPage: boolean | undefined,
128-
isFetchingNextPage: boolean;
129-
fetchNextPage: () => void;
130-
};
117+
} = useSearchContext();
118+
119+
const downstreamInfo = hits as ContentHit[];
131120

132121
useLoadOnScroll(
133122
hasNextPage,
@@ -142,36 +131,31 @@ const ComponentReviewList = ({
142131
);
143132
const queryClient = useQueryClient();
144133

145-
useEffect(() => {
146-
if (searchKeywords) {
147-
onSearchUpdate();
148-
}
149-
}, [searchKeywords]);
150-
151134
// Toggle preview changes modal
152135
const [isModalOpen, openModal, closeModal] = useToggle(false);
153136
const acceptChangesMutation = useAcceptLibraryBlockChanges();
154137
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
155138

156-
const setSeletecdBlockData = (info: ContentHit) => {
139+
const setSelectedBlockData = useCallback((info: ContentHit) => {
157140
setBlockData({
158141
displayName: info.displayName,
159142
downstreamBlockId: info.usageKey,
160143
upstreamBlockId: outOfSyncComponentsByKey[info.usageKey].upstreamUsageKey,
161144
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
162145
isVertical: info.blockType === 'vertical',
163146
});
164-
};
147+
}, [outOfSyncComponentsByKey]);
148+
165149
// Show preview changes on review
166150
const onReview = useCallback((info: ContentHit) => {
167-
setSeletecdBlockData(info);
151+
setSelectedBlockData(info);
168152
openModal();
169-
}, [setSeletecdBlockData, openModal]);
153+
}, [setSelectedBlockData, openModal]);
170154

171155
const onIgnoreClick = useCallback((info: ContentHit) => {
172-
setSeletecdBlockData(info);
156+
setSelectedBlockData(info);
173157
openConfirmModal();
174-
}, [setSeletecdBlockData, openConfirmModal]);
158+
}, [setSelectedBlockData, openConfirmModal]);
175159

176160
const reloadLinks = useCallback((usageKey: string) => {
177161
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
@@ -273,20 +257,14 @@ const ComponentReviewList = ({
273257
)}
274258
/>
275259
))}
276-
<PreviewLibraryXBlockChanges
277-
blockData={blockData}
278-
isModalOpen={isModalOpen}
279-
closeModal={closeModal}
280-
postChange={postChange}
281-
alertNode={(
282-
<AlertMessage
283-
show
284-
variant="warning"
285-
icon={Warning}
286-
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
287-
/>
288-
)}
289-
/>
260+
{blockData && (
261+
<PreviewLibraryXBlockChanges
262+
blockData={blockData}
263+
isModalOpen={isModalOpen}
264+
closeModal={closeModal}
265+
postChange={postChange}
266+
/>
267+
)}
290268
<DeleteModal
291269
isOpen={isConfirmModalOpen}
292270
close={closeConfirmModal}
@@ -303,37 +281,17 @@ const ComponentReviewList = ({
303281
const ReviewTabContent = ({ courseId }: Props) => {
304282
const intl = useIntl();
305283
const {
306-
data: linkPages,
284+
data: outOfSyncComponents,
307285
isLoading: isSyncComponentsLoading,
308-
hasNextPage,
309-
isFetchingNextPage,
310-
fetchNextPage,
311286
isError,
312287
error,
313288
} = useEntityLinks({ courseId, readyToSync: true });
314289

315-
const outOfSyncComponents = useMemo(
316-
() => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [],
317-
[linkPages],
318-
);
319290
const downstreamKeys = useMemo(
320291
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
321292
[outOfSyncComponents],
322293
);
323294

324-
useLoadOnScroll(
325-
hasNextPage,
326-
isFetchingNextPage,
327-
fetchNextPage,
328-
true,
329-
);
330-
331-
const onSearchUpdate = () => {
332-
if (hasNextPage && !isFetchingNextPage) {
333-
fetchNextPage();
334-
}
335-
};
336-
337295
const disableSortOptions = [
338296
SearchSortOption.RELEVANCE,
339297
SearchSortOption.OLDEST,
@@ -364,7 +322,6 @@ const ReviewTabContent = ({ courseId }: Props) => {
364322
</ActionRow>
365323
<ComponentReviewList
366324
outOfSyncComponents={outOfSyncComponents}
367-
onSearchUpdate={onSearchUpdate}
368325
/>
369326
</SearchContextProvider>
370327
);

0 commit comments

Comments
 (0)