Skip to content

Commit f0036c9

Browse files
committed
put the new drawer behind a posthog feature flag
1 parent c7ba673 commit f0036c9

17 files changed

+1750
-204
lines changed

frontends/main/src/common/feature_flags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44
export enum FeatureFlags {
55
EnableEcommerce = "enable-ecommerce",
6+
DrawerV2Enabled = "lr_drawer_v2",
67
}

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

Lines changed: 12 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,18 @@
1-
import React, { Suspense, useCallback, useEffect, useMemo } from "react"
2-
import {
3-
RoutedDrawer,
4-
LearningResourceExpanded,
5-
imgConfigs,
6-
} from "ol-components"
7-
import type {
8-
LearningResourceCardProps,
9-
RoutedDrawerProps,
10-
} from "ol-components"
11-
import { useLearningResourcesDetail } from "api/hooks/learningResources"
12-
import { useSearchParams, ReadonlyURLSearchParams } from "next/navigation"
13-
1+
import React, { useCallback } from "react"
142
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
15-
import { useUserMe } from "api/hooks/user"
16-
import NiceModal from "@ebay/nice-modal-react"
17-
import {
18-
AddToLearningPathDialog,
19-
AddToUserListDialog,
20-
} from "../Dialogs/AddToListDialog"
21-
import { SignupPopover } from "../SignupPopover/SignupPopover"
22-
import { usePostHog } from "posthog-js/react"
23-
24-
const RESOURCE_DRAWER_PARAMS = [RESOURCE_DRAWER_QUERY_PARAM] as const
25-
26-
const useCapturePageView = (resourceId: number) => {
27-
const { data, isSuccess } = useLearningResourcesDetail(Number(resourceId))
28-
const posthog = usePostHog()
29-
const apiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY
30-
31-
useEffect(() => {
32-
if (!apiKey || apiKey.length < 1) return
33-
if (!isSuccess) return
34-
posthog.capture("lrd_view", {
35-
resourceId: data?.id,
36-
readableId: data?.readable_id,
37-
platformCode: data?.platform?.code,
38-
resourceType: data?.resource_type,
39-
})
40-
}, [
41-
isSuccess,
42-
posthog,
43-
data?.id,
44-
data?.readable_id,
45-
data?.platform?.code,
46-
data?.resource_type,
47-
apiKey,
48-
])
49-
}
50-
51-
/**
52-
* Convert HTML to plaintext, removing any HTML tags.
53-
* This conversion method has some issues:
54-
* 1. It is unsafe for untrusted HTML
55-
* 2. It must be run in a browser, not on a server.
56-
*/
57-
// eslint-disable-next-line camelcase
58-
// const unsafe_html2plaintext = (text: string) => {
59-
// const div = document.createElement("div")
60-
// div.innerHTML = text
61-
// return div.textContent || div.innerText || ""
62-
// }
63-
64-
const DrawerContent: React.FC<{
65-
resourceId: number
66-
closeDrawer: () => void
67-
}> = ({ resourceId, closeDrawer }) => {
68-
const resource = useLearningResourcesDetail(Number(resourceId))
69-
const [signupEl, setSignupEl] = React.useState<HTMLElement | null>(null)
70-
const { data: user } = useUserMe()
71-
const handleAddToLearningPathClick: LearningResourceCardProps["onAddToLearningPathClick"] =
72-
useMemo(() => {
73-
if (user?.is_learning_path_editor) {
74-
return (event, resourceId: number) => {
75-
NiceModal.show(AddToLearningPathDialog, { resourceId })
76-
}
77-
}
78-
return null
79-
}, [user])
80-
const handleAddToUserListClick: LearningResourceCardProps["onAddToUserListClick"] =
81-
useMemo(() => {
82-
return (event, resourceId: number) => {
83-
if (!user?.is_authenticated) {
84-
setSignupEl(event.currentTarget)
85-
return
86-
}
87-
NiceModal.show(AddToUserListDialog, { resourceId })
88-
}
89-
}, [user])
90-
useCapturePageView(Number(resourceId))
91-
92-
return (
93-
<>
94-
<LearningResourceExpanded
95-
imgConfig={imgConfigs.large}
96-
resource={resource.data}
97-
user={user}
98-
onAddToLearningPathClick={handleAddToLearningPathClick}
99-
onAddToUserListClick={handleAddToUserListClick}
100-
closeDrawer={closeDrawer}
101-
/>
102-
<SignupPopover anchorEl={signupEl} onClose={() => setSignupEl(null)} />
103-
</>
104-
)
105-
}
106-
107-
const PAPER_PROPS: RoutedDrawerProps["PaperProps"] = {
108-
sx: {
109-
maxWidth: (theme) => ({
110-
[theme.breakpoints.up("md")]: {
111-
maxWidth: theme.breakpoints.values.md,
112-
},
113-
[theme.breakpoints.down("sm")]: {
114-
maxWidth: "100%",
115-
},
116-
}),
117-
minWidth: (theme) => ({
118-
[theme.breakpoints.down("md")]: {
119-
minWidth: "100%",
120-
},
121-
}),
122-
},
123-
}
3+
import { ReadonlyURLSearchParams, useSearchParams } from "next/navigation"
4+
import { useFeatureFlagEnabled } from "posthog-js/react"
5+
import LearningResourceDrawerV2 from "./LearningResourceDrawerV2"
6+
import LearningResourceDrawerV1 from "./LearningResourceDrawerV1"
7+
import { FeatureFlags } from "@/common/feature_flags"
1248

