diff --git a/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx b/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx index 87704724bd..bbfb594b2c 100644 --- a/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx +++ b/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx @@ -5,6 +5,7 @@ import { renderWithProviders, screen, waitFor, + within, } from "@/test-utils" import LearningResourceDrawer, { useOpenLearningResourceDrawer, @@ -12,6 +13,8 @@ import LearningResourceDrawer, { import { urls, factories, setMockResponse } from "api/test-utils" import { LearningResourceExpanded } from "ol-components" import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls" +import { ResourceTypeEnum } from "api" +import invariant from "tiny-invariant" jest.mock("ol-components", () => { const actual = jest.requireActual("ol-components") @@ -40,6 +43,7 @@ describe("LearningResourceDrawer", () => { ])( "Renders drawer content when resource=id is in the URL and captures the view if PostHog $descriptor", async ({ enablePostHog }) => { + setMockResponse.get(urls.userMe.get(), {}) APP_SETTINGS.POSTHOG = { api_key: enablePostHog ? "test1234" : "", // pragma: allowlist secret } @@ -95,4 +99,84 @@ describe("LearningResourceDrawer", () => { dog: "woof", }) }) + + test.each([ + { + isLearningPathEditor: true, + isAuthenticated: true, + expectAddToLearningPathButton: true, + expectAddToUserListButton: true, + }, + { + isLearningPathEditor: false, + isAuthenticated: true, + expectAddToLearningPathButton: false, + expectAddToUserListButton: true, + }, + { + isLearningPathEditor: false, + isAuthenticated: false, + expectAddToLearningPathButton: false, + expectAddToUserListButton: false, + }, + ])( + "Renders info section list buttons correctly", + async ({ + isLearningPathEditor, + isAuthenticated, + expectAddToLearningPathButton, + expectAddToUserListButton, + }) => { + const resource = factories.learningResources.resource({ + resource_type: ResourceTypeEnum.Course, + runs: [ + factories.learningResources.run({ + languages: ["en-us", "es-es", "fr-fr"], + }), + ], + }) + setMockResponse.get( + urls.learningResources.details({ id: resource.id }), + resource, + ) + + renderWithProviders(, { + url: `?resource=${resource.id}`, + user: { + is_learning_path_editor: isLearningPathEditor, + is_authenticated: isAuthenticated, + }, + }) + + expect(LearningResourceExpanded).toHaveBeenCalled() + + await waitFor(() => { + expectProps(LearningResourceExpanded, { resource }) + }) + + const section = screen + .getByRole("heading", { name: "Info" }) + .closest("section") + invariant(section) + + if (!isAuthenticated) { + const buttons = within(section).queryAllByRole("button") + expect(buttons).toHaveLength(0) + return + } else { + const buttons = within(section).getAllByRole("button") + const expectedButtons = + expectAddToLearningPathButton && expectAddToUserListButton ? 2 : 1 + expect(buttons).toHaveLength(expectedButtons) + expect( + !!within(section).queryByRole("button", { + name: "Add to Learning Path", + }), + ).toBe(expectAddToLearningPathButton) + expect( + !!within(section).queryByRole("button", { name: "Add to User List" }), + ).toBe(expectAddToUserListButton) + } + }, + ) }) diff --git a/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.tsx b/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.tsx index 754b43957b..cc762d7b83 100644 --- a/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.tsx +++ b/frontends/mit-learn/src/page-components/LearningResourceDrawer/LearningResourceDrawer.tsx @@ -1,15 +1,24 @@ -import React, { useEffect, useCallback } from "react" +import React, { useEffect, useCallback, useMemo } from "react" import { RoutedDrawer, LearningResourceExpanded, imgConfigs, } from "ol-components" -import type { RoutedDrawerProps } from "ol-components" +import type { + LearningResourceCardProps, + RoutedDrawerProps, +} from "ol-components" import { useLearningResourcesDetail } from "api/hooks/learningResources" import { useSearchParams, useLocation } from "react-router-dom" import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls" import { usePostHog } from "posthog-js/react" import MetaTags from "@/page-components/MetaTags/MetaTags" +import { useUserMe } from "api/hooks/user" +import NiceModal from "@ebay/nice-modal-react" +import { + AddToLearningPathDialog, + AddToUserListDialog, +} from "../Dialogs/AddToListDialog" const RESOURCE_DRAWER_PARAMS = [RESOURCE_DRAWER_QUERY_PARAM] as const @@ -56,6 +65,25 @@ const DrawerContent: React.FC<{ resourceId: number }> = ({ resourceId }) => { const resource = useLearningResourcesDetail(Number(resourceId)) + const { data: user } = useUserMe() + const handleAddToLearningPathClick: LearningResourceCardProps["onAddToLearningPathClick"] = + useMemo(() => { + if (user?.is_learning_path_editor) { + return (event, resourceId: number) => { + NiceModal.show(AddToLearningPathDialog, { resourceId }) + } + } + return null + }, [user]) + const handleAddToUserListClick: LearningResourceCardProps["onAddToUserListClick"] = + useMemo(() => { + if (user?.is_authenticated) { + return (event, resourceId: number) => { + NiceModal.show(AddToUserListDialog, { resourceId }) + } + } + return null + }, [user]) useCapturePageView(Number(resourceId)) return ( @@ -70,6 +98,9 @@ const DrawerContent: React.FC<{ ) diff --git a/frontends/ol-ckeditor/src/types/settings.d.ts b/frontends/ol-ckeditor/src/types/settings.d.ts index fbb37f3706..237e6ca039 100644 --- a/frontends/ol-ckeditor/src/types/settings.d.ts +++ b/frontends/ol-ckeditor/src/types/settings.d.ts @@ -8,5 +8,6 @@ export declare global { MITOL_API_BASE_URL: string PUBLIC_URL: string SITE_NAME: string + CSRF_COOKIE_NAME: string } } diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx index 315f2a3e62..67c767fb42 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx @@ -13,6 +13,8 @@ import { RiTranslate2, RiAwardLine, RiPresentationLine, + RiMenuAddLine, + RiBookmarkLine, } from "@remixicon/react" import { LearningResource, LearningResourceRun, ResourceTypeEnum } from "api" import { @@ -21,6 +23,9 @@ import { } from "ol-utilities" import { theme } from "../ThemeProvider/ThemeProvider" import Typography from "@mui/material/Typography" +import type { User } from "api/hooks/user" +import { CardActionButton } from "../LearningResourceCard/LearningResourceListCard" +import { LearningResourceCardProps } from "../LearningResourceCard/LearningResourceCard" const InfoItems = styled.section` display: flex; @@ -28,6 +33,17 @@ const InfoItems = styled.section` gap: 16px; ` +const InfoHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +` + +const ListButtonContainer = styled.div` + display: flex; + gap: 8px; +` + const InfoItemContainer = styled.div` display: flex; align-items: flex-start; @@ -223,14 +239,23 @@ const InfoItem = ({ label, Icon, value }: InfoItemProps) => { const InfoSection = ({ resource, run, + user, + onAddToLearningPathClick, + onAddToUserListClick, }: { resource?: LearningResource run?: LearningResourceRun + user?: User + onAddToLearningPathClick?: LearningResourceCardProps["onAddToLearningPathClick"] + onAddToUserListClick?: LearningResourceCardProps["onAddToUserListClick"] }) => { if (!resource) { return null } + const inUserList = !!resource?.user_list_parents?.length + const inLearningPath = !!resource?.learning_path_parents?.length + const infoItems = INFO_ITEMS.map(({ label, Icon, selector }) => ({ label, Icon, @@ -243,9 +268,39 @@ const InfoSection = ({ return ( - - Info - + + + Info + + + {user?.is_learning_path_editor && ( + + onAddToLearningPathClick + ? onAddToLearningPathClick(event, resource.id) + : null + } + > + + + )} + {user?.is_authenticated && ( + + onAddToUserListClick + ? onAddToUserListClick(event, resource.id) + : null + } + > + + + )} + + {infoItems.map((props, index) => ( ))} diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx index a76f386725..8949a85719 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx @@ -20,6 +20,8 @@ import type { SimpleSelectProps } from "../SimpleSelect/SimpleSelect" import { EmbedlyCard } from "../EmbedlyCard/EmbedlyCard" import { PlatformLogo, PLATFORMS } from "../Logo/Logo" import InfoSection from "./InfoSection" +import type { User } from "api/hooks/user" +import { LearningResourceCardProps } from "../LearningResourceCard/LearningResourceCard" const Container = styled.div<{ padTop?: boolean }>` display: flex; @@ -138,7 +140,10 @@ const OnPlatform = styled.span` type LearningResourceExpandedProps = { resource?: LearningResource + user?: User imgConfig: EmbedlyConfig + onAddToLearningPathClick?: LearningResourceCardProps["onAddToLearningPathClick"] + onAddToUserListClick?: LearningResourceCardProps["onAddToUserListClick"] } const ImageSection: React.FC<{ @@ -314,7 +319,10 @@ const formatRunDate = ( const LearningResourceExpanded: React.FC = ({ resource, + user, imgConfig, + onAddToLearningPathClick, + onAddToUserListClick, }) => { const [selectedRun, setSelectedRun] = useState(resource?.runs?.[0]) @@ -411,7 +419,13 @@ const LearningResourceExpanded: React.FC = ({ - + ) }