Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
Release Notes
=============

Version 0.36.3
--------------

- Ingest canvas courses (#2307)
- Update dependency urllib3 to v2.5.0 [SECURITY] (#2311)
- unpin dep (#2309)
- dashboard unenroll dialog functionality (#2303)
- update smoot to proper release (#2306)
- Use smoot components; fix radio button focus ring (#2304)
- update spinner usage (#2301)
- Feature Flag for MITxOnline API Call (#2305)
- Upgrade Smoot Design (#2302)
- Update dependency requests to v2.32.4 [SECURITY] (#2299)

Version 0.36.1 (Released June 10, 2025)
--------------

1 change: 1 addition & 0 deletions env/frontend.env
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ SENTRY_ENV=dev # Re-enable sentry
NEXT_PUBLIC_ORIGIN=${MITOL_APP_BASE_URL}
NEXT_PUBLIC_MITOL_API_BASE_URL=${MITOL_API_BASE_URL}
NEXT_PUBLIC_CSRF_COOKIE_NAME=${CSRF_COOKIE_NAME}
NEXT_PUBLIC_MITX_ONLINE_CSRF_COOKIE_NAME=csrftoken
NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=${MITOL_SUPPORT_EMAIL}

NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY}
2 changes: 1 addition & 1 deletion frontends/api/src/mitxonline/clients.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import axios from "axios"

const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL,
xsrfCookieName: process.env.NEXT_PUBLIC_CSRF_COOKIE_NAME,
xsrfCookieName: process.env.NEXT_PUBLIC_MITX_ONLINE_CSRF_COOKIE_NAME,
xsrfHeaderName: "X-CSRFToken",
withXSRFToken: true,
withCredentials:
18 changes: 16 additions & 2 deletions frontends/api/src/mitxonline/hooks/enrollment/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { enrollmentQueries } from "./queries"
import { enrollmentQueries, enrollmentKeys } from "./queries"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { enrollmentsApi } from "../../clients"

export { enrollmentQueries }
const useDestroyEnrollment = (enrollmentId: number) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => enrollmentsApi.enrollmentsDestroy({ id: enrollmentId }),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: enrollmentKeys.enrollmentsList(),
})
},
})
}

export { enrollmentQueries, enrollmentKeys, useDestroyEnrollment }
14 changes: 4 additions & 10 deletions frontends/api/src/mitxonline/hooks/enrollment/queries.ts
Original file line number Diff line number Diff line change
@@ -2,24 +2,18 @@ import { queryOptions } from "@tanstack/react-query"
import type { CourseRunEnrollment } from "@mitodl/mitxonline-api-axios/v1"

import { enrollmentsApi } from "../../clients"
import { RawAxiosRequestConfig } from "axios"

const enrollmentKeys = {
root: ["mitxonline", "enrollments"],
enrollmentsList: (opts: RawAxiosRequestConfig) => [
...enrollmentKeys.root,
"programEnrollments",
"list",
opts,
],
enrollmentsList: () => [...enrollmentKeys.root, "programEnrollments", "list"],
}

const enrollmentQueries = {
enrollmentsList: (opts: RawAxiosRequestConfig) =>
enrollmentsList: () =>
queryOptions({
queryKey: enrollmentKeys.enrollmentsList(opts),
queryKey: enrollmentKeys.enrollmentsList(),
queryFn: async (): Promise<CourseRunEnrollment[]> => {
return enrollmentsApi.enrollmentsList(opts).then((res) => res.data)
return enrollmentsApi.enrollmentsList().then((res) => res.data)
},
}),
}
3 changes: 2 additions & 1 deletion frontends/api/src/mitxonline/hooks/user/index.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"
import { usersApi } from "../../clients"
import type { User } from "@mitodl/mitxonline-api-axios/v1"

const useMitxOnlineCurrentUser = () =>
const useMitxOnlineCurrentUser = (opts: { enabled?: boolean } = {}) =>
useQuery({
queryKey: ["mitxonline", "currentUser"],
queryFn: async (): Promise<User> => {
@@ -11,6 +11,7 @@ const useMitxOnlineCurrentUser = () =>
...response.data,
}
},
...opts,
})

export { useMitxOnlineCurrentUser }
4 changes: 2 additions & 2 deletions frontends/api/src/mitxonline/test-utils/urls.ts
Original file line number Diff line number Diff line change
@@ -13,8 +13,8 @@ const currentUser = {
}

const enrollment = {
courseEnrollment: (opts?: RawAxiosRequestConfig) =>
`${API_BASE_URL}/api/v1/enrollments/${queryify(opts)}`,
courseEnrollment: (id?: number) =>
`${API_BASE_URL}/api/v1/enrollments/${id ? `${id}/` : ""}`,
}

const programs = {
2 changes: 1 addition & 1 deletion frontends/main/package.json
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
"@emotion/styled": "^11.11.0",
"@mitodl/course-search-utils": "3.3.2",
"@mitodl/mitxonline-api-axios": "^2025.6.3",
"@mitodl/smoot-design": "^6.6.1",
"@mitodl/smoot-design": "^6.10.0",
"@next/bundle-analyzer": "^14.2.15",
"@remixicon/react": "^4.2.0",
"@sentry/nextjs": "^9.0.0",
3 changes: 2 additions & 1 deletion frontends/main/src/app-pages/ChannelPage/ChannelSearch.tsx
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@ import { useResourceSearchParams } from "@mitodl/course-search-utils"
import type { Facets, BooleanFacets } from "@mitodl/course-search-utils"
import { useSearchParams } from "@mitodl/course-search-utils/next"
import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay"
import { Container, styled, VisuallyHidden } from "ol-components"
import { Container, styled } from "ol-components"
import { VisuallyHidden } from "@mitodl/smoot-design"
import { SearchField } from "@/page-components/SearchField/SearchField"
import { getFacets } from "./searchRequests"
import { keyBy } from "lodash"
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@ import {
Stack,
BannerBackground,
Typography,
VisuallyHidden,
UnitLogo,
} from "ol-components"
import { VisuallyHidden } from "@mitodl/smoot-design"
import { OfferedByEnum, SourceTypeEnum } from "api"
import { SearchSubscriptionToggle } from "@/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle"
import { ChannelDetails } from "@/page-components/ChannelDetails/ChannelDetails"
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client"
import React, { useState } from "react"
import { styled, MenuItem, Alert } from "ol-components"
import { styled, MenuItem } from "ol-components"
import { FeatureFlags } from "@/common/feature_flags"
import { useFeatureFlagEnabled } from "posthog-js/react"
import StyledContainer from "@/page-components/StyledContainer/StyledContainer"
// eslint-disable-next-line
import { InputLabel, Select } from "@mui/material"
import { Alert } from "@mitodl/smoot-design"
import { AiChat, AiChatProps } from "@mitodl/smoot-design/ai"
import { extractJSONFromComment } from "ol-utilities"
import { getCsrfToken } from "@/common/utils"
Original file line number Diff line number Diff line change
@@ -350,6 +350,7 @@ describe.each([
async ({ contextMenuItems }) => {
const course = dashboardCourse()
course.enrollment = {
id: faker.number.int(),
status: EnrollmentStatus.Completed,
mode: EnrollmentMode.Verified,
}
@@ -366,7 +367,7 @@ describe.each([
await user.click(contextMenuButton)
const expectedMenuItems = [
...contextMenuItems,
...getDefaultContextMenuItems("Test Course"),
...getDefaultContextMenuItems("Test Course", course.enrollment.id),
]
const menuItems = screen.getAllByRole("menuitem")
for (let i = 0; i < expectedMenuItems.length; i++) {
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ const MenuButton = styled(ActionButton)<{
},
])

const getDefaultContextMenuItems = (title: string) => {
const getDefaultContextMenuItems = (title: string, enrollmentId: number) => {
return [
{
className: "dashboard-card-menu-item",
@@ -93,7 +93,7 @@ const getDefaultContextMenuItems = (title: string) => {
key: "unenroll",
label: "Unenroll",
onClick: () => {
NiceModal.show(UnenrollDialog, { title })
NiceModal.show(UnenrollDialog, { title, enrollmentId })
},
},
]
@@ -332,10 +332,12 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
) : run.startDate ? (
<CourseStartCountdown startDate={run.startDate} />
) : null
const menuItems = contextMenuItems.concat(getDefaultContextMenuItems(title))
const menuItems = contextMenuItems.concat(
enrollment?.id ? getDefaultContextMenuItems(title, enrollment?.id) : [],
)
const contextMenu = isLoading ? (
<Skeleton variant="rectangular" width={12} height={24} />
) : (
) : menuItems.length > 0 ? (
<SimpleMenu
items={menuItems}
trigger={
@@ -349,7 +351,7 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
</MenuButton>
}
/>
)
) : null
const desktopLayout = (
<CardRoot
screenSize="desktop"
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from "react"
import {
renderWithProviders,
screen,
setMockResponse,
user,
within,
} from "@/test-utils"
import { EnrollmentDisplay } from "./EnrollmentDisplay"
import * as mitxonline from "api/mitxonline-test-utils"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { setupEnrollments } from "./test-utils"
import { faker } from "@faker-js/faker/locale/en"
import { mockAxiosInstance } from "api/test-utils"
import invariant from "tiny-invariant"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest
.mocked(useFeatureFlagEnabled)
.mockImplementation(() => false)

describe("DashboardDialogs", () => {
const setupApis = (includeExpired: boolean = true) => {
const { enrollments, completed, expired, started, notStarted } =
setupEnrollments(includeExpired)

mockedUseFeatureFlagEnabled.mockReturnValue(true)
setMockResponse.get(
mitxonline.urls.enrollment.courseEnrollment(),
enrollments,
)

return { enrollments, completed, expired, started, notStarted }
}

test("Opening the unenroll dialog and confirming the unenroll fires the proper API call", async () => {
const { enrollments } = setupApis()
const enrollment = faker.helpers.arrayElement(enrollments)

setMockResponse.delete(
mitxonline.urls.enrollment.courseEnrollment(enrollment.id),
null,
)
renderWithProviders(<EnrollmentDisplay />)

await screen.findByRole("heading", { name: "My Learning" })

const cards = await screen.findAllByTestId("enrollment-card-desktop")
expect(cards.length).toBe(enrollments.length)

const card = cards.find(
(c) => !!within(c).queryByText(enrollment.run.title),
)
invariant(card)

const contextMenuButton = await within(card).findByLabelText("More options")
await user.click(contextMenuButton)

const unenrollButton = await screen.findByRole("menuitem", {
name: "Unenroll",
})
await user.click(unenrollButton)

const confirmButton = await screen.findByRole("button", {
name: "Unenroll",
})
expect(confirmButton).toBeEnabled()

await user.click(confirmButton)

expect(mockAxiosInstance.request).toHaveBeenCalledWith(
expect.objectContaining({
method: "DELETE",
url: mitxonline.urls.enrollment.courseEnrollment(enrollment.id),
}),
)
})
})
Original file line number Diff line number Diff line change
@@ -5,20 +5,25 @@ import {
FormDialog,
DialogActions,
Stack,
Alert,
Checkbox,
LoadingSpinner,
} from "ol-components"
import { Button } from "@mitodl/smoot-design"
import { Button, Checkbox, Alert } from "@mitodl/smoot-design"

import NiceModal, { muiDialogV5 } from "@ebay/nice-modal-react"
import { useFormik } from "formik"
import { useDestroyEnrollment } from "api/mitxonline-hooks/enrollment"

const BoldText = styled.span(({ theme }) => ({
...theme.typography.subtitle1,
}))

const SpinnerContainer = styled.div({
marginLeft: "8px",
})

type DashboardDialogProps = {
title: string
enrollmentId: number
}
const EmailSettingsDialogInner: React.FC<DashboardDialogProps> = ({
title,
@@ -79,15 +84,22 @@ const EmailSettingsDialogInner: React.FC<DashboardDialogProps> = ({
)
}

const UnenrollDialogInner: React.FC<DashboardDialogProps> = ({ title }) => {
const UnenrollDialogInner: React.FC<DashboardDialogProps> = ({
title,
enrollmentId,
}) => {
const modal = NiceModal.useModal()
const destroyEnrollment = useDestroyEnrollment(enrollmentId)
const formik = useFormik({
enableReinitialize: true,
validateOnChange: false,
validateOnBlur: false,
initialValues: {},
onSubmit: async () => {
// TODO: Handle form submission
await destroyEnrollment.mutateAsync()
if (!destroyEnrollment.isError) {
modal.hide()
}
},
})
return (
@@ -107,15 +119,33 @@ const UnenrollDialogInner: React.FC<DashboardDialogProps> = ({ title }) => {
>
Cancel
</Button>
<Button variant="primary" type="submit">
<Button
variant="primary"
type="submit"
disabled={destroyEnrollment.isPending}
>
Unenroll
{destroyEnrollment.isPending && (
<SpinnerContainer>
<LoadingSpinner
loading={destroyEnrollment.isPending}
size={16}
/>
</SpinnerContainer>
)}
</Button>
</DialogActions>
}
>
<Typography variant="body1">
Are you sure you want to unenroll from {title}?
</Typography>
{destroyEnrollment.isError && (
<Alert severity="error">
There was a problem unenrolling you from this course. Please try again
later.
</Alert>
)}
</FormDialog>
)
}
Original file line number Diff line number Diff line change
@@ -4,109 +4,72 @@ import {
screen,
setMockResponse,
user,
within,
} from "@/test-utils"
import { EnrollmentDisplay } from "./EnrollmentDisplay"
import * as mitxonline from "api/mitxonline-test-utils"
import moment from "moment"
import { faker } from "@faker-js/faker/locale/en"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { setupEnrollments } from "./test-utils"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest
.mocked(useFeatureFlagEnabled)
.mockImplementation(() => false)

const courseEnrollment = mitxonline.factories.enrollment.courseEnrollment
const grade = mitxonline.factories.enrollment.grade
describe("EnrollmentDisplay", () => {
const setupApis = (includeExpired: boolean = true) => {
const completed = [
courseEnrollment({
run: { title: "C Course Ended" },
grades: [grade({ passed: true })],
}),
]
const expired = includeExpired
? [
courseEnrollment({
run: {
title: "A Course Ended",
end_date: faker.date.past().toISOString(),
},
}),
courseEnrollment({
run: {
title: "B Course Ended",
end_date: faker.date.past().toISOString(),
},
}),
]
: []
const started = [
courseEnrollment({
run: {
title: "A Course Started",
start_date: faker.date.past().toISOString(),
},
}),
courseEnrollment({
run: {
title: "B Course Started",
start_date: faker.date.past().toISOString(),
},
}),
]
const notStarted = [
courseEnrollment({
run: {
start_date: moment().add(1, "day").toISOString(), // Sooner first
},
}),
courseEnrollment({
run: {
start_date: moment().add(5, "day").toISOString(), // Later second
},
}),
]
const mitxonlineCourseEnrollments = faker.helpers.shuffle([
...expired,
...completed,
...started,
...notStarted,
])
const { enrollments, completed, expired, started, notStarted } =
setupEnrollments(includeExpired)

mockedUseFeatureFlagEnabled.mockReturnValue(true)
setMockResponse.get(
mitxonline.urls.enrollment.courseEnrollment(),
mitxonlineCourseEnrollments,
enrollments,
)

return {
mitxonlineCourseEnrollments,
mitxonlineCourses: { completed, expired, started, notStarted },
}
return { enrollments, completed, expired, started, notStarted }
}

test("Renders the expected cards", async () => {
const { mitxonlineCourses } = setupApis()
const { completed, started, notStarted } = setupApis()
renderWithProviders(<EnrollmentDisplay />)

await screen.findByRole("heading", { name: "My Learning" })

const cards = await screen.findAllByTestId("enrollment-card-desktop")
const expectedTitles = [
...mitxonlineCourses.started,
...mitxonlineCourses.notStarted,
...mitxonlineCourses.completed,
].map((e) => e.run.title)
const expectedTitles = [...started, ...notStarted, ...completed].map(
(e) => e.run.title,
)

expectedTitles.forEach((title, i) => {
expect(cards[i]).toHaveTextContent(title)
})
})

test("Renders the proper amount of unenroll and email settings buttons in the context menus", async () => {
const { enrollments } = setupApis()
renderWithProviders(<EnrollmentDisplay />)

const cards = await screen.findAllByTestId("enrollment-card-desktop")
expect(cards.length).toBe(enrollments.length)
for (const card of cards) {
const contextMenuButton =
await within(card).findByLabelText("More options")
await user.click(contextMenuButton)
const emailSettingsButton = await screen.findAllByRole("menuitem", {
name: "Email Settings",
})
const unenrollButton = await screen.findAllByRole("menuitem", {
name: "Unenroll",
})
expect(emailSettingsButton.length).toBe(1)
expect(unenrollButton.length).toBe(1)
await user.click(contextMenuButton)
}
})

test("Clicking show all reveals ended courses", async () => {
const { mitxonlineCourses } = setupApis()
const { completed, expired, started, notStarted } = setupApis()
renderWithProviders(<EnrollmentDisplay />)

const showAllButton = await screen.findByText("Show all")
@@ -116,10 +79,10 @@ describe("EnrollmentDisplay", () => {

const cards = await screen.findAllByTestId("enrollment-card-desktop")
const expectedTitles = [
...mitxonlineCourses.started,
...mitxonlineCourses.notStarted,
...mitxonlineCourses.completed,
...mitxonlineCourses.expired,
...started,
...notStarted,
...completed,
...expired,
].map((e) => e.run.title)

expectedTitles.forEach((title, i) => {
Original file line number Diff line number Diff line change
@@ -175,7 +175,7 @@ const EnrollmentExpandCollapse: React.FC<EnrollmentExpandCollapseProps> = ({

const EnrollmentDisplay = () => {
const { data: enrolledCourses, isLoading } = useQuery({
...enrollmentQueries.enrollmentsList({}),
...enrollmentQueries.enrollmentsList(),
select: mitxonlineEnrollments,
throwOnError: (error) => {
const err = error as MaybeHasStatusAndDetail
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import Image from "next/image"
import { styled, VisuallyHidden } from "ol-components"
import { styled } from "ol-components"
import { VisuallyHidden } from "@mitodl/smoot-design"
import CourseComplete from "@/public/images/icons/course-complete.svg"
import CourseInProgress from "@/public/images/icons/course-in-progress.svg"
import CourseUnenrolled from "@/public/images/icons/course-unenrolled.svg"
Original file line number Diff line number Diff line change
@@ -9,9 +9,12 @@ import type { DashboardCourse } from "./types"
import * as u from "api/test-utils"
import { urls, factories } from "api/mitxonline-test-utils"
import { setMockResponse } from "../../../test-utils"
import moment from "moment"

const makeCourses = factories.courses.courses
const makeProgram = factories.programs.program
const makeEnrollment = factories.enrollment.courseEnrollment
const makeGrade = factories.enrollment.grade

const dashboardCourse: PartialFactory<DashboardCourse> = (...overrides) => {
return mergeOverrides<DashboardCourse>(
@@ -29,6 +32,7 @@ const dashboardCourse: PartialFactory<DashboardCourse> = (...overrides) => {
coursewareUrl: faker.internet.url(),
},
enrollment: {
id: faker.number.int(),
status: faker.helpers.arrayElement(Object.values(EnrollmentStatus)),
mode: faker.helpers.arrayElement(Object.values(EnrollmentMode)),
},
@@ -37,6 +41,70 @@ const dashboardCourse: PartialFactory<DashboardCourse> = (...overrides) => {
)
}

const setupEnrollments = (includeExpired: boolean) => {
const completed = [
makeEnrollment({
run: { title: "C Course Ended" },
grades: [makeGrade({ passed: true })],
}),
]
const expired = includeExpired
? [
makeEnrollment({
run: {
title: "A Course Ended",
end_date: faker.date.past().toISOString(),
},
}),
makeEnrollment({
run: {
title: "B Course Ended",
end_date: faker.date.past().toISOString(),
},
}),
]
: []
const started = [
makeEnrollment({
run: {
title: "A Course Started",
start_date: faker.date.past().toISOString(),
},
}),
makeEnrollment({
run: {
title: "B Course Started",
start_date: faker.date.past().toISOString(),
},
}),
]
const notStarted = [
makeEnrollment({
run: {
start_date: moment().add(1, "day").toISOString(), // Sooner first
},
}),
makeEnrollment({
run: {
start_date: moment().add(5, "day").toISOString(), // Later second
},
}),
]
const enrollments = faker.helpers.shuffle([
...expired,
...completed,
...started,
...notStarted,
])
return {
enrollments: enrollments,
completed: completed,
expired: expired,
started: started,
notStarted: notStarted,
}
}

const setupProgramsAndCourses = () => {
const user = u.factories.user.user()
const orgX = factories.organizations.organization({ name: "Org X" })
@@ -82,4 +150,4 @@ const setupProgramsAndCourses = () => {
}
}

export { dashboardCourse, setupProgramsAndCourses }
export { dashboardCourse, setupEnrollments, setupProgramsAndCourses }
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ describe("Transforming mitxonline enrollment data to DashboardResource", () => {
canUpgrade: expect.any(Boolean), // check this in a moment
},
enrollment: {
id: apiData.id,
status: enrollmentStatus,
mode: apiData.enrollment_mode,
},
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ const mitxonlineEnrollment = (raw: CourseRunEnrollment): DashboardCourse => {
coursewareUrl: raw.run.courseware_url,
},
enrollment: {
id: raw.id,
mode: raw.enrollment_mode,
status: raw.grades[0]?.passed
? EnrollmentStatus.Completed
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ type DashboardCourse = {
canUpgrade: boolean
}
enrollment?: {
id: number
status: EnrollmentStatus
mode: EnrollmentMode
}
11 changes: 6 additions & 5 deletions frontends/main/src/app-pages/DashboardPage/DashboardLayout.tsx
Original file line number Diff line number Diff line change
@@ -14,14 +14,13 @@ import {
Container,
Skeleton,
Tab,
TabButtonLink,
TabButtonList,
TabContext,
TabPanel,
TabList,
Typography,
styled,
} from "ol-components"
import { TabButtonLink, TabButtonList } from "@mitodl/smoot-design"
import Link from "next/link"
import { usePathname } from "next/navigation"
import backgroundImage from "@/public/images/backgrounds/user_menu_background.svg"
@@ -308,13 +307,15 @@ const DashboardPage: React.FC<{
}> = ({ children }) => {
const pathname = usePathname()
const { isLoading: isLoadingUser, data: user } = useUserMe()
const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } =
useMitxOnlineCurrentUser()
const orgsEnabled = useFeatureFlagEnabled(FeatureFlags.OrganizationDashboard)
const { isLoading: isLoadingMitxOnlineUser, data: mitxOnlineUser } =
useMitxOnlineCurrentUser({ enabled: !!orgsEnabled })

const tabData = useMemo(
() =>
isLoadingMitxOnlineUser ? [] : getTabData(orgsEnabled, mitxOnlineUser),
isLoadingMitxOnlineUser
? getTabData(orgsEnabled)
: getTabData(orgsEnabled, mitxOnlineUser),
[isLoadingMitxOnlineUser, orgsEnabled, mitxOnlineUser],
)

Original file line number Diff line number Diff line change
@@ -18,7 +18,6 @@ import * as mitxonline from "api/mitxonline-test-utils"
import { useFeatureFlagEnabled } from "posthog-js/react"
import HomeContent from "./HomeContent"
import invariant from "tiny-invariant"
import { courseEnrollments } from "../../../../api/src/mitxonline/test-utils/factories/enrollment"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest
@@ -210,7 +209,7 @@ describe("HomeContent", () => {
mockedUseFeatureFlagEnabled.mockReturnValue(enrollmentsEnabled)

if (enrollmentsEnabled) {
const enrollments = courseEnrollments(3)
const enrollments = mitxonline.factories.enrollment.courseEnrollments(3)
setMockResponse.get(
mitxonline.urls.enrollment.courseEnrollment(),
enrollments,
10 changes: 5 additions & 5 deletions frontends/main/src/app-pages/DashboardPage/HomeContent.tsx
Original file line number Diff line number Diff line change
@@ -13,10 +13,10 @@ import {
FREE_COURSES_CAROUSEL,
} from "@/common/carousels"
import ResourceCarousel from "@/page-components/ResourceCarousel/ResourceCarousel"
import { useProfileMeQuery } from "api/hooks/profile"
import { EnrollmentDisplay } from "./CoursewareDisplay/EnrollmentDisplay"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { FeatureFlags } from "@/common/feature_flags"
import { useUserMe } from "api/hooks/user"

const SubTitleText = styled(Typography)(({ theme }) => ({
color: theme.custom.colors.darkGray2,
@@ -66,9 +66,9 @@ const TitleText = styled(Typography)(({ theme }) => ({
})) as typeof Typography

const HomeContent: React.FC = () => {
const { isLoading: isLoadingProfile, data: profile } = useProfileMeQuery()
const topics = profile?.preference_search_filters.topic
const certification = profile?.preference_search_filters.certification
const { isLoading: isLoadingProfile, data: user } = useUserMe()
const topics = user?.profile?.preference_search_filters.topic
const certification = user?.profile?.preference_search_filters.certification
const showEnrollments = useFeatureFlagEnabled(
FeatureFlags.EnrollmentDashboard,
)
@@ -93,7 +93,7 @@ const HomeContent: React.FC = () => {
titleComponent="h2"
title="Top picks for you"
isLoading={isLoadingProfile}
config={TopPicksCarouselConfig(profile)}
config={TopPicksCarouselConfig(user?.profile)}
/>
{topics?.map((topic, index) => (
<StyledResourceCarousel
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ describe("OrganizationContent", () => {
it("displays a header for each program returned and cards for courses in program", async () => {
const { orgX, programA, programB, coursesA, coursesB } =
setupProgramsAndCourses()
setMockResponse.get(urls.enrollment.courseEnrollment({}), [])
setMockResponse.get(urls.enrollment.courseEnrollment(), [])
renderWithProviders(<OrganizationContent orgSlug={orgX.slug} />)

await screen.findByRole("heading", {
@@ -60,7 +60,7 @@ describe("OrganizationContent", () => {
grades: [],
}),
]
setMockResponse.get(urls.enrollment.courseEnrollment({}), enrollments)
setMockResponse.get(urls.enrollment.courseEnrollment(), enrollments)
renderWithProviders(<OrganizationContent orgSlug={orgX.slug} />)

const [programElA] = await screen.findAllByTestId("org-program-root")
Original file line number Diff line number Diff line change
@@ -148,7 +148,7 @@ const OrganizationContentInternal: React.FC<
FeatureFlags.OrganizationDashboard,
)
const orgId = org.id
const enrollments = useQuery(enrollmentQueries.enrollmentsList({}))
const enrollments = useQuery(enrollmentQueries.enrollmentsList())
const programs = useQuery(programsQueries.programsList({ org_id: orgId }))
const courseGroups = useMitxonlineProgramsCourses(
programs.data?.results ?? [],
14 changes: 8 additions & 6 deletions frontends/main/src/app-pages/DashboardPage/ProfileContent.tsx
Original file line number Diff line number Diff line change
@@ -4,15 +4,17 @@ import { useFormik } from "formik"
import { useProfileMeMutation, useProfileMeQuery } from "api/hooks/profile"
import {
styled,
CircularProgress,
CheckboxChoiceBoxField,
CheckboxChoiceField,
RadioChoiceField,
SimpleSelectField,
TextField,
Skeleton,
} from "ol-components"
import { Button } from "@mitodl/smoot-design"
import {
Button,
CheckboxChoiceField,
RadioChoiceField,
TextField,
ButtonLoadingIcon,
} from "@mitodl/smoot-design"

import { useLearningResourceTopics } from "api/hooks/learningResources"
import {
@@ -169,7 +171,7 @@ const ProfileContent: React.FC = () => {
type="submit"
size="large"
variant="primary"
endIcon={isSaving ? <CircularProgress /> : null}
endIcon={isSaving ? <ButtonLoadingIcon /> : null}
disabled={!formik.dirty || isSaving}
form={formId}
>
Original file line number Diff line number Diff line change
@@ -10,15 +10,13 @@ import {
StepIconProps,
Container,
LoadingSpinner,
CircularProgress,
Typography,
CheckboxChoiceBoxField,
RadioChoiceBoxField,
SimpleSelectField,
Skeleton,
VisuallyHidden,
} from "ol-components"
import { Button } from "@mitodl/smoot-design"
import { Button, ButtonLoadingIcon, VisuallyHidden } from "@mitodl/smoot-design"

import { RiArrowRightLine, RiArrowLeftLine } from "@remixicon/react"
import { useProfileMeMutation, useProfileMeQuery } from "api/hooks/profile"
@@ -352,7 +350,7 @@ const OnboardingPage: React.FC = () => {
endIcon={
activeStep < NUM_STEPS - 1 ? (
isSaving ? (
<CircularProgress />
<ButtonLoadingIcon />
) : (
<RiArrowRightLine />
)
3 changes: 2 additions & 1 deletion frontends/main/src/app-pages/SearchPage/SearchPage.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@ import type { FacetManifest } from "@mitodl/course-search-utils"
import { useSearchParams } from "@mitodl/course-search-utils/next"
import { useResourceSearchParams } from "@mitodl/course-search-utils"
import SearchDisplay from "@/page-components/SearchDisplay/SearchDisplay"
import { styled, Container, theme, VisuallyHidden } from "ol-components"
import { styled, Container, theme } from "ol-components"
import { VisuallyHidden } from "@mitodl/smoot-design"
import { SearchField } from "@/page-components/SearchField/SearchField"
import { useOfferorsList } from "api/hooks/learningResources"
import { facetNames } from "./searchRequests"
Original file line number Diff line number Diff line change
@@ -3,11 +3,10 @@ import {
LoadingSpinner,
Typography,
styled,
CheckboxChoiceField,
FormDialog,
DialogActions,
} from "ol-components"
import { Button } from "@mitodl/smoot-design"
import { Button, CheckboxChoiceField } from "@mitodl/smoot-design"

import { RiAddLine } from "@remixicon/react"
import { usePostHog } from "posthog-js/react"
Original file line number Diff line number Diff line change
@@ -7,14 +7,13 @@ import {
theme,
PlatformLogo,
PLATFORM_LOGOS,
Input,
Typography,
} from "ol-components"
import Link from "next/link"
import type { ImageConfig, LearningResourceCardProps } from "ol-components"
import { DEFAULT_RESOURCE_IMG } from "ol-utilities"
import { ResourceTypeEnum, PlatformEnum } from "api"
import { Button, ButtonLink, ButtonProps } from "@mitodl/smoot-design"
import { Button, ButtonLink, ButtonProps, Input } from "@mitodl/smoot-design"
import type { LearningResource } from "api"
import {
RiBookmarkFill,
Original file line number Diff line number Diff line change
@@ -2,16 +2,13 @@ import React, { useCallback } from "react"
import { useFormik, FormikConfig } from "formik"
import NiceModal, { muiDialogV5 } from "@ebay/nice-modal-react"
import { RiDeleteBinLine } from "@remixicon/react"
import { Autocomplete, FormDialog, Dialog, MenuItem } from "ol-components"
import {
Button,
BooleanRadioChoiceField,
Alert,
TextField,
Autocomplete,
BooleanRadioChoiceField,
FormDialog,
Dialog,
MenuItem,
} from "ol-components"
import { Button } from "@mitodl/smoot-design"
} from "@mitodl/smoot-design"
import * as Yup from "yup"
import { PrivacyLevelEnum, type LearningPathResource, UserList } from "api"

Original file line number Diff line number Diff line change
@@ -4,14 +4,13 @@ import React from "react"
import { learningResourceQueries } from "api/hooks/learningResources"
import {
Carousel,
TabButton,
TabPanel,
TabContext,
TabButtonList,
styled,
Typography,
TypographyProps,
} from "ol-components"
import { TabButton, TabButtonList } from "@mitodl/smoot-design"
import type { TabConfig } from "./types"
import { LearningResource, PaginatedLearningResourceList } from "api"
import { ResourceCard } from "../ResourceCard/ResourceCard"
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import React from "react"
import {
TabButton,
TabContext,
TabButtonList,
TabPanel,
styled,
} from "ol-components"
import { TabContext, TabPanel, styled } from "ol-components"
import { TabButton, TabButtonList } from "@mitodl/smoot-design"
import { ResourceCategoryEnum, LearningResourcesSearchResponse } from "api"

const TabsList = styled(TabButtonList)(({ theme }) => ({
Original file line number Diff line number Diff line change
@@ -10,11 +10,14 @@ import {
truncateText,
css,
Drawer,
Checkbox,
VisuallyHidden,
Stack,
} from "ol-components"
import { Button, ButtonProps } from "@mitodl/smoot-design"
import {
Button,
ButtonProps,
childCheckboxStyles,
VisuallyHidden,
} from "@mitodl/smoot-design"

import {
RiCloseLine,
@@ -195,7 +198,7 @@ const FacetStyles = styled.div`
cursor: pointer;
}
${Checkbox.styles}
${({ theme }) => childCheckboxStyles(theme)}
.facet-count {
font-size: 12px;
93 changes: 0 additions & 93 deletions frontends/ol-components/src/components/Alert/Alert.stories.tsx

This file was deleted.

99 changes: 0 additions & 99 deletions frontends/ol-components/src/components/Alert/Alert.tsx

This file was deleted.

This file was deleted.

131 changes: 0 additions & 131 deletions frontends/ol-components/src/components/Checkbox/Checkbox.tsx

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"
import styled from "@emotion/styled"
import { type GridProps } from "@mui/material/Grid"
import { Checkbox } from "../Checkbox/Checkbox"
import { Checkbox } from "@mitodl/smoot-design"
import { Radio } from "../Radio/Radio"

const Container = styled.label(({ theme }) => {
@@ -53,7 +53,7 @@ const Description = styled.span(({ theme }) => ({
},
}))

const Input = styled.div({
const InputContainer = styled.div({
flexShrink: 0,
})

@@ -82,7 +82,7 @@ const ChoiceBox = ({
<Label>{label}</Label>
{description ? <Description>{description}</Description> : null}
</Text>
<Input>
<InputContainer>
{type === "checkbox" ? (
<Checkbox
name={name}
@@ -99,7 +99,7 @@ const ChoiceBox = ({
onChange={onChange}
/>
) : null}
</Input>
</InputContainer>
</Container>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"
import type { Meta, StoryObj } from "@storybook/react"
import { FormDialog, FormDialogProps } from "./FormDialog"
import { TextField } from "../TextField/TextField"
import { TextField } from "@mitodl/smoot-design"
import MuiButton from "@mui/material/Button"
import { action } from "@storybook/addon-actions"

20 changes: 0 additions & 20 deletions frontends/ol-components/src/components/Input/Input.mdx

This file was deleted.

185 changes: 0 additions & 185 deletions frontends/ol-components/src/components/Input/Input.stories.tsx

This file was deleted.

27 changes: 0 additions & 27 deletions frontends/ol-components/src/components/Input/Input.test.tsx

This file was deleted.

249 changes: 0 additions & 249 deletions frontends/ol-components/src/components/Input/Input.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -15,19 +15,21 @@ const Container = styled.div({

type LoadingSpinnerProps = {
loading: boolean
size?: number | string
"aria-label"?: string
}

const noDelay = { transitionDelay: "0ms" }

const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
loading,
size,
"aria-label": label = "Loading",
}) => {
return (
<Container>
<Fade in={loading} style={!loading ? noDelay : undefined} unmountOnExit>
<CircularProgress aria-label={label} />
<CircularProgress aria-label={label} size={size} />
</Fade>
</Container>
)

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react"
import { RiSearch2Line, RiCloseLine } from "@remixicon/react"
import { Input, AdornmentButton } from "../Input/Input"
import type { InputProps } from "../Input/Input"
import { Input, AdornmentButton } from "@mitodl/smoot-design"
import type { InputProps } from "@mitodl/smoot-design"
import styled from "@emotion/styled"

const StyledInput = styled(Input)(({ theme }) => ({
Original file line number Diff line number Diff line change
@@ -9,8 +9,8 @@ import type { InputBaseProps } from "@mui/material/InputBase"
import { FormFieldWrapper } from "../FormHelpers/FormHelpers"
import type { FormFieldWrapperProps } from "../FormHelpers/FormHelpers"
import styled from "@emotion/styled"
import { baseInputStyles } from "../Input/Input"
import { RiArrowDownSLine } from "@remixicon/react"
import type { Theme } from "@mui/material/styles"

type SelectProps<Value = unknown> = Omit<
MuiSelectProps<Value>,
@@ -22,6 +22,67 @@ type SelectInputProps = Omit<InputBaseProps, "size"> & {

const DEFAULT_SIZE = "medium"

/**
* Base styles for Input and Select components. Includes border, color, hover effects.
*
* TODO: This is duplicated in smoot-design's <index className="tsx">
*
* When SelectField is moved, we can remove the duplication.
*/
const baseInputStyles = (theme: Theme) => ({
backgroundColor: "white",
color: theme.custom.colors.darkGray2,
borderColor: theme.custom.colors.silverGrayLight,
borderWidth: "1px",
borderStyle: "solid",
borderRadius: "4px",
"&.Mui-disabled": {
backgroundColor: theme.custom.colors.lightGray1,
},
"&:hover:not(.Mui-disabled):not(.Mui-focused)": {
borderColor: theme.custom.colors.darkGray2,
},
"&.Mui-focused": {
/**
* When change border width, it affects either the elements outside of it or
* inside based on the border-box setting.
*
* Instead of changing the border width, we hide the border and change width
* using outline.
*/
borderColor: "transparent",
outline: "2px solid currentcolor",
outlineOffset: "-2px",
},
"&.Mui-error": {
borderColor: theme.custom.colors.red,
outlineColor: theme.custom.colors.red,
},
"& input::placeholder, textarea::placeholder": {
color: theme.custom.colors.silverGrayDark,
opacity: 1, // some browsers apply opacity to placeholder text
},
"& input:placeholder-shown, textarea:placeholder-shown": {
textOverflow: "ellipsis",
},
"& textarea": {
paddingTop: "8px",
paddingBottom: "8px",
},
"&.MuiInputBase-adornedStart": {
paddingLeft: "0",
input: {
paddingLeft: "8px",
},
},
"&.MuiInputBase-adornedEnd": {
paddingRight: "0",
input: {
paddingRight: "8px",
},
},
})

const SelectInput = styled(InputBase as React.FC<SelectInputProps>)(
({ theme, size = DEFAULT_SIZE }) => [
baseInputStyles(theme),

This file was deleted.

This file was deleted.

This file was deleted.

19 changes: 0 additions & 19 deletions frontends/ol-components/src/components/TextField/TextField.mdx

This file was deleted.

179 changes: 0 additions & 179 deletions frontends/ol-components/src/components/TextField/TextField.stories.tsx

This file was deleted.

This file was deleted.

97 changes: 0 additions & 97 deletions frontends/ol-components/src/components/TextField/TextField.tsx

This file was deleted.

This file was deleted.

18 changes: 1 addition & 17 deletions frontends/ol-components/src/index.ts
Original file line number Diff line number Diff line change
@@ -64,12 +64,6 @@ export type { TabProps } from "@mui/material/Tab"
export { default as TabList } from "@mui/lab/TabList"
export type { TabListProps } from "@mui/lab/TabList"

export {
TabButton,
TabButtonLink,
TabButtonList,
} from "./components/TabButtons/TabButtonList"

export { default as TabContext } from "@mui/lab/TabContext"
export type { TabContextProps } from "@mui/lab/TabContext"
export { default as TabPanel } from "@mui/lab/TabPanel"
@@ -99,20 +93,16 @@ export { default as Step } from "@mui/material/Step"
export { default as StepLabel } from "@mui/material/StepLabel"
export type { StepIconProps } from "@mui/material/StepIcon"

export { default as CircularProgress } from "@mui/material/CircularProgress"
export { default as FormGroup } from "@mui/material/FormGroup"
export { default as Slider } from "@mui/material/Slider"

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 { onReInitSlickA11y } from "./components/Carousel/util"

export * from "./components/Checkbox/Checkbox"
export * from "./components/Checkbox/CheckboxChoiceField"
export * from "./components/Chips/ChipLink"
export * from "./components/ChoiceBox/ChoiceBox"
export * from "./components/ChoiceBox/ChoiceBoxField"
@@ -133,20 +123,15 @@ export * from "./components/SimpleMenu/SimpleMenu"
export * from "./components/SortableList/SortableList"
export * from "./components/ThemeProvider/ThemeProvider"
export * from "./components/TruncateText/TruncateText"
export * from "./components/Radio/Radio"
export * from "./components/RadioChoiceField/RadioChoiceField"
export * from "./components/VisuallyHidden/VisuallyHidden"

export * from "./constants/imgConfigs"

export { Input, AdornmentButton } from "./components/Input/Input"
export type { InputProps, AdornmentButtonProps } from "./components/Input/Input"
export { SearchInput } from "./components/SearchInput/SearchInput"
export type {
SearchInputProps,
SearchSubmissionEvent,
} from "./components/SearchInput/SearchInput"
export { TextField } from "./components/TextField/TextField"

export {
SimpleSelect,
SimpleSelectField,
@@ -157,7 +142,6 @@ export type {
SimpleSelectOption,
} from "./components/SimpleSelect/SimpleSelect"

export type { TextFieldProps } from "./components/TextField/TextField"
export { SelectField } from "./components/SelectField/SelectField"
export type {
SelectChangeEvent,
98 changes: 98 additions & 0 deletions learning_resources/etl/canvas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logging
import zipfile
from collections.abc import Generator
from pathlib import Path
from tempfile import TemporaryDirectory

from defusedxml import ElementTree

from learning_resources.constants import LearningResourceType
from learning_resources.etl.constants import ETLSource
from learning_resources.etl.utils import _process_olx_path, calc_checksum
from learning_resources.models import LearningResource, LearningResourceRun

log = logging.getLogger(__name__)


def sync_canvas_archive(bucket, key: str, overwrite):
"""
Sync a Canvas course archive from S3
"""
from learning_resources.etl.loaders import load_content_files

with TemporaryDirectory() as export_tempdir:
course_archive_path = Path(export_tempdir, key.split("/")[-1])
bucket.download_file(key, course_archive_path)
run = run_for_canvas_archive(course_archive_path, overwrite=overwrite)
checksum = calc_checksum(course_archive_path)
if run:
load_content_files(
run,
transform_canvas_content_files(
course_archive_path, run, overwrite=overwrite
),
)
run.checksum = checksum
run.save()


def run_for_canvas_archive(course_archive_path, overwrite):
"""
Generate and return a LearningResourceRun for a Canvas course
"""
checksum = calc_checksum(course_archive_path)
course_info = parse_canvas_settings(course_archive_path)
course_title = course_info.get("title")
readable_id = course_info.get("course_code")
# create placeholder learning resource
resource, _ = LearningResource.objects.get_or_create(
readable_id=readable_id,
defaults={
"title": course_title,
"published": False,
"test_mode": True,
"etl_source": ETLSource.canvas.name,
"resource_type": LearningResourceType.course.name,
},
)
if resource.runs.count() == 0:
LearningResourceRun.objects.create(
run_id=f"{readable_id}+canvas", learning_resource=resource, published=True
)
run = resource.runs.first()
if run.checksum == checksum and not overwrite:
log.info("Checksums match for %s, skipping load", readable_id)
return None
run.checksum = checksum
run.save()
return run


def parse_canvas_settings(course_archive_path):
"""
Get course attributes from a Canvas course archive
"""
with zipfile.ZipFile(course_archive_path, "r") as course_archive:
xml_string = course_archive.read("course_settings/course_settings.xml")
tree = ElementTree.fromstring(xml_string)
attributes = {}
for node in tree.iter():
tag = node.tag.split("}")[1] if "}" in node.tag else node.tag
attributes[tag] = node.text
return attributes


def transform_canvas_content_files(
course_zipfile: Path, run: LearningResourceRun, *, overwrite
) -> Generator[dict, None, None]:
"""
Transform content files from a Canvas course zipfile
"""
basedir = course_zipfile.name.split(".")[0]
with (
TemporaryDirectory(prefix=basedir) as olx_path,
zipfile.ZipFile(course_zipfile.absolute(), "r") as course_archive,
):
for member in course_archive.infolist():
course_archive.extract(member, path=olx_path)
yield from _process_olx_path(olx_path, run, overwrite=overwrite)
1 change: 1 addition & 0 deletions learning_resources/etl/constants.py
Original file line number Diff line number Diff line change
@@ -90,6 +90,7 @@ class ETLSource(ExtendedEnum):
see = "see"
xpro = "xpro"
youtube = "youtube"
canvas = "canvas"


class CourseNumberType(Enum):
Loading