Skip to content

Commit 51bdf22

Browse files
authored
add topics carousels to v2 drawer (#1912)
* add topic carousels and exclude current drawer resource from any results * add a test for excludeResourceId * fix tests * update topic carousel titles * only display topic carousels for subtopics (topics that have a parent) and limit to 2
1 parent 4b543e4 commit 51bdf22

File tree

4 files changed

+119
-46
lines changed

4 files changed

+119
-46
lines changed

frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawerV2.test.tsx

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import LearningResourceDrawerV2 from "./LearningResourceDrawerV2"
1010
import { urls, factories, setMockResponse } from "api/test-utils"
1111
import { LearningResourceExpandedV2 } from "ol-components"
1212
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
13-
import { ResourceTypeEnum } from "api"
13+
import { CourseResource, LearningResource, ResourceTypeEnum } from "api"
14+
import { ControlledPromise } from "ol-test-utilities"
1415

1516
jest.mock("ol-components", () => {
1617
const actual = jest.requireActual("ol-components")
@@ -20,6 +21,22 @@ jest.mock("ol-components", () => {
2021
}
2122
})
2223

24+
const makeSearchResponse = (results: CourseResource[] | LearningResource[]) => {
25+
const responseData = {
26+
metadata: {
27+
suggestions: [],
28+
aggregations: {},
29+
},
30+
count: results.length,
31+
results: results,
32+
next: null,
33+
previous: null,
34+
}
35+
const promise = new ControlledPromise()
36+
promise.resolve(responseData)
37+
return responseData
38+
}
39+
2340
const mockedPostHogCapture = jest.fn()
2441

2542
jest.mock("posthog-js/react", () => ({
@@ -32,6 +49,52 @@ jest.mock("posthog-js/react", () => ({
3249
},
3350
}))
3451

52+
const setupApis = (resource: LearningResource) => {
53+
setMockResponse.get(
54+
urls.learningResources.details({ id: resource.id }),
55+
resource,
56+
)
57+
const count = 10
58+
const similarResources = factories.learningResources.resources({
59+
count,
60+
}).results
61+
setMockResponse.get(urls.userMe.get(), null, { code: 403 })
62+
setMockResponse.get(
63+
urls.learningResources.details({ id: resource.id }),
64+
resource,
65+
)
66+
setMockResponse.get(
67+
urls.learningResources.vectorSimilar({ id: resource.id }),
68+
similarResources,
69+
)
70+
const topicsCourses: CourseResource[] = []
71+
resource.topics?.forEach((topic) => {
72+
const topicCourses = factories.learningResources.courses({ count: 10 })
73+
topicCourses.results.map((course) => {
74+
course.topics = [factories.learningResources.topic({ name: topic.name })]
75+
})
76+
topicsCourses.push(...topicCourses.results)
77+
})
78+
resource.topics?.forEach((topic) => {
79+
setMockResponse.get(
80+
expect.stringContaining(
81+
urls.search.resources({
82+
limit: 12,
83+
resource_type: ["course"],
84+
sortby: "-views",
85+
topic: [topic.name],
86+
}),
87+
),
88+
makeSearchResponse(
89+
topicsCourses.filter(
90+
(course) => course.topics?.[0].name === topic.name,
91+
),
92+
),
93+
)
94+
})
95+
return { resource, similarResources }
96+
}
97+
3598
describe("LearningResourceDrawerV2", () => {
3699
it.each([
37100
{ descriptor: "is enabled", enablePostHog: true },
@@ -46,18 +109,7 @@ describe("LearningResourceDrawerV2", () => {
46109
? "12345abcdef" // pragma: allowlist secret
47110
: ""
48111
const resource = factories.learningResources.resource()
49-
setMockResponse.get(
50-
urls.learningResources.details({ id: resource.id }),
51-
resource,
52-
)
53-
setMockResponse.get(
54-
urls.learningResources.similar({ id: resource.id }),
55-
[],
56-
)
57-
setMockResponse.get(
58-
urls.learningResources.vectorSimilar({ id: resource.id }),
59-
[],
60-
)
112+
setupApis(resource)
61113

62114
renderWithProviders(<LearningResourceDrawerV2 />, {
63115
url: `?dog=woof&${RESOURCE_DRAWER_QUERY_PARAM}=${resource.id}`,
@@ -114,18 +166,7 @@ describe("LearningResourceDrawerV2", () => {
114166
}),
115167
],
116168
})
117-
setMockResponse.get(
118-
urls.learningResources.details({ id: resource.id }),
119-
resource,
120-
)
121-
setMockResponse.get(
122-
urls.learningResources.similar({ id: resource.id }),
123-
[],
124-
)
125-
setMockResponse.get(
126-
urls.learningResources.vectorSimilar({ id: resource.id }),
127-
[],
128-
)
169+
setupApis(resource)
129170
const user = factories.user.user({
130171
is_learning_path_editor: isLearningPathEditor,
131172
})
@@ -169,19 +210,7 @@ describe("LearningResourceDrawerV2", () => {
169210
}),
170211
],
171212
})
172-
const count = 10
173-
const similarResources = factories.learningResources.resources({
174-
count,
175-
}).results
176-
setMockResponse.get(urls.userMe.get(), null, { code: 403 })
177-
setMockResponse.get(
178-
urls.learningResources.details({ id: resource.id }),
179-
resource,
180-
)
181-
setMockResponse.get(
182-
urls.learningResources.vectorSimilar({ id: resource.id }),
183-
similarResources,
184-
)
213+
const { similarResources } = setupApis(resource)
185214
renderWithProviders(<LearningResourceDrawerV2 />, {
186215
url: `?resource=${resource.id}`,
187216
})

frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawerV2.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { usePostHog } from "posthog-js/react"
2222
import ResourceCarousel from "../ResourceCarousel/ResourceCarousel"
2323
import { useIsLearningPathMember } from "api/hooks/learningPaths"
2424
import { useIsUserListMember } from "api/hooks/userLists"
25+
import { TopicCarouselConfig } from "@/app-pages/DashboardPage/carousels"
2526

2627
const RESOURCE_DRAWER_PARAMS = [RESOURCE_DRAWER_QUERY_PARAM] as const
2728

@@ -108,16 +109,31 @@ const DrawerContent: React.FC<{
108109
},
109110
},
110111
]}
112+
excludeResourceId={resourceId}
111113
/>
112114
)
115+
const topics = resource.data?.topics
116+
?.filter((topic) => topic.parent)
117+
.slice(0, 2)
118+
const topicCarousels = topics?.map((topic) => (
119+
<ResourceCarousel
120+
key={topic.id}
121+
titleComponent="p"
122+
titleVariant="subtitle1"
123+
title={`Learning Resources in "${topic.name}"`}
124+
config={TopicCarouselConfig(topic.name)}
125+
data-testid={`topic-carousel-${topic}`}
126+
excludeResourceId={resourceId}
127+
/>
128+
))
113129

114130
return (
115131
<>
116132
<LearningResourceExpandedV2
117133
imgConfig={imgConfigs.large}
118134
resourceId={resourceId}
119135
resource={resource.data}
120-
carousels={[similarResourcesCarousel]}
136+
carousels={[similarResourcesCarousel, ...(topicCarousels || [])]}
121137
user={user}
122138
shareUrl={`${window.location.origin}/search?${RESOURCE_DRAWER_QUERY_PARAM}=${resourceId}`}
123139
inLearningPath={inLearningPath}

frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,28 @@ describe("ResourceCarousel", () => {
220220
expect(title.tagName).toBe(expectedTag)
221221
},
222222
)
223+
224+
it("Excludes a resource if excludeResourceId is provided", async () => {
225+
const config: ResourceCarouselProps["config"] = [
226+
{
227+
label: "Resources",
228+
data: {
229+
type: "resources",
230+
params: { resource_type: ["course", "program"], professional: true },
231+
},
232+
},
233+
]
234+
const { resources } = setupApis()
235+
renderWithProviders(
236+
<ResourceCarousel
237+
titleComponent="h1"
238+
title="My Carousel"
239+
config={config}
240+
excludeResourceId={resources.list.results[1].id}
241+
/>,
242+
)
243+
await screen.findByText(resources.list.results[0].title)
244+
await screen.findByText(resources.list.results[2].title)
245+
expect(screen.queryByText(resources.list.results[1].title)).toBeNull()
246+
})
223247
})

frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ type ResourceCarouselProps = {
177177
*/
178178
titleComponent?: React.ElementType
179179
titleVariant?: TypographyProps["variant"]
180+
excludeResourceId?: number
180181
}
181182
/**
182183
* A tabbed carousel that fetches resources based on the configuration provided.
@@ -197,6 +198,7 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
197198
"data-testid": dataTestId,
198199
titleComponent = "h4",
199200
titleVariant = "h4",
201+
excludeResourceId,
200202
}) => {
201203
const [tab, setTab] = React.useState("0")
202204
const [ref, setRef] = React.useState<HTMLDivElement | null>(null)
@@ -306,13 +308,15 @@ const ResourceCarousel: React.FC<ResourceCarouselProps> = ({
306308
{...tabConfig.cardProps}
307309
/>
308310
))
309-
: resources.map((resource) => (
310-
<ResourceCard
311-
key={resource.id}
312-
resource={resource}
313-
{...tabConfig.cardProps}
314-
/>
315-
))}
311+
: resources.map((resource) =>
312+
resource.id !== excludeResourceId ? (
313+
<ResourceCard
314+
key={resource.id}
315+
resource={resource}
316+
{...tabConfig.cardProps}
317+
/>
318+
) : null,
319+
)}
316320
</StyledCarousel>
317321
)}
318322
</PanelChildren>

0 commit comments

Comments
 (0)