Skip to content

Commit 8d8d5f6

Browse files
jonkaftonmbertrand
authored andcommitted
Query membership endpoints for resources belonging to user lists and learning paths (#1846)
* Retrieve user list and learning path memberships from new endpoints. Replace resource cache invalidation logic for membership keys * Remove commented dead code * Update Learning path hook and AddToListDialog tests * Null list memeberships from results * Pass in list booleans to the resource drawer * Remove redundant mock patch body * Fetch memeberships only when authenticated. Update tests. * Mock responses for tests * Clear list memberships from items list endpoints * Replace wait in test * Checkbox disabled state * Disable checkboxes if memberships not loaded
1 parent f1df4e1 commit 8d8d5f6

36 files changed

+447
-656
lines changed

frontends/api/src/hooks/learningPaths/index.test.ts

Lines changed: 23 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import { LearningResource } from "../../generated/v1"
55
import * as factories from "../../test-utils/factories"
66
import { setupReactQueryTest } from "../test-utils"
77
import { setMockResponse, urls, makeRequest } from "../../test-utils"
8-
import { useFeaturedLearningResourcesList } from "../learningResources"
9-
import { invalidateResourceQueries } from "../learningResources/invalidation"
108
import keyFactory from "../learningResources/keyFactory"
119
import {
1210
useLearningPathsDetail,
@@ -15,24 +13,11 @@ import {
1513
useLearningPathCreate,
1614
useLearningPathDestroy,
1715
useLearningPathUpdate,
18-
useLearningPathRelationshipMove,
19-
useLearningPathRelationshipCreate,
20-
useLearningPathRelationshipDestroy,
2116
} from "./index"
2217
import learningPathKeyFactory from "./keyFactory"
2318

2419
const factory = factories.learningResources
2520

26-
jest.mock("../learningResources/invalidation", () => {
27-
const actual = jest.requireActual("../learningResources/invalidation")
28-
return {
29-
__esModule: true,
30-
...actual,
31-
invalidateResourceQueries: jest.fn(),
32-
invalidateUserListQueries: jest.fn(),
33-
}
34-
})
35-
3621
/**
3722
* Assert that `hook` queries the API with the correct `url`, `method`, and
3823
* exposes the API's data.
@@ -172,6 +157,7 @@ describe("LearningPath CRUD", () => {
172157

173158
await waitFor(() => expect(result.current.isSuccess).toBe(true))
174159
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)
160+
175161
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
176162
"learningPaths",
177163
"list",
@@ -184,13 +170,23 @@ describe("LearningPath CRUD", () => {
184170
setMockResponse.delete(url, null)
185171

186172
const { wrapper, queryClient } = setupReactQueryTest()
173+
jest.spyOn(queryClient, "invalidateQueries")
174+
187175
const { result } = renderHook(useLearningPathDestroy, {
188176
wrapper,
189177
})
190178
result.current.mutate({ id: path.id })
191179
await waitFor(() => expect(result.current.isSuccess).toBe(true))
192180
expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
193-
expect(invalidateResourceQueries).toHaveBeenCalledWith(queryClient, path.id)
181+
182+
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
183+
"learningPaths",
184+
"list",
185+
])
186+
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
187+
"learningPaths",
188+
"membershipList",
189+
])
194190
})
195191

196192
test("useLearningPathUpdate calls correct API", async () => {
@@ -200,120 +196,22 @@ describe("LearningPath CRUD", () => {
200196
setMockResponse.patch(url, path)
201197

202198
const { wrapper, queryClient } = setupReactQueryTest()
199+
jest.spyOn(queryClient, "invalidateQueries")
200+
203201
const { result } = renderHook(useLearningPathUpdate, { wrapper })
204202
result.current.mutate(patch)
205203

206204
await waitFor(() => expect(result.current.isSuccess).toBe(true))
207205
expect(makeRequest).toHaveBeenCalledWith("patch", url, patch)
208206

209-
expect(invalidateResourceQueries).toHaveBeenCalledWith(queryClient, path.id)
210-
})
211-
212-
test("useLearningPathRelationshipMove calls correct API", async () => {
213-
const { relationship, pathUrls, keys } = makeData()
214-
const url = pathUrls.relationshipDetails
215-
setMockResponse.patch(url, null)
216-
217-
const { wrapper, queryClient } = setupReactQueryTest()
218-
jest.spyOn(queryClient, "invalidateQueries")
219-
const { result } = renderHook(useLearningPathRelationshipMove, { wrapper })
220-
result.current.mutate(relationship)
221-
222-
await waitFor(() => expect(result.current.isSuccess).toBe(true))
223-
expect(makeRequest).toHaveBeenCalledWith(
224-
"patch",
225-
url,
226-
expect.objectContaining({ position: relationship.position }),
227-
)
228-
229-
expect(queryClient.invalidateQueries).toHaveBeenCalledWith(
230-
keys.relationshipListing,
231-
)
207+
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
208+
"learningPaths",
209+
"list",
210+
])
211+
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
212+
"learningPaths",
213+
"detail",
214+
path.id,
215+
])
232216
})
233-
234-
test.each([{ isChildFeatured: false }, { isChildFeatured: true }])(
235-
"useLearningPathRelationshipCreate calls correct API and patches featured resources",
236-
async ({ isChildFeatured }) => {
237-
const { relationship, pathUrls, resourceWithoutList } = makeData()
238-
239-
const featured = factory.resources({ count: 3 })
240-
if (isChildFeatured) {
241-
featured.results[0] = resourceWithoutList
242-
}
243-
setMockResponse.get(urls.learningResources.featured(), featured)
244-
245-
const url = pathUrls.relationshipList
246-
const requestData = {
247-
child: relationship.child,
248-
parent: relationship.parent,
249-
position: relationship.position,
250-
}
251-
setMockResponse.post(url, relationship)
252-
253-
const { wrapper, queryClient } = setupReactQueryTest()
254-
const { result } = renderHook(useLearningPathRelationshipCreate, {
255-
wrapper,
256-
})
257-
const { result: featuredResult } = renderHook(
258-
useFeaturedLearningResourcesList,
259-
{ wrapper },
260-
)
261-
await waitFor(() => expect(featuredResult.current.data).toBe(featured))
262-
263-
result.current.mutate(requestData)
264-
265-
await waitFor(() => expect(result.current.isSuccess).toBe(true))
266-
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)
267-
268-
expect(invalidateResourceQueries).toHaveBeenCalledWith(
269-
queryClient,
270-
relationship.child,
271-
{ skipFeatured: false },
272-
)
273-
expect(invalidateResourceQueries).toHaveBeenCalledWith(
274-
queryClient,
275-
relationship.parent,
276-
)
277-
},
278-
)
279-
280-
test.each([{ isChildFeatured: false }, { isChildFeatured: true }])(
281-
"useLearningPathRelationshipDestroy calls correct API and patches child resource cache (isChildFeatured=$isChildFeatured)",
282-
async ({ isChildFeatured }) => {
283-
const { relationship, pathUrls } = makeData()
284-
const url = pathUrls.relationshipDetails
285-
286-
const featured = factory.resources({ count: 3 })
287-
if (isChildFeatured) {
288-
featured.results[0] = relationship.resource
289-
}
290-
setMockResponse.get(urls.learningResources.featured(), featured)
291-
292-
setMockResponse.delete(url, null)
293-
const { wrapper, queryClient } = setupReactQueryTest()
294-
295-
const { result } = renderHook(useLearningPathRelationshipDestroy, {
296-
wrapper,
297-
})
298-
const { result: featuredResult } = renderHook(
299-
useFeaturedLearningResourcesList,
300-
{ wrapper },
301-
)
302-
303-
await waitFor(() => expect(featuredResult.current.data).toBe(featured))
304-
result.current.mutate(relationship)
305-
await waitFor(() => expect(result.current.isSuccess).toBe(true))
306-
307-
expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
308-
expect(invalidateResourceQueries).toHaveBeenCalledWith(
309-
queryClient,
310-
relationship.child,
311-
{ skipFeatured: false },
312-
)
313-
expect(invalidateResourceQueries).toHaveBeenCalledWith(
314-
queryClient,
315-
relationship.parent,
316-
)
317-
},
318-
)
319217
})

frontends/api/src/hooks/learningPaths/index.ts

Lines changed: 24 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ import type {
1010
LearningpathsApiLearningpathsItemsListRequest as ItemsListRequest,
1111
LearningpathsApiLearningpathsCreateRequest as CreateRequest,
1212
LearningpathsApiLearningpathsDestroyRequest as DestroyRequest,
13-
LearningPathRelationshipRequest,
14-
MicroLearningPathRelationship,
1513
LearningPathResource,
1614
} from "../../generated/v1"
1715
import { learningPathsApi } from "../../clients"
1816
import learningPaths from "./keyFactory"
19-
import { invalidateResourceQueries } from "../learningResources/invalidation"
17+
import { useUserIsAuthenticated } from "api/hooks/user"
2018

2119
const useLearningPathsList = (
2220
params: ListRequest = {},
@@ -74,8 +72,9 @@ const useLearningPathUpdate = () => {
7472
id: params.id,
7573
PatchedLearningPathResourceRequest: params,
7674
}),
77-
onSettled: (_data, _err, vars) => {
78-
invalidateResourceQueries(queryClient, vars.id)
75+
onSettled: (data, err, vars) => {
76+
queryClient.invalidateQueries(learningPaths.list._def)
77+
queryClient.invalidateQueries(learningPaths.detail(vars.id).queryKey)
7978
},
8079
})
8180
}
@@ -85,8 +84,9 @@ const useLearningPathDestroy = () => {
8584
return useMutation({
8685
mutationFn: (params: DestroyRequest) =>
8786
learningPathsApi.learningpathsDestroy(params),
88-
onSettled: (_data, _err, vars) => {
89-
invalidateResourceQueries(queryClient, vars.id)
87+
onSettled: () => {
88+
queryClient.invalidateQueries(learningPaths.list._def)
89+
queryClient.invalidateQueries(learningPaths.membershipList._def)
9090
},
9191
})
9292
}
@@ -96,22 +96,6 @@ interface ListItemMoveRequest {
9696
id: number
9797
position?: number
9898
}
99-
const useLearningPathRelationshipMove = () => {
100-
const queryClient = useQueryClient()
101-
return useMutation({
102-
mutationFn: ({ parent, id, position }: ListItemMoveRequest) =>
103-
learningPathsApi.learningpathsItemsPartialUpdate({
104-
learning_resource_id: parent,
105-
id,
106-
PatchedLearningPathRelationshipRequest: { position },
107-
}),
108-
onSettled: (_data, _err, vars) => {
109-
queryClient.invalidateQueries(
110-
learningPaths.detail(vars.parent)._ctx.infiniteItems._def,
111-
)
112-
},
113-
})
114-
}
11599

116100
const useLearningPathListItemMove = () => {
117101
const queryClient = useQueryClient()
@@ -131,47 +115,26 @@ const useLearningPathListItemMove = () => {
131115
})
132116
}
133117

134-
const useLearningPathRelationshipCreate = () => {
135-
const queryClient = useQueryClient()
136-
return useMutation({
137-
mutationFn: (params: LearningPathRelationshipRequest) =>
138-
learningPathsApi.learningpathsItemsCreate({
139-
learning_resource_id: params.parent,
140-
LearningPathRelationshipRequest: params,
141-
}),
142-
onSettled: (_response, _err, vars) => {
143-
invalidateResourceQueries(
144-
queryClient,
145-
vars.child,
146-
// do NOT skip invalidating the /featured/ lists,
147-
// Changing a learning path might change the members of the featured
148-
// lists.
149-
{ skipFeatured: false },
150-
)
151-
invalidateResourceQueries(queryClient, vars.parent)
118+
const useIsLearningPathMember = (resourceId?: number) => {
119+
return useQuery({
120+
...learningPaths.membershipList(),
121+
select: (data) => {
122+
return !!data.find((relationship) => relationship.child === resourceId)
152123
},
124+
enabled: useUserIsAuthenticated() && !!resourceId,
153125
})
154126
}
155127

156-
const useLearningPathRelationshipDestroy = () => {
157-
const queryClient = useQueryClient()
158-
return useMutation({
159-
mutationFn: (params: MicroLearningPathRelationship) =>
160-
learningPathsApi.learningpathsItemsDestroy({
161-
id: params.id,
162-
learning_resource_id: params.parent,
163-
}),
164-
onSettled: (_response, _err, vars) => {
165-
invalidateResourceQueries(
166-
queryClient,
167-
vars.child,
168-
// do NOT skip invalidating the /featured/ lists,
169-
// Changing a learning path might change the members of the featured
170-
// lists.
171-
{ skipFeatured: false },
172-
)
173-
invalidateResourceQueries(queryClient, vars.parent)
128+
const useLearningPathMemberList = (resourceId?: number) => {
129+
return useQuery({
130+
...learningPaths.membershipList(),
131+
132+
select: (data) => {
133+
return data
134+
.filter((relationship) => relationship.child === resourceId)
135+
.map((relationship) => relationship.parent.toString())
174136
},
137+
enabled: useUserIsAuthenticated() && !!resourceId,
175138
})
176139
}
177140

@@ -182,8 +145,7 @@ export {
182145
useLearningPathCreate,
183146
useLearningPathUpdate,
184147
useLearningPathDestroy,
185-
useLearningPathRelationshipMove,
186148
useLearningPathListItemMove,
187-
useLearningPathRelationshipCreate,
188-
useLearningPathRelationshipDestroy,
149+
useIsLearningPathMember,
150+
useLearningPathMemberList,
189151
}

frontends/api/src/hooks/learningPaths/keyFactory.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
LearningpathsApiLearningpathsListRequest as ListRequest,
77
PaginatedLearningPathRelationshipList,
88
} from "../../generated/v1"
9+
import { clearListMemberships } from "../learningResources/keyFactory"
910

1011
const learningPaths = createQueryKeys("learningPaths", {
1112
detail: (id: number) => ({
@@ -18,15 +19,22 @@ const learningPaths = createQueryKeys("learningPaths", {
1819
contextQueries: {
1920
infiniteItems: (itemsP: ItemsListRequest) => ({
2021
queryKey: [itemsP],
21-
queryFn: ({ pageParam }: { pageParam?: string } = {}) => {
22+
queryFn: async ({ pageParam }: { pageParam?: string } = {}) => {
2223
// Use generated API for first request, then use next parameter
2324
const request = pageParam
2425
? axiosInstance.request<PaginatedLearningPathRelationshipList>({
2526
method: "get",
2627
url: pageParam,
2728
})
2829
: learningPathsApi.learningpathsItemsList(itemsP)
29-
return request.then((res) => res.data)
30+
const { data } = await request
31+
return {
32+
...data,
33+
results: data.results.map((relation) => ({
34+
...relation,
35+
resource: clearListMemberships(relation.resource),
36+
})),
37+
}
3038
},
3139
}),
3240
},
@@ -37,6 +45,13 @@ const learningPaths = createQueryKeys("learningPaths", {
3745
return learningPathsApi.learningpathsList(params).then((res) => res.data)
3846
},
3947
}),
48+
membershipList: () => ({
49+
queryKey: ["membershipList"],
50+
queryFn: async () => {
51+
const { data } = await learningPathsApi.learningpathsMembershipList()
52+
return data
53+
},
54+
}),
4055
})
4156

4257
export default learningPaths

frontends/api/src/hooks/learningResources/index.test.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@ import { UseQueryResult } from "@tanstack/react-query"
1111

1212
const factory = factories.learningResources
1313

14-
jest.mock("./invalidation", () => {
15-
const actual = jest.requireActual("./invalidation")
16-
return {
17-
__esModule: true,
18-
...actual,
19-
invalidateResourceQueries: jest.fn(),
20-
invalidateUserListQueries: jest.fn(),
21-
}
22-
})
23-
2414
/**
2515
* Assert that `hook` queries the API with the correct `url`, `method`, and
2616
* exposes the API's data.

0 commit comments

Comments
 (0)