Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"

import type { MutationHookOptions } from "../../../mutations/mutationMeta"
import mitxAxios from "../../axios"
import {
NOTIFICATION_PREFERENCES_URL,
notificationPreferencesKeys,
notificationPreferencesQueries,
} from "./queries"
import type {
NotificationPreferences,
NotificationPreferenceUpdate,
PreferenceConfig,
PreferenceGroup,
} from "./queries"

type UseNotificationPreferencesOptions = {
enabled?: boolean
}

const useNotificationPreferences = ({
enabled = true,
}: UseNotificationPreferencesOptions = {}) => {
return useQuery({
...notificationPreferencesQueries.detail(),
enabled,
})
}

/**
* Open edX updates one channel per request, so each toggle or cadence change is
* its own mutation.
*
* The response body is not enough to render from — the LMS fans a change to a
* grouped type out to several types — so we re-read on success rather than
* patching the cache.
*/
const useUpdateNotificationPreference = ({
meta,
}: MutationHookOptions = {}) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (update: NotificationPreferenceUpdate) =>
mitxAxios.put(NOTIFICATION_PREFERENCES_URL, update),
onSuccess: () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that onSuccess calls invalidateQueries without returning its promise, so the mutation leaves isPending as soon as the PUT returns, before the refetch lands. For that gap the row is enabled again but still shows the old config. A second click then sends the same inverted value, which is the bug the pending guard was meant to prevent. The comment in NotificationPreferences.tsx:171-175 says "disable them until the refetch lands", which isn't what happens yet. Returning the promise fixes it: onSuccess: () => queryClient.invalidateQueries({ queryKey: ... }). TanStack waits on a promise returned from onSuccess, so isPending then covers the refetch. The existing test can't catch this because its PUT never resolves. A test where the PUT resolves but the GET is held would.

queryClient.invalidateQueries({
queryKey: notificationPreferencesKeys.detail(),
})
},
meta,
})
}

export {
useNotificationPreferences,
useUpdateNotificationPreference,
notificationPreferencesQueries,
notificationPreferencesKeys,
}
export type {
NotificationPreferences,
NotificationPreferenceUpdate,
PreferenceConfig,
PreferenceGroup,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { queryOptions } from "@tanstack/react-query"

import mitxAxios from "../../axios"

/**
* MITx Online proxies these straight through from Open edX, which owns the
* state. The endpoint is excluded from MITx Online's OpenAPI schema, so there
* is no generated client for it and we call it through the configured axios
* instance instead.
*/
const NOTIFICATION_PREFERENCES_URL = "/api/notification-preferences/"

type NotificationChannel = "web" | "email" | "email_cadence"

type PreferenceConfig = {
web: boolean
push: boolean
email: boolean
email_cadence: string
info: string
}

type PreferenceGroup = {
enabled: boolean
/**
* Keyed by notification type -> the channels that type locks, e.g.
* `{ new_discussion_post: ["push"] }`. Not a flat list.
*/
non_editable: Record<string, string[]> | string[] | null
notification_types: Record<string, PreferenceConfig>
}

type NotificationPreferences = {
data?: Record<string, PreferenceGroup> | null
/** The LMS gates the whole feature with this. */
show_preferences?: boolean
show_email_preferences?: boolean
}

type NotificationPreferenceUpdate = {
notification_app: string
notification_type: string
notification_channel: NotificationChannel
} & ({ value: boolean } | { email_cadence: string })

const notificationPreferencesKeys = {
root: ["mitxonline", "notificationPreferences"],
detail: () => [...notificationPreferencesKeys.root, "detail"],
}

const notificationPreferencesQueries = {
detail: () =>
queryOptions({
queryKey: notificationPreferencesKeys.detail(),
queryFn: async (): Promise<NotificationPreferences> => {
return mitxAxios
.get(NOTIFICATION_PREFERENCES_URL)
.then((res) => res.data)
},
/**
* A 409 means the learner has no Open edX account yet, and a 4xx will not
* start working by asking again. Retrying only delays the notice the
* section shows in its place.
*/
retry: false,
}),
}

export {
notificationPreferencesQueries,
notificationPreferencesKeys,
NOTIFICATION_PREFERENCES_URL,
}
export type {
NotificationChannel,
NotificationPreferences,
NotificationPreferenceUpdate,
PreferenceConfig,
PreferenceGroup,
}
6 changes: 6 additions & 0 deletions frontends/api/src/mitxonline/test-utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ const verifiedProgramEnrollments = {
`${getApiBaseUrl()}/api/v2/verified_program_enrollments/${encodeURIComponent(courserunId)}/`,
}

const notificationPreferences = {
get: () => `${getApiBaseUrl()}/api/notification-preferences/`,
put: () => `${getApiBaseUrl()}/api/notification-preferences/`,
}

export {
b2b,
b2bAttach,
Expand All @@ -184,4 +189,5 @@ export {
baskets,
orders,
verifiedProgramEnrollments,
notificationPreferences,
}
Loading
Loading