1259
const LearningResourceDrawer = () => {
126-
return (
127-
<Suspense>
128-
<RoutedDrawer
129-
anchor="right"
130-
requiredParams={RESOURCE_DRAWER_PARAMS}
131-
PaperProps={PAPER_PROPS}
132-
hideCloseButton={true}
133-
>
134-
{({ params, closeDrawer }) => {
135-
return (
136-
<DrawerContent
137-
resourceId={Number(params.resource)}
138-
closeDrawer={closeDrawer}
139-
/>
140-
)
141-
}}
142-
</RoutedDrawer>
143-
</Suspense>
144-
)
10+
const drawerV2 = useFeatureFlagEnabled(FeatureFlags.DrawerV2Enabled)
11+
// console.log(`LearningResourceDrawer: drawerV2=${drawerV2}`)
12+
if (drawerV2 === undefined) {
13+
return null
14+
}
15+
return drawerV2 ? <LearningResourceDrawerV2 /> : <LearningResourceDrawerV1 />
14516
}
14617

14718
const getOpenDrawerSearchParams = (
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React from "react"
2+
import {
3+
expectProps,
4+
renderWithProviders,
5+
screen,
6+
waitFor,
7+
within,
8+
} from "@/test-utils"
9+
import LearningResourceDrawerV1 from "./LearningResourceDrawerV1"
10+
import { urls, factories, setMockResponse } from "api/test-utils"
11+
import { LearningResourceExpandedV1 } from "ol-components"
12+
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
13+
import { ResourceTypeEnum } from "api"
14+
import invariant from "tiny-invariant"
15+
16+
jest.mock("ol-components", () => {
17+
const actual = jest.requireActual("ol-components")
18+
return {
19+
...actual,
20+
LearningResourceExpandedV1: jest.fn(actual.LearningResourceExpandedV1),
21+
}
22+
})
23+
24+
const mockedPostHogCapture = jest.fn()
25+
26+
jest.mock("posthog-js/react", () => ({
27+
PostHogProvider: (props: { children: React.ReactNode }) => (
28+
<div data-testid="phProvider">{props.children}</div>
29+
),
30+
31+
usePostHog: () => {
32+
return { capture: mockedPostHogCapture }
33+
},
34+
}))
35+
36+
describe("LearningResourceDrawerV1", () => {
37+
it.each([
38+
{ descriptor: "is enabled", enablePostHog: true },
39+
{ descriptor: "is not enabled", enablePostHog: false },
40+
])(
41+
"Renders drawer content when resource=id is in the URL and captures the view if PostHog $descriptor",
42+
async ({ enablePostHog }) => {
43+
setMockResponse.get(urls.userMe.get(), {})
44+
process.env.NEXT_PUBLIC_POSTHOG_API_KEY = enablePostHog
45+
? "12345abcdef" // pragma: allowlist secret
46+
: ""
47+
const resource = factories.learningResources.resource()
48+
setMockResponse.get(
49+
urls.learningResources.details({ id: resource.id }),
50+
resource,
51+
)
52+
53+
renderWithProviders(<LearningResourceDrawerV1 />, {
54+
url: `?dog=woof&${RESOURCE_DRAWER_QUERY_PARAM}=${resource.id}`,
55+
})
56+
expect(LearningResourceExpandedV1).toHaveBeenCalled()
57+
await waitFor(() => {
58+
expectProps(LearningResourceExpandedV1, { resource })
59+
})
60+
await screen.findByRole("heading", { name: resource.title })
61+
62+
if (enablePostHog) {
63+
expect(mockedPostHogCapture).toHaveBeenCalled()
64+
} else {
65+
expect(mockedPostHogCapture).not.toHaveBeenCalled()
66+
}
67+
},
68+
)
69+
70+
it("Does not render drawer content when resource=id is NOT in the URL", async () => {
71+
renderWithProviders(<LearningResourceDrawerV1 />, {
72+
url: "?dog=woof",
73+
})
74+
expect(LearningResourceExpandedV1).not.toHaveBeenCalled()
75+
})
76+
77+
test.each([
78+
{
79+
isLearningPathEditor: true,
80+
isAuthenticated: true,
81+
expectAddToLearningPathButton: true,
82+
},
83+
{
84+
isLearningPathEditor: false,
85+
isAuthenticated: true,
86+
expectAddToLearningPathButton: false,
87+
},
88+
{
89+
isLearningPathEditor: false,
90+
isAuthenticated: false,
91+
expectAddToLearningPathButton: false,
92+
},
93+
])(
94+
"Renders info section list buttons correctly",
95+
async ({
96+
isLearningPathEditor,
97+
isAuthenticated,
98+
expectAddToLearningPathButton,
99+
}) => {
100+
const resource = factories.learningResources.resource({
101+
resource_type: ResourceTypeEnum.Course,
102+
runs: [
103+
factories.learningResources.run({
104+
languages: ["en-us", "es-es", "fr-fr"],
105+
}),
106+
],
107+
})
108+
setMockResponse.get(
109+
urls.learningResources.details({ id: resource.id }),
110+
resource,
111+
)
112+
const user = factories.user.user({
113+
is_learning_path_editor: isLearningPathEditor,
114+
})
115+
if (isAuthenticated) {
116+
setMockResponse.get(urls.userMe.get(), user)
117+
} else {
118+
setMockResponse.get(urls.userMe.get(), null, { code: 403 })
119+
}
120+
121+
renderWithProviders(<LearningResourceDrawerV1 />, {
122+
url: `?resource=${resource.id}`,
123+
})
124+
125+
expect(LearningResourceExpandedV1).toHaveBeenCalled()
126+
127+
await waitFor(() => {
128+
expectProps(LearningResourceExpandedV1, { resource })
129+
})
130+
131+
const section = screen
132+
.getByRole("heading", { name: "Info" })
133+
.closest("section")
134+
invariant(section)
135+
136+
const buttons = within(section).getAllByRole("button")
137+
const expectedButtons = expectAddToLearningPathButton ? 2 : 1
138+
expect(buttons).toHaveLength(expectedButtons)
139+
expect(
140+
!!within(section).queryByRole("button", {
141+
name: "Add to Learning Path",
142+
}),
143+
).toBe(expectAddToLearningPathButton)
144+
},
145+
)
146+
})

0 commit comments

Comments
 (0)