Skip to content

Commit 397fcbb

Browse files
fix: ensure optimistic cache updates for unenroll and saved for later (#1331)
1 parent a0913e8 commit 397fcbb

File tree

8 files changed

+97
-83
lines changed

8 files changed

+97
-83
lines changed

src/components/app/routes/data/utils.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ export async function ensureEnterpriseAppData({
8787
}
8888

8989
// Optimistically update the query cache with the auto-activated or auto-applied subscription license.
90-
queryClient.setQueryData(subscriptionsQuery.queryKey, {
91-
...queryClient.getQueryData(subscriptionsQuery.queryKey),
90+
queryClient.setQueryData(subscriptionsQuery.queryKey, (oldData) => ({
91+
...oldData,
9292
subscriptionLicensesByStatus: updatedLicensesByStatus,
9393
subscriptionPlan: activatedOrAutoAppliedLicense.subscriptionPlan,
9494
subscriptionLicense: activatedOrAutoAppliedLicense,
9595
subscriptionLicenses: updatedSubscriptionLicenses,
96-
});
96+
}));
9797
}
9898

9999
return subscriptionsData;
@@ -331,14 +331,14 @@ export async function ensureActiveEnterpriseCustomerUser({
331331
enterpriseCustomer = nextActiveEnterpriseCustomer;
332332
activeEnterpriseCustomer = nextActiveEnterpriseCustomer;
333333
allLinkedEnterpriseCustomerUsers = updatedLinkedEnterpriseCustomerUsers;
334-
// Optimistically update the BFF layer (use helper)
334+
// Optimistically update the BFF query cache
335335
if (matchedBFFQuery) {
336-
queryClient.setQueryData(matchedBFFQuery({ enterpriseSlug }), {
337-
...queryClient.getQueryData(matchedBFFQuery({ enterpriseSlug })),
336+
queryClient.setQueryData(matchedBFFQuery({ enterpriseSlug }).queryKey, (oldData) => ({
337+
...oldData,
338338
enterpriseCustomer,
339339
activeEnterpriseCustomer,
340340
allLinkedEnterpriseCustomerUsers: updatedLinkedEnterpriseCustomerUsers,
341-
});
341+
}));
342342
}
343343
return {
344344
enterpriseCustomer,

src/components/app/routes/data/utils.test.js

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -194,22 +194,19 @@ describe('ensureActiveEnterpriseCustomerUser', () => {
194194
});
195195
if (isBFFData) {
196196
expect(mockQueryClient.setQueryData).toHaveBeenCalledTimes(timesUpdateActiveEnterpriseCustomerCalled);
197-
expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(
198-
{
199-
queryFn: expect.any(Function),
200-
queryKey: queryEnterpriseLearnerDashboardBFF(
201-
{ enterpriseSlug: expectedEnterpriseCustomer.slug },
202-
).queryKey,
203-
},
204-
{
205-
...mockQueryClient.getQueryData(queryEnterpriseLearnerDashboardBFF(
206-
{ enterpriseSlug: expectedEnterpriseCustomer.slug },
207-
)),
208-
enterpriseCustomer: expectedEnterpriseCustomer,
209-
activeEnterpriseCustomer: expectedEnterpriseCustomer,
210-
allLinkedEnterpriseCustomerUsers: expectedAllLinkedEnterpriseCustomers,
211-
},
212-
);
197+
const expectedQuery = queryEnterpriseLearnerDashboardBFF({
198+
enterpriseSlug: expectedEnterpriseCustomer.slug,
199+
});
200+
expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(expectedQuery.queryKey, expect.any(Function));
201+
// Get the function that was passed to setQueryData
202+
const updateFunction = mockQueryClient.setQueryData.mock.calls[0][1];
203+
const result = updateFunction(mockEnterpriseLearnerData);
204+
expect(result).toEqual({
205+
enterpriseCustomer: expectedEnterpriseCustomer,
206+
activeEnterpriseCustomer: expectedEnterpriseCustomer,
207+
allLinkedEnterpriseCustomerUsers: expectedAllLinkedEnterpriseCustomers,
208+
shouldUpdateActiveEnterpriseCustomerUser: false,
209+
});
213210
}
214211
}
215212
},

src/components/app/routes/loaders/tests/rootLoader.test.jsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,19 @@ describe('rootLoader', () => {
431431
});
432432

433433
// Assert the subscriptions query cache is optimistically updated
434-
expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(subscriptionsQuery.queryKey, {
434+
expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(subscriptionsQuery.queryKey, expect.any(Function));
435+
// Get the function that was passed to setQueryData
436+
const updateFunction = mockQueryClient.setQueryData.mock.calls[0][1];
437+
// Call the function with a mock oldData value if needed to simulate the cache update
438+
const result = updateFunction({
439+
subscriptionLicenses: [mockSubscriptionsData.subscriptionLicense],
440+
subscriptionLicensesByStatus: {
441+
[LICENSE_STATUS.PENDING]: [mockSubscriptionsData.subscriptionLicense],
442+
},
443+
subscriptionLicense: mockSubscriptionsData.subscriptionLicense,
444+
subscriptionPlan: mockSubscriptionsData.subscriptionPlan,
445+
});
446+
expect(result).toEqual({
435447
subscriptionLicenses: [
436448
{
437449
...mockSubscriptionsData.subscriptionLicense,

src/components/dashboard/main-content/course-enrollments/course-cards/unenroll/UnenrollModal.jsx

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useQueryClient } from '@tanstack/react-query';
44
import {
55
ActionRow, Alert, AlertModal, Button, StatefulButton,
66
} from '@openedx/paragon';
7-
import { logError, logInfo } from '@edx/frontend-platform/logging';
7+
import { logError } from '@edx/frontend-platform/logging';
88

99
import { ToastsContext } from '../../../../../Toasts';
1010
import { unenrollFromCourse } from './data';
@@ -51,28 +51,29 @@ const UnenrollModal = ({
5151
const bffQueryKeysToUpdate = [dashboardBFFQueryKey];
5252
// Update the enterpriseCourseEnrollments data in the cache for each BFF query.
5353
bffQueryKeysToUpdate.forEach((queryKey) => {
54-
const existingBFFData = queryClient.getQueryData(queryKey);
55-
if (!existingBFFData) {
56-
logInfo(`Skipping optimistic cache update of ${JSON.stringify(queryKey)} as no cached query data exists yet.`);
57-
return;
58-
}
59-
const updatedBFFData = {
60-
...existingBFFData,
61-
enterpriseCourseEnrollments: existingBFFData.enterpriseCourseEnrollments.filter(enrollmentForCourseFilter),
62-
};
63-
queryClient.setQueryData(queryKey, updatedBFFData);
54+
queryClient.setQueryData(queryKey, (oldData) => {
55+
if (!oldData) {
56+
return oldData;
57+
}
58+
return {
59+
...oldData,
60+
enterpriseCourseEnrollments: oldData.enterpriseCourseEnrollments.filter(enrollmentForCourseFilter),
61+
allEnrollmentsByStatus: Object.keys(oldData.allEnrollmentsByStatus).reduce((acc, status) => {
62+
const filteredEnrollments = oldData.allEnrollmentsByStatus[status].filter(enrollmentForCourseFilter);
63+
acc[status] = filteredEnrollments;
64+
return acc;
65+
}, {}),
66+
};
67+
});
6468
});
6569
}
6670

6771
// Update the legacy queryEnterpriseCourseEnrollments cache as well.
6872
const enterpriseCourseEnrollmentsQueryKey = queryEnterpriseCourseEnrollments(enterpriseCustomer.uuid).queryKey;
69-
const existingCourseEnrollmentsData = queryClient.getQueryData(enterpriseCourseEnrollmentsQueryKey);
70-
if (!existingCourseEnrollmentsData) {
71-
logInfo(`Skipping optimistic cache update of ${JSON.stringify(enterpriseCourseEnrollmentsQueryKey)} as no cached query data exists yet.`);
72-
return;
73-
}
74-
const updatedCourseEnrollmentsData = existingCourseEnrollmentsData.filter(enrollmentForCourseFilter);
75-
queryClient.setQueryData(enterpriseCourseEnrollmentsQueryKey, updatedCourseEnrollmentsData);
73+
queryClient.setQueryData(enterpriseCourseEnrollmentsQueryKey, (oldData) => {
74+
const updatedCourseEnrollmentsData = oldData?.filter(enrollmentForCourseFilter);
75+
return updatedCourseEnrollmentsData;
76+
});
7677
};
7778

7879
const handleUnenrollButtonClick = async () => {

src/components/dashboard/main-content/course-enrollments/course-cards/unenroll/UnenrollModal.test.jsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { render, screen, waitFor } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import { QueryClientProvider } from '@tanstack/react-query';
44
import '@testing-library/jest-dom/extend-expect';
5-
import { logInfo } from '@edx/frontend-platform/logging';
65
import { COURSE_STATUSES } from '../../../../../../constants';
76
import { unenrollFromCourse } from './data';
87
import UnenrollModal from './UnenrollModal';
@@ -35,10 +34,6 @@ jest.mock('../../../../../app/data', () => ({
3534
fetchEnterpriseLearnerDashboard: jest.fn(),
3635
}));
3736

38-
jest.mock('@edx/frontend-platform/logging', () => ({
39-
logInfo: jest.fn(),
40-
}));
41-
4237
const mockEnterpriseCustomer = enterpriseCustomerFactory();
4338
const mockEnterpriseCourseEnrollment = enterpriseCourseEnrollmentFactory();
4439
const mockEnterpriseCourseEnrollments = [mockEnterpriseCourseEnrollment];
@@ -188,14 +183,11 @@ describe('<UnenrollModal />', () => {
188183
const bffDashboardData = mockQueryClient.getQueryData(
189184
queryEnterpriseLearnerDashboardBFF({ enterpriseSlug: mockEnterpriseCustomer.slug }).queryKey,
190185
);
191-
let expectedLogInfoCalls = 0;
192186
if (isBFFEnabled) {
193187
// Only verify the BFF queryEnterpriseCourseEnrollments cache is updated if BFF feature is enabled.
194188
let expectedBFFDashboardData;
195189
if (existingBFFDashboardQueryData) {
196190
expectedBFFDashboardData = learnerDashboardBFFResponse;
197-
} else {
198-
expectedLogInfoCalls += 1;
199191
}
200192
expect(bffDashboardData).toEqual(expectedBFFDashboardData);
201193
} else {
@@ -214,14 +206,9 @@ describe('<UnenrollModal />', () => {
214206
let expectedLegacyEnrollmentsData;
215207
if (existingEnrollmentsQueryData) {
216208
expectedLegacyEnrollmentsData = [];
217-
} else {
218-
expectedLogInfoCalls += 1;
219209
}
220210
expect(legacyEnrollmentsData).toEqual(expectedLegacyEnrollmentsData);
221211

222-
// Verify logInfo calls
223-
expect(logInfo).toHaveBeenCalledTimes(expectedLogInfoCalls);
224-
225212
// Verify side effects
226213
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
227214
expect(mockAddToast).toHaveBeenCalledTimes(1);

src/components/dashboard/main-content/course-enrollments/data/hooks.js

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
import { useMutation, useQueryClient } from '@tanstack/react-query';
55
import { AppContext } from '@edx/frontend-platform/react';
66
import { camelCaseObject } from '@edx/frontend-platform/utils';
7-
import { logError, logInfo } from '@edx/frontend-platform/logging';
7+
import { logError } from '@edx/frontend-platform/logging';
88
import { sendEnterpriseTrackEventWithDelay } from '@edx/frontend-enterprise-utils';
99

1010
import { useLocation } from 'react-router-dom';
@@ -42,6 +42,7 @@ import { ASSIGNMENTS_EXPIRING_WARNING_LOCALSTORAGE_KEY } from '../../../data/con
4242
import { LICENSE_STATUS } from '../../../../enterprise-user-subsidy/data/constants';
4343
import { useStatefulEnroll } from '../../../../stateful-enroll/data';
4444
import { COURSE_STATUSES } from '../../../../../constants';
45+
import { findCourseStatusKey } from '../../../../../utils/common';
4546

4647
/**
4748
* Return data for upgrading a course using the user's subsidies
@@ -494,32 +495,47 @@ export function useUpdateCourseEnrollmentStatus() {
494495
const dashboardBFFQueryKey = queryEnterpriseLearnerDashboardBFF({
495496
enterpriseSlug: enterpriseCustomer.slug,
496497
}).queryKey;
497-
498498
const bffQueryKeysToUpdate = [dashboardBFFQueryKey];
499499
// Update the enterpriseCourseEnrollments data in the cache for each BFF query.
500500
bffQueryKeysToUpdate.forEach((queryKey) => {
501-
const existingBFFData = queryClient.getQueryData(queryKey);
502-
if (!existingBFFData) {
503-
logInfo(`Skipping optimistic cache update of ${JSON.stringify(queryKey)} as no cached query data exists yet.`);
504-
return;
505-
}
506-
const updatedBFFData = {
507-
...existingBFFData,
508-
enterpriseCourseEnrollments: existingBFFData.enterpriseCourseEnrollments.map(transformUpdatedEnrollment),
509-
};
510-
queryClient.setQueryData(queryKey, updatedBFFData);
501+
queryClient.setQueryData(queryKey, (oldData) => {
502+
if (!oldData) {
503+
return oldData;
504+
}
505+
const updatedEnrollments = oldData.enterpriseCourseEnrollments.map(transformUpdatedEnrollment);
506+
const updatedAllEnrollmentsByStatus = Object.keys(oldData.allEnrollmentsByStatus).reduce((acc, status) => {
507+
acc[status] = oldData.allEnrollmentsByStatus[status]
508+
.filter((enrollment) => enrollment.courseRunId !== courseRunId)
509+
.map(transformUpdatedEnrollment);
510+
return acc;
511+
}, {});
512+
513+
// Find the updated enrollment.
514+
const updatedEnrollment = updatedEnrollments.find(enrollment => enrollment.courseRunId === courseRunId);
515+
if (updatedEnrollment) {
516+
const newCourseStatusKey = findCourseStatusKey(updatedEnrollment.courseRunStatus);
517+
// Add the enrollment to the new status group.
518+
if (!updatedAllEnrollmentsByStatus[newCourseStatusKey]) {
519+
updatedAllEnrollmentsByStatus[newCourseStatusKey] = [];
520+
}
521+
updatedAllEnrollmentsByStatus[newCourseStatusKey].push(updatedEnrollment);
522+
}
523+
524+
return {
525+
...oldData,
526+
enterpriseCourseEnrollments: updatedEnrollments,
527+
allEnrollmentsByStatus: updatedAllEnrollmentsByStatus,
528+
};
529+
});
511530
});
512531
}
513532

514533
// Update the legacy queryEnterpriseCourseEnrollments cache as well.
515534
const enterpriseCourseEnrollmentsQueryKey = queryEnterpriseCourseEnrollments(enterpriseCustomer.uuid).queryKey;
516-
const existingCourseEnrollmentsData = queryClient.getQueryData(enterpriseCourseEnrollmentsQueryKey);
517-
if (!existingCourseEnrollmentsData) {
518-
logInfo(`Skipping optimistic cache update of ${JSON.stringify(enterpriseCourseEnrollmentsQueryKey)} as no cached query data exists yet.`);
519-
return;
520-
}
521-
const updatedCourseEnrollmentsData = existingCourseEnrollmentsData.map(transformUpdatedEnrollment);
522-
queryClient.setQueryData(enterpriseCourseEnrollmentsQueryKey, updatedCourseEnrollmentsData);
535+
queryClient.setQueryData(enterpriseCourseEnrollmentsQueryKey, (oldData) => {
536+
const updatedData = oldData?.map(transformUpdatedEnrollment);
537+
return updatedData;
538+
});
523539
}, [queryClient, enterpriseCustomer, isBFFEnabled]);
524540
}
525541

src/components/dashboard/main-content/course-enrollments/data/tests/hooks.test.jsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { act, renderHook } from '@testing-library/react-hooks';
22
import * as logger from '@edx/frontend-platform/logging';
3-
import { logInfo } from '@edx/frontend-platform/logging';
43
import { AppContext } from '@edx/frontend-platform/react';
54
import { sendEnterpriseTrackEventWithDelay } from '@edx/frontend-enterprise-utils';
65
import dayjs from 'dayjs';
@@ -1218,7 +1217,6 @@ describe('useUpdateCourseEnrollmentStatus', () => {
12181217
enterpriseSlug: mockEnterpriseCustomer.slug,
12191218
}).queryKey,
12201219
);
1221-
let expectedLogInfoCalls = 0;
12221220
const expectedCourseRunStatus = doesCourseRunIdMatch
12231221
? newEnrollmentStatus
12241222
: originalEnrollmentStatus;
@@ -1229,9 +1227,6 @@ describe('useUpdateCourseEnrollmentStatus', () => {
12291227
);
12301228
if (existingBFFDashboardQueryData) {
12311229
expect(foundMockEnrollment.courseRunStatus).toEqual(expectedCourseRunStatus);
1232-
} else {
1233-
expectedLogInfoCalls += 1;
1234-
expect(dashboardBFFData).toBeUndefined();
12351230
}
12361231
}
12371232

@@ -1245,12 +1240,8 @@ describe('useUpdateCourseEnrollmentStatus', () => {
12451240
if (existingEnrollmentsQueryData) {
12461241
expect(foundMockEnrollment.courseRunStatus).toEqual(expectedCourseRunStatus);
12471242
} else {
1248-
expectedLogInfoCalls += 1;
12491243
expect(enrollmentsData).toBeUndefined();
12501244
}
1251-
1252-
// Verify logInfo calls
1253-
expect(logInfo).toHaveBeenCalledTimes(expectedLogInfoCalls);
12541245
});
12551246
});
12561247
});

src/utils/common.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { logError } from '@edx/frontend-platform/logging';
22
import dayjs from './dayjs';
3+
import { COURSE_STATUSES } from '../constants';
34

45
export const isCourseEnded = endDate => dayjs(endDate).isBefore(dayjs());
56

@@ -193,3 +194,12 @@ export const formatPrice = (price, options = {}) => {
193194
});
194195
return USDollar.format(Math.abs(price));
195196
};
197+
198+
export function findCourseStatusKey(statusValue) {
199+
for (const key in COURSE_STATUSES) {
200+
if (COURSE_STATUSES[key] === statusValue) {
201+
return key;
202+
}
203+
}
204+
return undefined;
205+
}

0 commit comments

Comments
 (0)