diff --git a/frontends/mit-open/src/common/urls.ts b/frontends/mit-open/src/common/urls.ts index 51eaaa4076..be80a3b73e 100644 --- a/frontends/mit-open/src/common/urls.ts +++ b/frontends/mit-open/src/common/urls.ts @@ -8,10 +8,6 @@ export const LEARNINGPATH_LISTING = "/learningpaths/" export const LEARNINGPATH_VIEW = "/learningpaths/:id" export const learningPathsView = (id: number) => generatePath(LEARNINGPATH_VIEW, { id: String(id) }) -export const USERLIST_LISTING = "/userlists/" -export const USERLIST_VIEW = "/userlists/:id" -export const userListView = (id: number) => - generatePath(USERLIST_VIEW, { id: String(id) }) export const PROGRAMLETTER_VIEW = "/program_letter/:id/view/" export const programLetterView = (id: string) => generatePath(PROGRAMLETTER_VIEW, { id: String(id) }) @@ -70,7 +66,16 @@ export const login = ({ return `${LOGIN}?next=${next}` } -export const DASHBOARD = "/dashboard/" +export const DASHBOARD_HOME = "/dashboard/" + +export const MY_LISTS = "/dashboard/my-lists/" +export const USERLIST_VIEW = "/dashboard/my-lists/:id" +export const userListView = (id: number) => + generatePath(USERLIST_VIEW, { id: String(id) }) + +export const PROFILE = "/dashboard/profile/" + +export const SETTINGS = "/dashboard/settings/" export const SEARCH = "/search/" diff --git a/frontends/mit-open/src/page-components/Header/UserMenu.tsx b/frontends/mit-open/src/page-components/Header/UserMenu.tsx index 2259371395..3c35c4395b 100644 --- a/frontends/mit-open/src/page-components/Header/UserMenu.tsx +++ b/frontends/mit-open/src/page-components/Header/UserMenu.tsx @@ -107,7 +107,7 @@ const UserMenu: React.FC = ({ variant }) => { label: "Dashboard", key: "dashboard", allow: !!user?.is_authenticated, - href: urls.DASHBOARD, + href: urls.DASHBOARD_HOME, }, { label: "Learning Paths", diff --git a/frontends/mit-open/src/page-components/ItemsListing/ItemsListingComponent.tsx b/frontends/mit-open/src/page-components/ItemsListing/ItemsListingComponent.tsx index b413343d01..13627b7bf0 100644 --- a/frontends/mit-open/src/page-components/ItemsListing/ItemsListingComponent.tsx +++ b/frontends/mit-open/src/page-components/ItemsListing/ItemsListingComponent.tsx @@ -1,20 +1,11 @@ import React from "react" -import { Grid, Button, Typography, styled } from "ol-components" -import { RiPencilFill, RiArrowUpDownLine } from "@remixicon/react" +import { Grid, Button, Typography, styled, Link } from "ol-components" +import { RiArrowLeftLine, RiArrowUpDownLine } from "@remixicon/react" import { useToggle, pluralize } from "ol-utilities" import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" import ItemsListing from "./ItemsListing" import type { LearningResourceListItem } from "./ItemsListing" - -const Container = styled(GridContainer)` - margin-top: 30px; - margin-bottom: 100px; -` - -const TitleContainer = styled(Grid)` - margin-top: 10px; - margin-bottom: 20px; -` +import { MY_LISTS } from "@/common/urls" type OnEdit = () => void type ListData = { @@ -35,6 +26,48 @@ type ItemsListingComponentProps = { condensed?: boolean } +const HeaderText = styled.div(({ theme }) => ({ + h3: { + ...theme.typography.h3, + [theme.breakpoints.down("sm")]: { + marginBottom: "24px", + ...theme.typography.h5, + }, + }, +})) + +const DescriptionText = styled(Typography)(({ theme }) => ({ + color: theme.custom.colors.silverGrayDark, + ...theme.typography.body1, + [theme.breakpoints.down("sm")]: { + display: "none", + }, +})) + +const HeaderGrid = styled(Grid)(({ theme }) => ({ + gap: "24px", + [theme.breakpoints.down("sm")]: { + width: "100%", + }, +})) + +const EditButton = styled(Button)(({ theme }) => ({ + [theme.breakpoints.down("sm")]: { + width: "100%", + }, +})) + +const ReorderGrid = styled(Grid)(({ theme }) => ({ + [theme.breakpoints.down("sm")]: { + display: "none", + }, +})) + +const CountText = styled(Typography)(({ theme }) => ({ + color: theme.custom.colors.silverGrayDark, + ...theme.typography.buttonSmall, +})) + const ItemsListingComponent: React.FC = ({ listType, list, @@ -51,54 +84,71 @@ const ItemsListingComponent: React.FC = ({ const count = list?.item_count return ( - + - - - {list?.title} - - {list?.description &&

{list.description}

} -
- {showSort && !!items.length && ( - - )} - {count !== undefined && count > 0 - ? `${count} ${pluralize("item", count)}` - : null} + + + + + - {canEdit ? ( - - ) : null} + + + {list?.title} + + {list?.description && ( + {list.description} + )} + + + {canEdit ? ( + + Edit List + + ) : null} + + + + {showSort && !!items.length && ( + + )} + + + {count !== undefined && count > 0 ? ( + {`${count} ${pluralize("item", count)}`} + ) : null} + +
= ({ condensed={condensed} />
-
+ ) } diff --git a/frontends/mit-open/src/page-components/UserListCardTemplate/UserListCardTemplate.test.tsx b/frontends/mit-open/src/page-components/UserListCard/UserListCardCondensed.test.tsx similarity index 58% rename from frontends/mit-open/src/page-components/UserListCardTemplate/UserListCardTemplate.test.tsx rename to frontends/mit-open/src/page-components/UserListCard/UserListCardCondensed.test.tsx index 8af919565d..06168e2b31 100644 --- a/frontends/mit-open/src/page-components/UserListCardTemplate/UserListCardTemplate.test.tsx +++ b/frontends/mit-open/src/page-components/UserListCard/UserListCardCondensed.test.tsx @@ -1,20 +1,19 @@ import React from "react" -import { render, screen } from "@testing-library/react" -import UserListCardTemplate from "./UserListCardTemplate" +import { screen } from "@testing-library/react" +import UserListCardCondensed from "./UserListCardCondensed" import * as factories from "api/test-utils/factories" -import { makeImgConfig } from "ol-utilities/test-utils/factories" +import { userListView } from "@/common/urls" +import { renderWithProviders } from "@/test-utils" const userListFactory = factories.userLists describe("UserListCard", () => { it("renders title and cover image", () => { const userList = userListFactory.userList() - const imgConfig = makeImgConfig() - render( - , ) const heading = screen.getByRole("heading", { name: userList.title }) diff --git a/frontends/mit-open/src/page-components/UserListCard/UserListCardCondensed.tsx b/frontends/mit-open/src/page-components/UserListCard/UserListCardCondensed.tsx new file mode 100644 index 0000000000..9fa5debd43 --- /dev/null +++ b/frontends/mit-open/src/page-components/UserListCard/UserListCardCondensed.tsx @@ -0,0 +1,75 @@ +import React from "react" +import { UserList } from "api" +import { pluralize } from "ol-utilities" +import { RiListCheck3 } from "@remixicon/react" +import { ListCardCondensed, styled, theme, Typography } from "ol-components" + +const StyledCard = styled(ListCardCondensed)({ + display: "flex", + alignItems: "center", + padding: "16px", + margin: "0", + gap: "16px", + width: "100%", +}) + +const TextContainer = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: "8px", + flex: "1 0 0", +}) + +const ItemsText = styled(Typography)(({ theme }) => ({ + color: theme.custom.colors.silverGrayDark, +})) + +const IconContainer = styled.div(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: "8px", + borderRadius: "4px", + color: theme.custom.colors.silverGrayDark, + background: theme.custom.colors.lightGray1, + [theme.breakpoints.down("sm")]: { + display: "none", + }, +})) + +type UserListCardCondensedProps = { + userList: U + href?: string + className?: string +} + +const UserListCardCondensed = ({ + userList, + href, + className, +}: UserListCardCondensedProps) => { + return ( + + + + + {userList.title} + + + {userList.item_count} {pluralize("item", userList.item_count)} + + + + + + + + ) +} + +export default UserListCardCondensed +export type { UserListCardCondensedProps } diff --git a/frontends/mit-open/src/page-components/UserListCardTemplate/UserListCardTemplate.tsx b/frontends/mit-open/src/page-components/UserListCardTemplate/UserListCardTemplate.tsx deleted file mode 100644 index 91da95becc..0000000000 --- a/frontends/mit-open/src/page-components/UserListCardTemplate/UserListCardTemplate.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useCallback } from "react" -import CardTemplate from "../CardTemplate/CardTemplate" -import { UserList } from "api" -import { EmbedlyConfig, pluralize } from "ol-utilities" -import { styled } from "ol-components" - -const TypeRow = styled.div` - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - min-height: 1.5em; /* ensure consistent height even if no certificate */ -` - -type CardVariant = "column" | "row" | "row-reverse" -type OnActivateCard = (userList: UserList) => void -type UserListCardTemplateProps = { - /** - * Whether the course picture and info display as a column or row. - */ - variant: CardVariant - userList: U - sortable?: boolean - className?: string - imgConfig: EmbedlyConfig - onActivate?: OnActivateCard - footerActionSlot?: React.ReactNode -} - -const UserListCardTemplate = ({ - variant, - userList, - className, - imgConfig, - sortable, - onActivate, - footerActionSlot, -}: UserListCardTemplateProps) => { - const handleActivate = useCallback( - () => onActivate?.(userList), - [userList, onActivate], - ) - const extraDetails = ( - - {userList.description} - - ) - return ( - - {userList.item_count} {pluralize("item", userList.item_count)} - - } - footerActionSlot={footerActionSlot} - > - ) -} - -export default UserListCardTemplate -export type { UserListCardTemplateProps } diff --git a/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx b/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx index acc6a70d58..777aee60fb 100644 --- a/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx +++ b/frontends/mit-open/src/pages/DashboardPage/Dashboard.test.tsx @@ -7,7 +7,11 @@ import { } from "../../test-utils" import { factories, urls } from "api/test-utils" import { Permissions } from "@/common/permissions" -import { DashboardPage, DashboardTabLabels } from "./DashboardPage" +import { + DashboardPage, + DashboardTabKeys, + DashboardTabLabels, +} from "./DashboardPage" import { faker } from "@faker-js/faker/locale/en" import { CourseResource, @@ -16,6 +20,7 @@ import { } from "api" import { ControlledPromise } from "ol-test-utilities" import React from "react" +import { DASHBOARD_HOME, MY_LISTS, PROFILE } from "@/common/urls" describe("DashboardPage", () => { const makeSearchResponse = ( @@ -244,13 +249,15 @@ describe("DashboardPage", () => { test("Renders the expected tab links", async () => { setupAPIs() renderWithProviders() - Object.keys(DashboardTabLabels).forEach((key) => { + const urls = [DASHBOARD_HOME, MY_LISTS, PROFILE] + urls.forEach((url: string) => { + const key = DashboardTabKeys[url as keyof typeof DashboardTabKeys] const desktopTab = screen.getByTestId(`desktop-tab-${key}`) const mobileTab = screen.getByTestId(`mobile-tab-${key}`) expect(desktopTab).toBeInTheDocument() expect(mobileTab).toBeInTheDocument() - expect(desktopTab).toHaveAttribute("href", `/#${key}`) - expect(mobileTab).toHaveAttribute("href", `/#${key}`) + expect(desktopTab).toHaveAttribute("href", url) + expect(mobileTab).toHaveAttribute("href", url) }) }) diff --git a/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx b/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx index 002da268bd..930173ab59 100644 --- a/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx +++ b/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react" +import React from "react" import { RiAccountCircleFill, RiDashboardLine, @@ -23,9 +23,8 @@ import { import { MetaTags } from "ol-utilities" import { Link } from "react-router-dom" import { useUserMe } from "api/hooks/user" -import { useLocation } from "react-router" -import { UserListListingComponent } from "../UserListListingPage/UserListListingPage" -import { UserList } from "api" +import { useLocation, useParams } from "react-router" +import UserListListingComponent from "../UserListListingComponent/UserListListingComponent" import { ProfileEditForm } from "./ProfileEditForm" import { useProfileMeQuery } from "api/hooks/profile" @@ -40,6 +39,7 @@ import { import ResourceCarousel from "@/page-components/ResourceCarousel/ResourceCarousel" import UserListDetailsTab from "./UserListDetailsTab" import { SettingsPage } from "./SettingsPage" +import { DASHBOARD_HOME, MY_LISTS, PROFILE, SETTINGS } from "@/common/urls" /** * @@ -253,22 +253,37 @@ const StyledResourceCarousel = styled(ResourceCarousel)(({ theme }) => ({ }, })) +const TabKeys = { + [DASHBOARD_HOME]: "home", + [MY_LISTS]: "my-lists", + [PROFILE]: "profile", + [SETTINGS]: "settings", +} + +const TabLabels = { + [DASHBOARD_HOME]: "Home", + [MY_LISTS]: "My Lists", + [PROFILE]: "Profile", + [SETTINGS]: "Settings", +} + interface UserMenuTabProps { icon: React.ReactNode text: string + tabKey: string value: string currentValue: string onClick?: () => void } const UserMenuTab: React.FC = (props) => { - const { icon, text, value, currentValue, onClick } = props + const { icon, text, tabKey, value, currentValue, onClick } = props const selected = value === currentValue return ( = (props) => { ) } -enum TabValues { - HOME = "home", - MY_LISTS = "my-lists", - SETTINGS = "settings", - PROFILE = "profile", -} - -const TabLabels = { - [TabValues.HOME]: "Home", - [TabValues.MY_LISTS]: "My Lists", - [TabValues.PROFILE]: "Profile", - [TabValues.SETTINGS]: "Settings", -} -const keyFromHash = (hash: string) => { - const keys = Object.values(TabValues) - const match = keys.find((key) => `#${key}` === hash) - return match ?? "home" +type RouteParams = { + id: string } const DashboardPage: React.FC = () => { const { isLoading: isLoadingUser, data: user } = useUserMe() const { isLoading: isLoadingProfile, data: profile } = useProfileMeQuery() - const { hash } = useLocation() - const tabValue = keyFromHash(hash) - const [userListAction, setUserListAction] = useState("list") - const [userListId, setUserListId] = useState(0) + const { pathname } = useLocation() + const id = Number(useParams().id) || -1 + const showUserListDetail = pathname.includes(MY_LISTS) && id !== -1 + const tabValue = showUserListDetail + ? MY_LISTS + : [DASHBOARD_HOME, MY_LISTS, PROFILE, SETTINGS].includes(pathname) + ? pathname + : DASHBOARD_HOME const topics = profile?.preference_search_filters.topic const certification = profile?.preference_search_filters.certification - const handleActivateUserList = useCallback((userList: UserList) => { - setUserListId(userList.id) - setUserListAction("detail") - }, []) - const desktopMenu = ( @@ -339,27 +338,30 @@ const DashboardPage: React.FC = () => { > } - text={TabLabels[TabValues.HOME]} - value={TabValues.HOME} + text={TabLabels[DASHBOARD_HOME]} + tabKey={TabKeys[DASHBOARD_HOME]} + value={DASHBOARD_HOME} currentValue={tabValue} /> } - text={TabLabels[TabValues.MY_LISTS]} - value={TabValues.MY_LISTS} + text={TabLabels[MY_LISTS]} + tabKey={TabKeys[MY_LISTS]} + value={MY_LISTS} currentValue={tabValue} - onClick={() => setUserListAction("list")} /> } - text={TabLabels[TabValues.PROFILE]} - value={TabValues.PROFILE} + text={TabLabels[PROFILE]} + tabKey={TabKeys[PROFILE]} + value={PROFILE} currentValue={tabValue} /> } - text={TabLabels[TabValues.SETTINGS]} - value={TabValues.SETTINGS} + text={TabLabels[SETTINGS]} + tabKey={TabKeys[SETTINGS]} + value={SETTINGS} currentValue={tabValue} /> @@ -370,28 +372,27 @@ const DashboardPage: React.FC = () => { const mobileMenu = ( setUserListAction("list")} /> @@ -408,108 +409,94 @@ const DashboardPage: React.FC = () => { {mobileMenu} {desktopMenu} - - - - - - Your MIT Learning Journey - - - A customized course list based on your preferences. - - - - - Edit Profile - - - - - {topics?.map((topic, index) => ( + {showUserListDetail ? ( + + + + ) : ( + + + + + + Your MIT Learning Journey + + + A customized course list based on your preferences. + + + + + Edit Profile + + + - ))} - {certification === true ? ( + {topics?.map((topic, index) => ( + + ))} + {certification === true ? ( + + ) : ( + + )} - ) : ( - )} - - - - - {userListAction === "list" ? ( -
- -
- ) : ( -
- -
- )} -
- - Profile - {isLoadingProfile || typeof profile === "undefined" ? ( - - ) : ( -
- -
- )} -
- - Settings - {isLoadingProfile || !profile ? ( - - ) : ( -
- -
- )} -
-
+
+ + + + + Profile + {isLoadingProfile || !profile ? ( + + ) : ( +
+ +
+ )} +
+ + Settings + {isLoadingProfile || !profile ? ( + + ) : ( +
+ +
+ )} +
+
+ )} @@ -518,4 +505,8 @@ const DashboardPage: React.FC = () => { ) } -export { DashboardPage, TabLabels as DashboardTabLabels } +export { + DashboardPage, + TabKeys as DashboardTabKeys, + TabLabels as DashboardTabLabels, +} diff --git a/frontends/mit-open/src/pages/HomePage/HomePage.test.tsx b/frontends/mit-open/src/pages/HomePage/HomePage.test.tsx index d36f46d073..2c4e7e4651 100644 --- a/frontends/mit-open/src/pages/HomePage/HomePage.test.tsx +++ b/frontends/mit-open/src/pages/HomePage/HomePage.test.tsx @@ -293,7 +293,7 @@ describe("Home Page personalize section", () => { expect(link).toHaveAttribute( "href", routes.login({ - pathname: routes.DASHBOARD, + pathname: routes.DASHBOARD_HOME, }), ) }) diff --git a/frontends/mit-open/src/pages/HomePage/PersonalizeSection.tsx b/frontends/mit-open/src/pages/HomePage/PersonalizeSection.tsx index 34f380e2cc..515edd1f41 100644 --- a/frontends/mit-open/src/pages/HomePage/PersonalizeSection.tsx +++ b/frontends/mit-open/src/pages/HomePage/PersonalizeSection.tsx @@ -65,7 +65,7 @@ const AUTH_TEXT_DATA = { text: "Ready to keep learning? Choose from your saved courses, find personalized recommendations, and see what's trending on your dashboard.", linkProps: { children: "My Dashboard", - href: urls.DASHBOARD, + href: urls.DASHBOARD_HOME, }, }, anonymous: { @@ -75,7 +75,7 @@ const AUTH_TEXT_DATA = { children: "Sign Up for Free", reloadDocument: true, href: urls.login({ - pathname: urls.DASHBOARD, + pathname: urls.DASHBOARD_HOME, }), }, }, diff --git a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts index b8ac2ccee5..663a188f97 100644 --- a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts +++ b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.test.ts @@ -100,7 +100,7 @@ describe("ListDetailsPage", () => { setup({ path, userSettings }) await screen.findByRole("heading", { name: path.title }) - const editButton = screen.queryByRole("button", { name: "Edit" }) + const editButton = screen.queryByRole("button", { name: "Edit List" }) expect(!!editButton).toBe(canEdit) }, ) @@ -168,7 +168,7 @@ describe("ListDetailsPage", () => { test("Edit buttons opens editing dialog", async () => { const path = factories.learningResources.learningPath() setup({ path, userSettings: { is_learning_path_editor: true } }) - const editButton = await screen.findByRole("button", { name: "Edit" }) + const editButton = await screen.findByRole("button", { name: "Edit List" }) const editList = jest.spyOn(manageListDialogs, "upsertLearningPath") editList.mockImplementationOnce(jest.fn()) diff --git a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx index f0efbbcbeb..1764734acc 100644 --- a/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx +++ b/frontends/mit-open/src/pages/ListDetailsPage/ListDetailsPage.tsx @@ -1,9 +1,13 @@ import React from "react" -import { Container, BannerPage } from "ol-components" +import { Container, BannerPage, styled } from "ol-components" import { MetaTags } from "ol-utilities" import ItemsListingComponent from "@/page-components/ItemsListing/ItemsListingComponent" import type { ItemsListingComponentProps } from "@/page-components/ItemsListing/ItemsListingComponent" +const StyledContainer = styled(Container)({ + paddingTop: "24px", +}) + const ListDetailsPage: React.FC = ({ listType, list, @@ -20,7 +24,7 @@ const ListDetailsPage: React.FC = ({ className="learningpaths-page" > - + = ({ isFetching={isFetching} handleEdit={handleEdit} /> - + ) } diff --git a/frontends/mit-open/src/pages/OnboardingPage/OnboardingPage.tsx b/frontends/mit-open/src/pages/OnboardingPage/OnboardingPage.tsx index f0ccc2a73b..9dcdbf4e7b 100644 --- a/frontends/mit-open/src/pages/OnboardingPage/OnboardingPage.tsx +++ b/frontends/mit-open/src/pages/OnboardingPage/OnboardingPage.tsx @@ -20,7 +20,7 @@ import { import { MetaTags } from "ol-utilities" import { RiArrowRightLine, RiArrowLeftLine } from "@remixicon/react" import { useProfileMeMutation, useProfileMeQuery } from "api/hooks/profile" -import { DASHBOARD } from "@/common/urls" +import { DASHBOARD_HOME } from "@/common/urls" import { useFormik } from "formik" import { useLearningResourceTopics } from "api/hooks/learningResources" @@ -168,7 +168,7 @@ const OnboardingPage: React.FC = () => { if (activeStep < NUM_STEPS - 1) { setActiveStep((prevActiveStep) => prevActiveStep + 1) } else { - navigate(DASHBOARD) + navigate(DASHBOARD_HOME) } }, validateOnChange: false, diff --git a/frontends/mit-open/src/pages/UserListListingComponent/UserListListingComponent.test.tsx b/frontends/mit-open/src/pages/UserListListingComponent/UserListListingComponent.test.tsx new file mode 100644 index 0000000000..c35e645070 --- /dev/null +++ b/frontends/mit-open/src/pages/UserListListingComponent/UserListListingComponent.test.tsx @@ -0,0 +1,107 @@ +import React from "react" +import { faker } from "@faker-js/faker/locale/en" +import { factories, urls } from "api/test-utils" +import { + screen, + renderWithProviders, + setMockResponse, + user, + expectProps, + within, +} from "../../test-utils" +import type { User } from "../../test-utils" + +import UserListListingComponent from "./UserListListingComponent" +import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs" +import UserListCardCondensed from "@/page-components/UserListCard/UserListCardCondensed" +import { userListView } from "@/common/urls" + +jest.mock("../../page-components/UserListCard/UserListCardCondensed", () => { + const actual = jest.requireActual( + "../../page-components/UserListCard/UserListCardCondensed", + ) + return { + __esModule: true, + ...actual, + default: jest.fn(actual.default), + } +}) +const spyULCardCondensed = jest.mocked(UserListCardCondensed) + +/** + * Set up the mock API responses for lists pages. + */ +const setup = ({ + listsCount = faker.number.int({ min: 2, max: 5 }), + user = { is_learning_path_editor: false }, +}: { + user?: Partial + listsCount?: number +} = {}) => { + const paths = factories.userLists.userLists({ count: listsCount }) + setMockResponse.get(urls.userLists.list(), paths) + + setMockResponse.get(urls.userSubscription.check(), factories.percolateQueries) + + const { location } = renderWithProviders( + , + { + user, + }, + ) + + return { paths, location } +} + +describe("UserListListingPage", () => { + it("Has heading 'User Lists' and correct page title", async () => { + setup() + screen.getByRole("heading", { name: "My Lists" }) + }) + + it("Renders a card for each user list", async () => { + const { paths } = setup() + const titles = paths.results.map((userList) => userList.title) + const headings = await screen.findAllByRole("heading", { + name: (value) => titles.includes(value), + }) + + // for sanity + expect(headings.length).toBeGreaterThan(0) + expect(titles.length).toBe(headings.length) + + paths.results.forEach((userList) => { + expectProps(spyULCardCondensed, { + href: userListView(userList.id), + userList: userList, + }) + }) + }) + + test("Clicking new list opens the creation dialog", async () => { + const createList = jest + .spyOn(manageListDialogs, "upsertUserList") + .mockImplementationOnce(jest.fn()) + setup() + const newListButton = await screen.findByRole("button", { + name: "Create new list", + }) + + expect(createList).not.toHaveBeenCalled() + await user.click(newListButton) + + // Check details of this dialog elsewhere + expect(createList).toHaveBeenCalledWith() + }) + + test("Clicking on the card shows the list detail view", async () => { + const { paths, location } = setup() + const card = await screen.findByTestId( + `user-list-card-condensed-${paths.results[0].id}`, + ) + const cardLink = within(card).getByRole("link") + + await user.click(cardLink) + expect(location.current.pathname).toBe(userListView(paths.results[0].id)) + }) +}) diff --git a/frontends/mit-open/src/pages/UserListListingComponent/UserListListingComponent.tsx b/frontends/mit-open/src/pages/UserListListingComponent/UserListListingComponent.tsx new file mode 100644 index 0000000000..aaa3daaef2 --- /dev/null +++ b/frontends/mit-open/src/pages/UserListListingComponent/UserListListingComponent.tsx @@ -0,0 +1,75 @@ +import React, { useCallback } from "react" +import { + Button, + LoadingSpinner, + styled, + Typography, + PlainList, +} from "ol-components" + +import { useUserListList } from "api/hooks/learningResources" + +import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" + +import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs" +import { userListView } from "@/common/urls" +import UserListCardCondensed from "@/page-components/UserListCard/UserListCardCondensed" + +const Header = styled(Typography)({ + marginBottom: "16px", +}) + +const NewListButton = styled(Button)(({ theme }) => ({ + marginTop: "24px", + width: "200px", + [theme.breakpoints.down("sm")]: { + width: "100%", + }, +})) + +type UserListListingComponentProps = { + title?: string +} + +const UserListListingComponent: React.FC = ( + props, +) => { + const { title } = props + const listingQuery = useUserListList() + const handleCreate = useCallback(() => { + manageListDialogs.upsertUserList() + }, []) + + return ( + + +
{title}
+
+ + {listingQuery.data && ( + + {listingQuery.data.results?.map((list) => { + return ( +
  • + +
  • + ) + })} +
    + )} + + Create new list + +
    +
    +
    + ) +} + +export default UserListListingComponent diff --git a/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.test.tsx b/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.test.tsx deleted file mode 100644 index 753b9e9c39..0000000000 --- a/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import React from "react" -import { faker } from "@faker-js/faker/locale/en" -import { factories, urls } from "api/test-utils" -import { - screen, - renderWithProviders, - setMockResponse, - user, - expectProps, - waitFor, -} from "../../test-utils" -import type { User } from "../../test-utils" - -import { UserListListingPage } from "./UserListListingPage" -import UserListCardTemplate from "@/page-components/UserListCardTemplate/UserListCardTemplate" -import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs" - -jest.mock( - "../../page-components/UserListCardTemplate/UserListCardTemplate", - () => { - const actual = jest.requireActual( - "../../page-components/UserListCardTemplate/UserListCardTemplate", - ) - return { - __esModule: true, - ...actual, - default: jest.fn(actual.default), - } - }, -) -const spyULCardTemplate = jest.mocked(UserListCardTemplate) - -/** - * Set up the mock API responses for lists pages. - */ -const setup = ({ - listsCount = faker.number.int({ min: 2, max: 5 }), - user = { is_learning_path_editor: false }, -}: { - user?: Partial - listsCount?: number -} = {}) => { - const paths = factories.userLists.userLists({ count: listsCount }) - - setMockResponse.get(urls.userLists.list(), paths) - - setMockResponse.get(urls.userSubscription.check(), factories.percolateQueries) - - const { location } = renderWithProviders(, { - user, - }) - - return { paths, location } -} - -describe("UserListListingPage", () => { - it("Has heading 'User Lists' and correct page title", async () => { - setup() - screen.getByRole("heading", { name: "User Lists" }) - await waitFor(() => expect(document.title).toBe("My Lists | MIT Open")) - }) - - it("Renders a card for each user list", async () => { - const { paths } = setup() - const titles = paths.results.map((userList) => userList.title) - const headings = await screen.findAllByRole("heading", { - name: (value) => titles.includes(value), - }) - - // for sanity - expect(headings.length).toBeGreaterThan(0) - expect(titles.length).toBe(headings.length) - - paths.results.forEach((userList) => { - expectProps(spyULCardTemplate, { userList: userList }) - }) - }) - - test("Clicking edit -> Edit on opens the editing dialog", async () => { - const editList = jest - .spyOn(manageListDialogs, "upsertUserList") - .mockImplementationOnce(jest.fn()) - - const { paths } = setup() - const path = faker.helpers.arrayElement(paths.results) - - const menuButton = await screen.findByRole("button", { - name: `Edit list ${path.title}`, - }) - await user.click(menuButton) - const editButton = screen.getByRole("menuitem", { name: "Edit" }) - await user.click(editButton) - - expect(editList).toHaveBeenCalledWith(path) - }) - - test("Clicking edit -> Delete opens the deletion dialog", async () => { - const deleteList = jest - .spyOn(manageListDialogs, "destroyUserList") - .mockImplementationOnce(jest.fn()) - - const { paths } = setup() - const path = faker.helpers.arrayElement(paths.results) - - const menuButton = await screen.findByRole("button", { - name: `Edit list ${path.title}`, - }) - await user.click(menuButton) - const deleteButton = screen.getByRole("menuitem", { name: "Delete" }) - - await user.click(deleteButton) - - // Check details of this dialog elsewhere - expect(deleteList).toHaveBeenCalledWith(path) - }) - - test("Clicking new list opens the creation dialog", async () => { - const createList = jest - .spyOn(manageListDialogs, "upsertUserList") - .mockImplementationOnce(jest.fn()) - setup() - const newListButton = await screen.findByRole("button", { - name: "Create new list", - }) - - expect(createList).not.toHaveBeenCalled() - await user.click(newListButton) - - // Check details of this dialog elsewhere - expect(createList).toHaveBeenCalledWith() - }) - - test("Clicking on list title navigates to list page", async () => { - const { location, paths } = setup() - const path = faker.helpers.arrayElement(paths.results) - const listTitle = await screen.findByRole("heading", { name: path.title }) - await user.click(listTitle) - expect(location.current).toEqual( - expect.objectContaining({ - pathname: `/userlists/${path.id}`, - search: "", - hash: "", - }), - ) - }) -}) diff --git a/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx b/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx deleted file mode 100644 index 1b5e85ddc9..0000000000 --- a/frontends/mit-open/src/pages/UserListListingPage/UserListListingPage.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useCallback, useMemo } from "react" -import { - Button, - Grid, - LoadingSpinner, - BannerPage, - Container, - styled, - SimpleMenuItem, - SimpleMenu, - ActionButton, - Typography, - PlainList, - imgConfigs, -} from "ol-components" -import { RiPencilFill, RiMore2Fill, RiDeleteBin7Fill } from "@remixicon/react" - -import { MetaTags } from "ol-utilities" -import type { UserList } from "api" -import { useUserListList } from "api/hooks/learningResources" - -import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" - -import UserListCardTemplate from "@/page-components/UserListCardTemplate/UserListCardTemplate" -import { useNavigate } from "react-router" -import * as urls from "@/common/urls" -import { manageListDialogs } from "@/page-components/ManageListDialogs/ManageListDialogs" - -const PageContainer = styled(Container)({ - marginTop: "1rem", -}) - -const ListHeaderGrid = styled(Grid)({ - marginBottom: "1rem", -}) - -type EditUserListMenuProps = { - userList: UserList -} - -const EditUserListMenu: React.FC = ({ userList }) => { - const items: SimpleMenuItem[] = useMemo( - () => [ - { - key: "edit", - label: "Edit", - icon: , - onClick: () => manageListDialogs.upsertUserList(userList), - }, - { - key: "delete", - label: "Delete", - icon: , - onClick: () => manageListDialogs.destroyUserList(userList), - }, - ], - [userList], - ) - return ( - - - - } - items={items} - /> - ) -} - -type UserListListingComponentProps = { - title?: string - onActivate: (userList: UserList) => void -} - -const UserListListingComponent: React.FC = ( - props, -) => { - const { title, onActivate } = props - const listingQuery = useUserListList() - const handleCreate = useCallback(() => { - manageListDialogs.upsertUserList() - }, []) - - return ( - - - - - - {title} - - - - - - -
    - - {listingQuery.data && ( - - {listingQuery.data.results?.map((list) => { - return ( -
  • - } - /> -
  • - ) - })} -
    - )} -
    -
    -
    - ) -} - -const UserListListingPage: React.FC = () => { - const navigate = useNavigate() - const handleActivate = useCallback( - (userList: UserList) => { - const path = urls.userListView(userList.id) - navigate(path) - }, - [navigate], - ) - return ( - - - - - - - ) -} - -export { UserListListingComponent, UserListListingPage } diff --git a/frontends/mit-open/src/routes.tsx b/frontends/mit-open/src/routes.tsx index 626daad097..dd92134a55 100644 --- a/frontends/mit-open/src/routes.tsx +++ b/frontends/mit-open/src/routes.tsx @@ -7,7 +7,6 @@ import LearningPathListingPage from "@/pages/LearningPathListingPage/LearningPat import ChannelPage from "@/pages/ChannelPage/ChannelPage" import EditChannelPage from "@/pages/ChannelPage/EditChannelPage" -import { UserListListingPage } from "./pages/UserListListingPage/UserListListingPage" import ArticleDetailsPage from "@/pages/ArticleDetailsPage/ArticleDetailsPage" import { ArticleCreatePage, ArticleEditPage } from "@/pages/ArticleUpsertPages" import ProgramLetterPage from "@/pages/ProgramLetterPage/ProgramLetterPage" @@ -21,7 +20,6 @@ import Header from "@/page-components/Header/Header" import Footer from "@/page-components/Footer/Footer" import { Permissions } from "@/common/permissions" import SearchPage from "./pages/SearchPage/SearchPage" -import UserListDetailsPage from "./pages/ListDetailsPage/UserListDetailsPage" import LearningPathDetailsPage from "./pages/ListDetailsPage/LearningPathDetailsPage" import LearningResourceDrawer from "./page-components/LearningResourceDrawer/LearningResourceDrawer" import DepartmentListingPage from "./pages/DepartmentListingPage/DepartmentListingPage" @@ -84,10 +82,18 @@ const routes: RouteObject[] = [ element: , }, { - path: urls.USERLIST_LISTING, + path: urls.DASHBOARD_HOME, element: ( - + + + ), + }, + { + path: urls.MY_LISTS, + element: ( + + ), }, @@ -95,12 +101,20 @@ const routes: RouteObject[] = [ path: urls.USERLIST_VIEW, element: ( - + + + ), + }, + { + path: urls.PROFILE, + element: ( + + ), }, { - path: urls.DASHBOARD, + path: urls.SETTINGS, element: ( diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index da0757a32f..0ebac874ff 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -164,6 +164,7 @@ export * from "./components/Alert/Alert" export * from "./components/BannerPage/BannerPage" export * from "./components/Breadcrumbs/Breadcrumbs" export * from "./components/Card/Card" +export * from "./components/Card/ListCardCondensed" export * from "./components/Carousel/Carousel" export * from "./components/Checkbox/Checkbox" export * from "./components/Checkbox/CheckboxChoiceField"