feat(admin-ui): Phase B frontend — notifications preferences route + UI
This commit is contained in:
@@ -719,3 +719,18 @@ export type NotificationsResponse = {
|
||||
/** Unread count for badge — pre-filtered server-side. */
|
||||
totalUnread: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase B — per-channel notification opt-in/out для current user. Backend
|
||||
* endpoint: GET/PUT /api/v1/me/notifications/preferences. Default semantics
|
||||
* (server-side, when row missing): email=true, express=false, telegram=false.
|
||||
*
|
||||
* <p>Express/Telegram channels пока not wired backend-side — toggles в UI
|
||||
* disabled с tooltip 'Скоро'. Email — единственный фактически работающий
|
||||
* канал в Phase A/B.
|
||||
*/
|
||||
export type NotificationPreferences = {
|
||||
email: boolean
|
||||
express: boolean
|
||||
telegram: boolean
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type DecisionRequest,
|
||||
type DictionaryDetail,
|
||||
type DraftResponse,
|
||||
type NotificationPreferences,
|
||||
type PublishSchemaDraftRequest,
|
||||
type PublishSchemaDraftResult,
|
||||
type RecordResponse,
|
||||
@@ -700,3 +701,43 @@ export const useMarkAllNotificationsRead = () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase B — update per-channel notification preferences. PUT upserts all 3
|
||||
* каналов атомарно (backend @Transactional). Optimistic update — UI flips
|
||||
* toggle instantly, onError refetch restores original state.
|
||||
*
|
||||
* <p>Cache key 'notifications/me/preferences' — separate from feed query
|
||||
* (которая использует 'notifications/me'). Invalidate prefs alone не
|
||||
* triggers feed refetch.
|
||||
*/
|
||||
export const useUpdateNotificationPreferences = () => {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async (prefs: NotificationPreferences): Promise<NotificationPreferences> => {
|
||||
const { data } = await apiClient.put<NotificationPreferences>(
|
||||
'/me/notifications/preferences',
|
||||
prefs,
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: async (next) => {
|
||||
await qc.cancelQueries({ queryKey: ['notifications', 'me', 'preferences'] })
|
||||
const prev = qc.getQueryData<NotificationPreferences>([
|
||||
'notifications',
|
||||
'me',
|
||||
'preferences',
|
||||
])
|
||||
qc.setQueryData(['notifications', 'me', 'preferences'], next)
|
||||
return { prev }
|
||||
},
|
||||
onError: (_err, _next, ctx) => {
|
||||
if (ctx?.prev) {
|
||||
qc.setQueryData(['notifications', 'me', 'preferences'], ctx.prev)
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
qc.invalidateQueries({ queryKey: ['notifications', 'me', 'preferences'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type CascadePlan,
|
||||
type ChangelogDiff,
|
||||
type ChangelogResponse,
|
||||
type NotificationPreferences,
|
||||
type NotificationsResponse,
|
||||
type SchemaDraft,
|
||||
type SchemaReviewQueuePage,
|
||||
@@ -851,3 +852,23 @@ export const myNotificationsQuery = (unreadOnly = false, limit = 20) =>
|
||||
|
||||
export const useMyNotifications = (unreadOnly = false, limit = 20) =>
|
||||
useQuery(myNotificationsQuery(unreadOnly, limit))
|
||||
|
||||
/**
|
||||
* Phase B — per-channel notification preferences для current user. GET — single
|
||||
* row (no pagination), 401 если anonymous. Default semantics filled in
|
||||
* backend-side когда DB row missing (email=true / express=false / telegram=false).
|
||||
*/
|
||||
export const myNotificationPreferencesQuery = () =>
|
||||
queryOptions({
|
||||
queryKey: ['notifications', 'me', 'preferences'] as const,
|
||||
queryFn: async (): Promise<NotificationPreferences> => {
|
||||
const { data } = await apiClient.get<NotificationPreferences>(
|
||||
'/me/notifications/preferences',
|
||||
)
|
||||
return data
|
||||
},
|
||||
staleTime: 60_000, // prefs не меняются часто, кэш дольше
|
||||
})
|
||||
|
||||
export const useMyNotificationPreferences = () =>
|
||||
useQuery(myNotificationPreferencesQuery())
|
||||
|
||||
Reference in New Issue
Block a user