@checkstack/notification-frontend 0.0.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -0
- package/package.json +1 -1
- package/src/components/NotificationBell.tsx +47 -64
- package/src/pages/NotificationSettingsPage.tsx +183 -265
- package/src/pages/NotificationsPage.tsx +65 -74
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# @checkstack/notification-frontend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 7a23261: ## TanStack Query Integration
|
|
8
|
+
|
|
9
|
+
Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
|
|
10
|
+
|
|
11
|
+
### New Features
|
|
12
|
+
|
|
13
|
+
- **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
|
|
14
|
+
- **Automatic request deduplication**: Multiple components requesting the same data share a single network request
|
|
15
|
+
- **Built-in caching**: Configurable stale time and cache duration per query
|
|
16
|
+
- **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
|
|
17
|
+
- **Background refetching**: Stale data is automatically refreshed when components mount
|
|
18
|
+
|
|
19
|
+
### Contract Changes
|
|
20
|
+
|
|
21
|
+
All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const getItems = proc()
|
|
25
|
+
.meta({ operationType: "query", access: [access.read] })
|
|
26
|
+
.output(z.array(itemSchema))
|
|
27
|
+
.query();
|
|
28
|
+
|
|
29
|
+
const createItem = proc()
|
|
30
|
+
.meta({ operationType: "mutation", access: [access.manage] })
|
|
31
|
+
.input(createItemSchema)
|
|
32
|
+
.output(itemSchema)
|
|
33
|
+
.mutation();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// Before (forPlugin pattern)
|
|
40
|
+
const api = useApi(myPluginApiRef);
|
|
41
|
+
const [items, setItems] = useState<Item[]>([]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
api.getItems().then(setItems);
|
|
44
|
+
}, [api]);
|
|
45
|
+
|
|
46
|
+
// After (usePluginClient pattern)
|
|
47
|
+
const client = usePluginClient(MyPluginApi);
|
|
48
|
+
const { data: items, isLoading } = client.getItems.useQuery({});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Bug Fixes
|
|
52
|
+
|
|
53
|
+
- Fixed `rpc.test.ts` test setup for middleware type inference
|
|
54
|
+
- Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
|
|
55
|
+
- Fixed null→undefined warnings in notification and queue frontends
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [7a23261]
|
|
60
|
+
- @checkstack/frontend-api@0.2.0
|
|
61
|
+
- @checkstack/common@0.3.0
|
|
62
|
+
- @checkstack/auth-frontend@0.3.0
|
|
63
|
+
- @checkstack/notification-common@0.2.0
|
|
64
|
+
- @checkstack/ui@0.2.1
|
|
65
|
+
- @checkstack/signal-frontend@0.0.7
|
|
66
|
+
|
|
67
|
+
## 0.1.0
|
|
68
|
+
|
|
69
|
+
### Minor Changes
|
|
70
|
+
|
|
71
|
+
- 9faec1f: # Unified AccessRule Terminology Refactoring
|
|
72
|
+
|
|
73
|
+
This release completes a comprehensive terminology refactoring from "permission" to "accessRule" across the entire codebase, establishing a consistent and modern access control vocabulary.
|
|
74
|
+
|
|
75
|
+
## Changes
|
|
76
|
+
|
|
77
|
+
### Core Infrastructure (`@checkstack/common`)
|
|
78
|
+
|
|
79
|
+
- Introduced `AccessRule` interface as the primary access control type
|
|
80
|
+
- Added `accessPair()` helper for creating read/manage access rule pairs
|
|
81
|
+
- Added `access()` builder for individual access rules
|
|
82
|
+
- Replaced `Permission` type with `AccessRule` throughout
|
|
83
|
+
|
|
84
|
+
### API Changes
|
|
85
|
+
|
|
86
|
+
- `env.registerPermissions()` → `env.registerAccessRules()`
|
|
87
|
+
- `meta.permissions` → `meta.access` in RPC contracts
|
|
88
|
+
- `usePermission()` → `useAccess()` in frontend hooks
|
|
89
|
+
- Route `permission:` field → `accessRule:` field
|
|
90
|
+
|
|
91
|
+
### UI Changes
|
|
92
|
+
|
|
93
|
+
- "Roles & Permissions" tab → "Roles & Access Rules"
|
|
94
|
+
- "You don't have permission..." → "You don't have access..."
|
|
95
|
+
- All permission-related UI text updated
|
|
96
|
+
|
|
97
|
+
### Documentation & Templates
|
|
98
|
+
|
|
99
|
+
- Updated 18 documentation files with AccessRule terminology
|
|
100
|
+
- Updated 7 scaffolding templates with `accessPair()` pattern
|
|
101
|
+
- All code examples use new AccessRule API
|
|
102
|
+
|
|
103
|
+
## Migration Guide
|
|
104
|
+
|
|
105
|
+
### Backend Plugins
|
|
106
|
+
|
|
107
|
+
```diff
|
|
108
|
+
- import { permissionList } from "./permissions";
|
|
109
|
+
- env.registerPermissions(permissionList);
|
|
110
|
+
+ import { accessRules } from "./access";
|
|
111
|
+
+ env.registerAccessRules(accessRules);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### RPC Contracts
|
|
115
|
+
|
|
116
|
+
```diff
|
|
117
|
+
- .meta({ userType: "user", permissions: [permissions.read.id] })
|
|
118
|
+
+ .meta({ userType: "user", access: [access.read] })
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Frontend Hooks
|
|
122
|
+
|
|
123
|
+
```diff
|
|
124
|
+
- const canRead = accessApi.usePermission(permissions.read.id);
|
|
125
|
+
+ const canRead = accessApi.useAccess(access.read);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Routes
|
|
129
|
+
|
|
130
|
+
```diff
|
|
131
|
+
- permission: permissions.entityRead.id,
|
|
132
|
+
+ accessRule: access.read,
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Patch Changes
|
|
136
|
+
|
|
137
|
+
- Updated dependencies [9faec1f]
|
|
138
|
+
- Updated dependencies [95eeec7]
|
|
139
|
+
- Updated dependencies [f533141]
|
|
140
|
+
- @checkstack/auth-frontend@0.2.0
|
|
141
|
+
- @checkstack/common@0.2.0
|
|
142
|
+
- @checkstack/frontend-api@0.1.0
|
|
143
|
+
- @checkstack/notification-common@0.1.0
|
|
144
|
+
- @checkstack/ui@0.2.0
|
|
145
|
+
- @checkstack/signal-frontend@0.0.6
|
|
146
|
+
|
|
3
147
|
## 0.0.5
|
|
4
148
|
|
|
5
149
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import { Bell, CheckCheck } from "lucide-react";
|
|
4
4
|
import {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
Button,
|
|
12
12
|
stripMarkdown,
|
|
13
13
|
} from "@checkstack/ui";
|
|
14
|
-
import { useApi,
|
|
14
|
+
import { useApi, usePluginClient } from "@checkstack/frontend-api";
|
|
15
15
|
import { useSignal } from "@checkstack/signal-frontend";
|
|
16
16
|
import { resolveRoute } from "@checkstack/common";
|
|
17
17
|
import type { Notification } from "@checkstack/notification-common";
|
|
@@ -27,54 +27,42 @@ import { authApiRef } from "@checkstack/auth-frontend/api";
|
|
|
27
27
|
export const NotificationBell = () => {
|
|
28
28
|
const authApi = useApi(authApiRef);
|
|
29
29
|
const { data: session, isPending: isAuthLoading } = authApi.useSession();
|
|
30
|
-
const
|
|
31
|
-
const notificationClient = rpcApi.forPlugin(NotificationApi);
|
|
30
|
+
const notificationClient = usePluginClient(NotificationApi);
|
|
32
31
|
|
|
33
|
-
const [unreadCount, setUnreadCount] = useState(0);
|
|
34
|
-
const [recentNotifications, setRecentNotifications] = useState<
|
|
35
|
-
Notification[]
|
|
36
|
-
>([]);
|
|
37
32
|
const [isOpen, setIsOpen] = useState(false);
|
|
38
|
-
const [loading, setLoading] = useState(true);
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.error("Failed to fetch unread count:", error);
|
|
48
|
-
}
|
|
49
|
-
}, [notificationClient, session]);
|
|
34
|
+
// State for real-time updates
|
|
35
|
+
const [signalUnreadCount, setSignalUnreadCount] = useState<
|
|
36
|
+
number | undefined
|
|
37
|
+
>();
|
|
38
|
+
const [signalNotifications, setSignalNotifications] = useState<
|
|
39
|
+
Notification[] | undefined
|
|
40
|
+
>();
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
offset: 0,
|
|
58
|
-
unreadOnly: true, // Only show unread notifications in the dropdown
|
|
59
|
-
});
|
|
60
|
-
setRecentNotifications(notifications);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error("Failed to fetch notifications:", error);
|
|
63
|
-
}
|
|
64
|
-
}, [notificationClient, session]);
|
|
42
|
+
// Fetch unread count via useQuery
|
|
43
|
+
const { data: unreadData, isLoading: unreadLoading } =
|
|
44
|
+
notificationClient.getUnreadCount.useQuery(undefined, {
|
|
45
|
+
enabled: !!session,
|
|
46
|
+
staleTime: 30_000,
|
|
47
|
+
});
|
|
65
48
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
// Fetch recent notifications via useQuery
|
|
50
|
+
const { data: notificationsData, isLoading: notificationsLoading } =
|
|
51
|
+
notificationClient.getNotifications.useQuery(
|
|
52
|
+
{ limit: 5, offset: 0, unreadOnly: true },
|
|
53
|
+
{
|
|
54
|
+
enabled: !!session && isOpen,
|
|
55
|
+
staleTime: 30_000,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Mark all as read mutation
|
|
60
|
+
const markAsReadMutation = notificationClient.markAsRead.useMutation();
|
|
61
|
+
|
|
62
|
+
// Use signal data if available, otherwise use query data
|
|
63
|
+
const unreadCount = signalUnreadCount ?? unreadData?.count ?? 0;
|
|
64
|
+
const recentNotifications =
|
|
65
|
+
signalNotifications ?? notificationsData?.notifications ?? [];
|
|
78
66
|
|
|
79
67
|
// ==========================================================================
|
|
80
68
|
// REALTIME SIGNAL SUBSCRIPTIONS (replaces polling)
|
|
@@ -85,10 +73,10 @@ export const NotificationBell = () => {
|
|
|
85
73
|
NOTIFICATION_RECEIVED,
|
|
86
74
|
useCallback((payload) => {
|
|
87
75
|
// Increment unread count
|
|
88
|
-
|
|
76
|
+
setSignalUnreadCount((prev) => (prev ?? 0) + 1);
|
|
89
77
|
|
|
90
78
|
// Add to recent notifications if dropdown is open
|
|
91
|
-
|
|
79
|
+
setSignalNotifications((prev) => [
|
|
92
80
|
{
|
|
93
81
|
id: payload.id,
|
|
94
82
|
title: payload.title,
|
|
@@ -98,7 +86,7 @@ export const NotificationBell = () => {
|
|
|
98
86
|
isRead: false,
|
|
99
87
|
createdAt: new Date(),
|
|
100
88
|
},
|
|
101
|
-
...prev.slice(0, 4), // Keep only 5 items
|
|
89
|
+
...(prev ?? []).slice(0, 4), // Keep only 5 items
|
|
102
90
|
]);
|
|
103
91
|
}, [])
|
|
104
92
|
);
|
|
@@ -107,7 +95,7 @@ export const NotificationBell = () => {
|
|
|
107
95
|
useSignal(
|
|
108
96
|
NOTIFICATION_COUNT_CHANGED,
|
|
109
97
|
useCallback((payload) => {
|
|
110
|
-
|
|
98
|
+
setSignalUnreadCount(payload.unreadCount);
|
|
111
99
|
}, [])
|
|
112
100
|
);
|
|
113
101
|
|
|
@@ -117,32 +105,25 @@ export const NotificationBell = () => {
|
|
|
117
105
|
useCallback((payload) => {
|
|
118
106
|
if (payload.notificationId) {
|
|
119
107
|
// Single notification marked as read - remove from list
|
|
120
|
-
|
|
121
|
-
prev.filter((n) => n.id !== payload.notificationId)
|
|
108
|
+
setSignalNotifications((prev) =>
|
|
109
|
+
(prev ?? []).filter((n) => n.id !== payload.notificationId)
|
|
122
110
|
);
|
|
123
|
-
|
|
111
|
+
setSignalUnreadCount((prev) => Math.max(0, (prev ?? 1) - 1));
|
|
124
112
|
} else {
|
|
125
113
|
// All marked as read - clear the list
|
|
126
|
-
|
|
127
|
-
|
|
114
|
+
setSignalNotifications([]);
|
|
115
|
+
setSignalUnreadCount(0);
|
|
128
116
|
}
|
|
129
117
|
}, [])
|
|
130
118
|
);
|
|
131
119
|
|
|
132
120
|
// ==========================================================================
|
|
133
121
|
|
|
134
|
-
// Fetch notifications when dropdown opens
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
if (isOpen) {
|
|
137
|
-
void fetchRecentNotifications();
|
|
138
|
-
}
|
|
139
|
-
}, [isOpen, fetchRecentNotifications]);
|
|
140
|
-
|
|
141
122
|
const handleMarkAllAsRead = async () => {
|
|
142
123
|
try {
|
|
143
|
-
await
|
|
144
|
-
|
|
145
|
-
|
|
124
|
+
await markAsReadMutation.mutateAsync({});
|
|
125
|
+
setSignalUnreadCount(0);
|
|
126
|
+
setSignalNotifications([]);
|
|
146
127
|
} catch (error) {
|
|
147
128
|
console.error("Failed to mark all as read:", error);
|
|
148
129
|
}
|
|
@@ -157,6 +138,8 @@ export const NotificationBell = () => {
|
|
|
157
138
|
return;
|
|
158
139
|
}
|
|
159
140
|
|
|
141
|
+
const loading = unreadLoading || notificationsLoading;
|
|
142
|
+
|
|
160
143
|
if (loading) {
|
|
161
144
|
return (
|
|
162
145
|
<Button variant="ghost" size="icon" className="relative" disabled>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
2
|
import { Bell, Clock, Zap, Send } from "lucide-react";
|
|
3
3
|
import {
|
|
4
4
|
PageLayout,
|
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
DynamicForm,
|
|
10
10
|
} from "@checkstack/ui";
|
|
11
11
|
import {
|
|
12
|
+
usePluginClient,
|
|
12
13
|
useApi,
|
|
13
|
-
|
|
14
|
-
permissionApiRef,
|
|
14
|
+
accessApiRef,
|
|
15
15
|
} from "@checkstack/frontend-api";
|
|
16
16
|
import type { EnrichedSubscription } from "@checkstack/notification-common";
|
|
17
17
|
import {
|
|
18
18
|
NotificationApi,
|
|
19
|
-
|
|
19
|
+
notificationAccess,
|
|
20
20
|
} from "@checkstack/notification-common";
|
|
21
21
|
import {
|
|
22
22
|
StrategyCard,
|
|
@@ -28,287 +28,218 @@ import {
|
|
|
28
28
|
} from "../components/UserChannelCard";
|
|
29
29
|
|
|
30
30
|
export const NotificationSettingsPage = () => {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const notificationClient = rpcApi.forPlugin(NotificationApi);
|
|
31
|
+
const notificationClient = usePluginClient(NotificationApi);
|
|
32
|
+
const accessApi = useApi(accessApiRef);
|
|
34
33
|
const toast = useToast();
|
|
35
34
|
|
|
36
|
-
// Check if user has admin
|
|
37
|
-
const { allowed: isAdmin } =
|
|
38
|
-
permissions.notificationAdmin.id
|
|
39
|
-
);
|
|
35
|
+
// Check if user has admin access
|
|
36
|
+
const { allowed: isAdmin } = accessApi.useAccess(notificationAccess.admin);
|
|
40
37
|
|
|
41
|
-
//
|
|
42
|
-
const [retentionSchema, setRetentionSchema] = useState<
|
|
43
|
-
Record<string, unknown> | undefined
|
|
44
|
-
>();
|
|
38
|
+
// Local state for editing
|
|
45
39
|
const [retentionSettings, setRetentionSettings] = useState<
|
|
46
40
|
Record<string, unknown>
|
|
47
41
|
>({
|
|
48
42
|
retentionDays: 30,
|
|
49
43
|
enabled: false,
|
|
50
44
|
});
|
|
51
|
-
const [retentionLoading, setRetentionLoading] = useState(true);
|
|
52
|
-
const [retentionSaving, setRetentionSaving] = useState(false);
|
|
53
45
|
const [retentionValid, setRetentionValid] = useState(true);
|
|
54
|
-
|
|
55
|
-
// Subscription state - now uses enriched subscriptions only
|
|
56
|
-
const [subscriptions, setSubscriptions] = useState<EnrichedSubscription[]>(
|
|
57
|
-
[]
|
|
58
|
-
);
|
|
59
|
-
const [subsLoading, setSubsLoading] = useState(true);
|
|
60
|
-
|
|
61
|
-
// Delivery strategies state (admin only)
|
|
62
|
-
const [strategies, setStrategies] = useState<DeliveryStrategy[]>([]);
|
|
63
|
-
const [strategiesLoading, setStrategiesLoading] = useState(true);
|
|
64
|
-
const [strategySaving, setStrategySaving] = useState<string | undefined>();
|
|
65
|
-
|
|
66
|
-
// User channels state
|
|
67
|
-
const [userChannels, setUserChannels] = useState<UserDeliveryChannel[]>([]);
|
|
68
|
-
const [channelsLoading, setChannelsLoading] = useState(true);
|
|
69
46
|
const [channelSaving, setChannelSaving] = useState<string | undefined>();
|
|
70
47
|
const [channelConnecting, setChannelConnecting] = useState<
|
|
71
48
|
string | undefined
|
|
72
49
|
>();
|
|
73
50
|
const [channelTesting, setChannelTesting] = useState<string | undefined>();
|
|
51
|
+
const [strategySaving, setStrategySaving] = useState<string | undefined>();
|
|
74
52
|
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
setRetentionLoading(false);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
const [schema, settings] = await Promise.all([
|
|
83
|
-
notificationClient.getRetentionSchema(),
|
|
84
|
-
notificationClient.getRetentionSettings(),
|
|
85
|
-
]);
|
|
86
|
-
setRetentionSchema(schema as Record<string, unknown>);
|
|
87
|
-
setRetentionSettings(settings);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
const message =
|
|
90
|
-
error instanceof Error
|
|
91
|
-
? error.message
|
|
92
|
-
: "Failed to load retention settings";
|
|
93
|
-
toast.error(message);
|
|
94
|
-
} finally {
|
|
95
|
-
setRetentionLoading(false);
|
|
96
|
-
}
|
|
97
|
-
}, [notificationClient, isAdmin, toast]);
|
|
53
|
+
// Query: Retention schema (admin only)
|
|
54
|
+
const { data: retentionSchema } =
|
|
55
|
+
notificationClient.getRetentionSchema.useQuery({}, { enabled: isAdmin });
|
|
98
56
|
|
|
99
|
-
//
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const subsData = await notificationClient.getSubscriptions();
|
|
103
|
-
setSubscriptions(subsData);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
const message =
|
|
106
|
-
error instanceof Error
|
|
107
|
-
? error.message
|
|
108
|
-
: "Failed to fetch subscriptions";
|
|
109
|
-
toast.error(message);
|
|
110
|
-
} finally {
|
|
111
|
-
setSubsLoading(false);
|
|
112
|
-
}
|
|
113
|
-
}, [notificationClient, toast]);
|
|
57
|
+
// Query: Retention settings (admin only)
|
|
58
|
+
const { data: fetchedRetentionSettings, isLoading: retentionLoading } =
|
|
59
|
+
notificationClient.getRetentionSettings.useQuery({}, { enabled: isAdmin });
|
|
114
60
|
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const data = await notificationClient.getDeliveryStrategies();
|
|
123
|
-
setStrategies(data as DeliveryStrategy[]);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
const message =
|
|
126
|
-
error instanceof Error
|
|
127
|
-
? error.message
|
|
128
|
-
: "Failed to load delivery channels";
|
|
129
|
-
toast.error(message);
|
|
130
|
-
} finally {
|
|
131
|
-
setStrategiesLoading(false);
|
|
132
|
-
}
|
|
133
|
-
}, [notificationClient, isAdmin, toast]);
|
|
61
|
+
// Query: Subscriptions
|
|
62
|
+
const {
|
|
63
|
+
data: subscriptions = [],
|
|
64
|
+
isLoading: subsLoading,
|
|
65
|
+
refetch: refetchSubscriptions,
|
|
66
|
+
} = notificationClient.getSubscriptions.useQuery({});
|
|
134
67
|
|
|
135
|
-
//
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
68
|
+
// Query: Delivery strategies (admin only)
|
|
69
|
+
const {
|
|
70
|
+
data: strategies = [],
|
|
71
|
+
isLoading: strategiesLoading,
|
|
72
|
+
refetch: refetchStrategies,
|
|
73
|
+
} = notificationClient.getDeliveryStrategies.useQuery(
|
|
74
|
+
{},
|
|
75
|
+
{ enabled: isAdmin }
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Query: User delivery channels
|
|
79
|
+
const {
|
|
80
|
+
data: userChannels = [],
|
|
81
|
+
isLoading: channelsLoading,
|
|
82
|
+
refetch: refetchChannels,
|
|
83
|
+
} = notificationClient.getUserDeliveryChannels.useQuery({});
|
|
148
84
|
|
|
85
|
+
// Sync fetched retention settings to local state
|
|
149
86
|
useEffect(() => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
87
|
+
if (fetchedRetentionSettings) {
|
|
88
|
+
setRetentionSettings(fetchedRetentionSettings);
|
|
89
|
+
}
|
|
90
|
+
}, [fetchedRetentionSettings]);
|
|
91
|
+
|
|
92
|
+
// Mutations
|
|
93
|
+
const setRetentionMutation =
|
|
94
|
+
notificationClient.setRetentionSettings.useMutation({
|
|
95
|
+
onSuccess: () => {
|
|
96
|
+
toast.success("Retention settings saved");
|
|
97
|
+
},
|
|
98
|
+
onError: (error) => {
|
|
99
|
+
toast.error(
|
|
100
|
+
error instanceof Error ? error.message : "Failed to save settings"
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
160
104
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
105
|
+
const unsubscribeMutation = notificationClient.unsubscribe.useMutation({
|
|
106
|
+
onSuccess: () => {
|
|
107
|
+
toast.success("Unsubscribed successfully");
|
|
108
|
+
void refetchSubscriptions();
|
|
109
|
+
},
|
|
110
|
+
onError: (error) => {
|
|
111
|
+
toast.error(
|
|
112
|
+
error instanceof Error ? error.message : "Failed to unsubscribe"
|
|
166
113
|
);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const updateStrategyMutation =
|
|
118
|
+
notificationClient.updateDeliveryStrategy.useMutation({
|
|
119
|
+
onSuccess: () => {
|
|
120
|
+
toast.success("Updated delivery channel");
|
|
121
|
+
void refetchStrategies();
|
|
122
|
+
setStrategySaving(undefined);
|
|
123
|
+
},
|
|
124
|
+
onError: (error) => {
|
|
125
|
+
toast.error(
|
|
126
|
+
error instanceof Error ? error.message : "Failed to update channel"
|
|
127
|
+
);
|
|
128
|
+
setStrategySaving(undefined);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const setUserPreferenceMutation =
|
|
133
|
+
notificationClient.setUserDeliveryPreference.useMutation({
|
|
134
|
+
onSuccess: () => {
|
|
135
|
+
toast.success("Updated notification channel");
|
|
136
|
+
void refetchChannels();
|
|
137
|
+
setChannelSaving(undefined);
|
|
138
|
+
},
|
|
139
|
+
onError: (error) => {
|
|
140
|
+
toast.error(
|
|
141
|
+
error instanceof Error ? error.message : "Failed to update preference"
|
|
142
|
+
);
|
|
143
|
+
setChannelSaving(undefined);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const unlinkChannelMutation =
|
|
148
|
+
notificationClient.unlinkDeliveryChannel.useMutation({
|
|
149
|
+
onSuccess: () => {
|
|
150
|
+
toast.success("Disconnected notification channel");
|
|
151
|
+
void refetchChannels();
|
|
152
|
+
setChannelSaving(undefined);
|
|
153
|
+
},
|
|
154
|
+
onError: (error) => {
|
|
155
|
+
toast.error(
|
|
156
|
+
error instanceof Error ? error.message : "Failed to disconnect"
|
|
157
|
+
);
|
|
158
|
+
setChannelSaving(undefined);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const getOAuthUrlMutation =
|
|
163
|
+
notificationClient.getDeliveryOAuthUrl.useMutation({
|
|
164
|
+
onSuccess: (data) => {
|
|
165
|
+
globalThis.location.href = data.authUrl;
|
|
166
|
+
},
|
|
167
|
+
onError: (error) => {
|
|
168
|
+
toast.error(
|
|
169
|
+
error instanceof Error ? error.message : "Failed to start OAuth flow"
|
|
170
|
+
);
|
|
171
|
+
setChannelConnecting(undefined);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const sendTestMutation = notificationClient.sendTestNotification.useMutation({
|
|
176
|
+
onSettled: () => {
|
|
177
|
+
setChannelTesting(undefined);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const handleSaveRetention = () => {
|
|
182
|
+
setRetentionMutation.mutate(
|
|
183
|
+
retentionSettings as { enabled: boolean; retentionDays: number }
|
|
184
|
+
);
|
|
175
185
|
};
|
|
176
186
|
|
|
177
|
-
const handleUnsubscribe =
|
|
178
|
-
|
|
179
|
-
await notificationClient.unsubscribe({ groupId });
|
|
180
|
-
setSubscriptions((prev) => prev.filter((s) => s.groupId !== groupId));
|
|
181
|
-
toast.success("Unsubscribed successfully");
|
|
182
|
-
} catch (error) {
|
|
183
|
-
const message =
|
|
184
|
-
error instanceof Error ? error.message : "Failed to unsubscribe";
|
|
185
|
-
toast.error(message);
|
|
186
|
-
}
|
|
187
|
+
const handleUnsubscribe = (groupId: string) => {
|
|
188
|
+
unsubscribeMutation.mutate({ groupId });
|
|
187
189
|
};
|
|
188
190
|
|
|
189
|
-
// Handle strategy update (enabled state and config)
|
|
190
191
|
const handleStrategyUpdate = async (
|
|
191
192
|
strategyId: string,
|
|
192
193
|
enabled: boolean,
|
|
193
194
|
config?: Record<string, unknown>,
|
|
194
195
|
layoutConfig?: Record<string, unknown>
|
|
195
196
|
) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
204
|
-
// Update local state
|
|
205
|
-
setStrategies((prev) =>
|
|
206
|
-
prev.map((s) =>
|
|
207
|
-
s.qualifiedId === strategyId
|
|
208
|
-
? { ...s, enabled, config, layoutConfig }
|
|
209
|
-
: s
|
|
210
|
-
)
|
|
211
|
-
);
|
|
212
|
-
toast.success(`${enabled ? "Enabled" : "Disabled"} delivery channel`);
|
|
213
|
-
} catch (error) {
|
|
214
|
-
const message =
|
|
215
|
-
error instanceof Error ? error.message : "Failed to update channel";
|
|
216
|
-
toast.error(message);
|
|
217
|
-
} finally {
|
|
218
|
-
setStrategySaving(undefined);
|
|
219
|
-
}
|
|
197
|
+
setStrategySaving(strategyId);
|
|
198
|
+
await updateStrategyMutation.mutateAsync({
|
|
199
|
+
strategyId,
|
|
200
|
+
enabled,
|
|
201
|
+
config,
|
|
202
|
+
layoutConfig,
|
|
203
|
+
});
|
|
220
204
|
};
|
|
221
205
|
|
|
222
|
-
// Handle user channel toggle
|
|
223
206
|
const handleChannelToggle = async (strategyId: string, enabled: boolean) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
230
|
-
setUserChannels((prev) =>
|
|
231
|
-
prev.map((c) => (c.strategyId === strategyId ? { ...c, enabled } : c))
|
|
232
|
-
);
|
|
233
|
-
toast.success(`${enabled ? "Enabled" : "Disabled"} notification channel`);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
const message =
|
|
236
|
-
error instanceof Error ? error.message : "Failed to update preference";
|
|
237
|
-
toast.error(message);
|
|
238
|
-
} finally {
|
|
239
|
-
setChannelSaving(undefined);
|
|
240
|
-
}
|
|
207
|
+
setChannelSaving(strategyId);
|
|
208
|
+
await setUserPreferenceMutation.mutateAsync({
|
|
209
|
+
strategyId,
|
|
210
|
+
enabled,
|
|
211
|
+
});
|
|
241
212
|
};
|
|
242
213
|
|
|
243
|
-
// Handle OAuth connect
|
|
244
214
|
const handleChannelConnect = async (strategyId: string) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
251
|
-
// Redirect to OAuth provider
|
|
252
|
-
globalThis.location.href = authUrl;
|
|
253
|
-
} catch (error) {
|
|
254
|
-
const message =
|
|
255
|
-
error instanceof Error ? error.message : "Failed to start OAuth flow";
|
|
256
|
-
toast.error(message);
|
|
257
|
-
setChannelConnecting(undefined);
|
|
258
|
-
}
|
|
215
|
+
setChannelConnecting(strategyId);
|
|
216
|
+
await getOAuthUrlMutation.mutateAsync({
|
|
217
|
+
strategyId,
|
|
218
|
+
returnUrl: globalThis.location.pathname,
|
|
219
|
+
});
|
|
259
220
|
};
|
|
260
221
|
|
|
261
|
-
// Handle OAuth disconnect
|
|
262
222
|
const handleChannelDisconnect = async (strategyId: string) => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await notificationClient.unlinkDeliveryChannel({ strategyId });
|
|
266
|
-
setUserChannels((prev) =>
|
|
267
|
-
prev.map((c) =>
|
|
268
|
-
c.strategyId === strategyId
|
|
269
|
-
? { ...c, linkedAt: undefined, enabled: false, isConfigured: false }
|
|
270
|
-
: c
|
|
271
|
-
)
|
|
272
|
-
);
|
|
273
|
-
toast.success("Disconnected notification channel");
|
|
274
|
-
} catch (error) {
|
|
275
|
-
const message =
|
|
276
|
-
error instanceof Error ? error.message : "Failed to disconnect";
|
|
277
|
-
toast.error(message);
|
|
278
|
-
} finally {
|
|
279
|
-
setChannelSaving(undefined);
|
|
280
|
-
}
|
|
223
|
+
setChannelSaving(strategyId);
|
|
224
|
+
await unlinkChannelMutation.mutateAsync({ strategyId });
|
|
281
225
|
};
|
|
282
226
|
|
|
283
|
-
// Handle user config save
|
|
284
227
|
const handleChannelConfigSave = async (
|
|
285
228
|
strategyId: string,
|
|
286
229
|
userConfig: Record<string, unknown>
|
|
287
230
|
) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
? { ...c, userConfig, isConfigured: true }
|
|
301
|
-
: c
|
|
302
|
-
)
|
|
303
|
-
);
|
|
304
|
-
toast.success("Saved channel settings");
|
|
305
|
-
} catch (error) {
|
|
306
|
-
const message =
|
|
307
|
-
error instanceof Error ? error.message : "Failed to save settings";
|
|
308
|
-
toast.error(message);
|
|
309
|
-
} finally {
|
|
310
|
-
setChannelSaving(undefined);
|
|
311
|
-
}
|
|
231
|
+
setChannelSaving(strategyId);
|
|
232
|
+
const channel = userChannels.find((c) => c.strategyId === strategyId);
|
|
233
|
+
await setUserPreferenceMutation.mutateAsync({
|
|
234
|
+
strategyId,
|
|
235
|
+
enabled: channel?.enabled ?? false,
|
|
236
|
+
userConfig,
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const handleTest = async (strategyId: string) => {
|
|
241
|
+
setChannelTesting(strategyId);
|
|
242
|
+
return sendTestMutation.mutateAsync({ strategyId });
|
|
312
243
|
};
|
|
313
244
|
|
|
314
245
|
return (
|
|
@@ -327,7 +258,7 @@ export const NotificationSettingsPage = () => {
|
|
|
327
258
|
Loading your channels...
|
|
328
259
|
</div>
|
|
329
260
|
</Card>
|
|
330
|
-
) : userChannels.length === 0 ? (
|
|
261
|
+
) : (userChannels as UserDeliveryChannel[]).length === 0 ? (
|
|
331
262
|
<Card className="p-4">
|
|
332
263
|
<div className="text-center py-4 text-muted-foreground">
|
|
333
264
|
No notification channels available. Contact your administrator
|
|
@@ -336,7 +267,7 @@ export const NotificationSettingsPage = () => {
|
|
|
336
267
|
</Card>
|
|
337
268
|
) : (
|
|
338
269
|
<div className="space-y-3">
|
|
339
|
-
{userChannels.map((channel) => (
|
|
270
|
+
{(userChannels as UserDeliveryChannel[]).map((channel) => (
|
|
340
271
|
<UserChannelCard
|
|
341
272
|
key={channel.strategyId}
|
|
342
273
|
channel={channel}
|
|
@@ -344,21 +275,7 @@ export const NotificationSettingsPage = () => {
|
|
|
344
275
|
onConnect={handleChannelConnect}
|
|
345
276
|
onDisconnect={handleChannelDisconnect}
|
|
346
277
|
onSaveConfig={handleChannelConfigSave}
|
|
347
|
-
onTest={
|
|
348
|
-
setChannelTesting(strategyId);
|
|
349
|
-
try {
|
|
350
|
-
const result =
|
|
351
|
-
await notificationClient.sendTestNotification({
|
|
352
|
-
strategyId,
|
|
353
|
-
});
|
|
354
|
-
if (!result.success) {
|
|
355
|
-
alert(`Test failed: ${result.error}`);
|
|
356
|
-
}
|
|
357
|
-
return result;
|
|
358
|
-
} finally {
|
|
359
|
-
setChannelTesting(undefined);
|
|
360
|
-
}
|
|
361
|
-
}}
|
|
278
|
+
onTest={handleTest}
|
|
362
279
|
saving={channelSaving === channel.strategyId}
|
|
363
280
|
connecting={channelConnecting === channel.strategyId}
|
|
364
281
|
testing={channelTesting === channel.strategyId}
|
|
@@ -376,13 +293,13 @@ export const NotificationSettingsPage = () => {
|
|
|
376
293
|
icon={<Bell className="h-5 w-5" />}
|
|
377
294
|
/>
|
|
378
295
|
<Card className="p-4">
|
|
379
|
-
{subscriptions.length === 0 ? (
|
|
296
|
+
{(subscriptions as EnrichedSubscription[]).length === 0 ? (
|
|
380
297
|
<div className="text-center py-4 text-muted-foreground">
|
|
381
298
|
No active subscriptions
|
|
382
299
|
</div>
|
|
383
300
|
) : (
|
|
384
301
|
<div className="space-y-3">
|
|
385
|
-
{subscriptions.map((sub) => (
|
|
302
|
+
{(subscriptions as EnrichedSubscription[]).map((sub) => (
|
|
386
303
|
<div
|
|
387
304
|
key={sub.groupId}
|
|
388
305
|
className="flex items-center justify-between py-2 border-b last:border-0"
|
|
@@ -399,7 +316,8 @@ export const NotificationSettingsPage = () => {
|
|
|
399
316
|
<Button
|
|
400
317
|
variant="outline"
|
|
401
318
|
size="sm"
|
|
402
|
-
onClick={() =>
|
|
319
|
+
onClick={() => handleUnsubscribe(sub.groupId)}
|
|
320
|
+
disabled={unsubscribeMutation.isPending}
|
|
403
321
|
>
|
|
404
322
|
Unsubscribe
|
|
405
323
|
</Button>
|
|
@@ -439,7 +357,7 @@ export const NotificationSettingsPage = () => {
|
|
|
439
357
|
Loading delivery channels...
|
|
440
358
|
</div>
|
|
441
359
|
</Card>
|
|
442
|
-
) : strategies.length === 0 ? (
|
|
360
|
+
) : (strategies as DeliveryStrategy[]).length === 0 ? (
|
|
443
361
|
<Card className="p-4">
|
|
444
362
|
<div className="text-center py-4 text-muted-foreground">
|
|
445
363
|
No delivery channels registered. Plugins can register delivery
|
|
@@ -448,7 +366,7 @@ export const NotificationSettingsPage = () => {
|
|
|
448
366
|
</Card>
|
|
449
367
|
) : (
|
|
450
368
|
<div className="space-y-3">
|
|
451
|
-
{strategies.map((strategy) => (
|
|
369
|
+
{(strategies as DeliveryStrategy[]).map((strategy) => (
|
|
452
370
|
<StrategyCard
|
|
453
371
|
key={strategy.qualifiedId}
|
|
454
372
|
strategy={strategy}
|
|
@@ -477,18 +395,18 @@ export const NotificationSettingsPage = () => {
|
|
|
477
395
|
) : (
|
|
478
396
|
<div className="space-y-4">
|
|
479
397
|
<DynamicForm
|
|
480
|
-
schema={retentionSchema}
|
|
398
|
+
schema={retentionSchema as Record<string, unknown>}
|
|
481
399
|
value={retentionSettings}
|
|
482
400
|
onChange={setRetentionSettings}
|
|
483
401
|
onValidChange={setRetentionValid}
|
|
484
402
|
/>
|
|
485
403
|
<Button
|
|
486
|
-
onClick={
|
|
487
|
-
|
|
488
|
-
}}
|
|
489
|
-
disabled={retentionSaving || !retentionValid}
|
|
404
|
+
onClick={handleSaveRetention}
|
|
405
|
+
disabled={setRetentionMutation.isPending || !retentionValid}
|
|
490
406
|
>
|
|
491
|
-
{
|
|
407
|
+
{setRetentionMutation.isPending
|
|
408
|
+
? "Saving..."
|
|
409
|
+
: "Save Settings"}
|
|
492
410
|
</Button>
|
|
493
411
|
</div>
|
|
494
412
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState
|
|
1
|
+
import { useState } from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import { Bell, Check, Trash2, ChevronDown } from "lucide-react";
|
|
4
4
|
import {
|
|
@@ -13,88 +13,82 @@ import {
|
|
|
13
13
|
DropdownMenuTrigger,
|
|
14
14
|
Markdown,
|
|
15
15
|
} from "@checkstack/ui";
|
|
16
|
-
import {
|
|
16
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
17
17
|
import type { Notification } from "@checkstack/notification-common";
|
|
18
18
|
import { NotificationApi } from "@checkstack/notification-common";
|
|
19
19
|
|
|
20
20
|
export const NotificationsPage = () => {
|
|
21
|
-
const
|
|
22
|
-
const notificationClient = rpcApi.forPlugin(NotificationApi);
|
|
21
|
+
const notificationClient = usePluginClient(NotificationApi);
|
|
23
22
|
const toast = useToast();
|
|
24
23
|
|
|
25
|
-
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
26
|
-
const [total, setTotal] = useState(0);
|
|
27
|
-
const [loading, setLoading] = useState(true);
|
|
28
24
|
const [filter, setFilter] = useState<"all" | "unread">("all");
|
|
29
25
|
const [page, setPage] = useState(0);
|
|
30
26
|
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
|
|
31
27
|
const pageSize = 20;
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
setTotal(totalCount);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
const message =
|
|
46
|
-
error instanceof Error
|
|
47
|
-
? error.message
|
|
48
|
-
: "Failed to fetch notifications";
|
|
49
|
-
toast.error(message);
|
|
50
|
-
} finally {
|
|
51
|
-
setLoading(false);
|
|
52
|
-
}
|
|
53
|
-
}, [notificationClient, page, filter, toast]);
|
|
29
|
+
// Query: Fetch notifications
|
|
30
|
+
const {
|
|
31
|
+
data: notificationsData,
|
|
32
|
+
isLoading: loading,
|
|
33
|
+
refetch,
|
|
34
|
+
} = notificationClient.getNotifications.useQuery({
|
|
35
|
+
limit: pageSize,
|
|
36
|
+
offset: page * pageSize,
|
|
37
|
+
unreadOnly: filter === "unread",
|
|
38
|
+
});
|
|
54
39
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}, [fetchNotifications]);
|
|
40
|
+
const notifications = notificationsData?.notifications ?? [];
|
|
41
|
+
const total = notificationsData?.total ?? 0;
|
|
58
42
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
prev.map((n) => (n.id === notificationId ? { ...n, isRead: true } : n))
|
|
64
|
-
);
|
|
43
|
+
// Mutation: Mark as read
|
|
44
|
+
const markAsReadMutation = notificationClient.markAsRead.useMutation({
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
void refetch();
|
|
65
47
|
toast.success("Notification marked as read");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
48
|
+
},
|
|
49
|
+
onError: (error) => {
|
|
50
|
+
toast.error(
|
|
51
|
+
error instanceof Error ? error.message : "Failed to mark as read"
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
setTotal((prev) => prev - 1);
|
|
56
|
+
// Mutation: Delete notification
|
|
57
|
+
const deleteMutation = notificationClient.deleteNotification.useMutation({
|
|
58
|
+
onSuccess: () => {
|
|
59
|
+
void refetch();
|
|
78
60
|
toast.success("Notification deleted");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
61
|
+
},
|
|
62
|
+
onError: (error) => {
|
|
63
|
+
toast.error(
|
|
64
|
+
error instanceof Error ? error.message : "Failed to delete notification"
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
87
68
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
// Mutation: Mark all as read
|
|
70
|
+
const markAllAsReadMutation = notificationClient.markAsRead.useMutation({
|
|
71
|
+
onSuccess: () => {
|
|
72
|
+
void refetch();
|
|
92
73
|
toast.success("All notifications marked as read");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
74
|
+
},
|
|
75
|
+
onError: (error) => {
|
|
76
|
+
toast.error(
|
|
77
|
+
error instanceof Error ? error.message : "Failed to mark all as read"
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const handleMarkAsRead = (notificationId: string) => {
|
|
83
|
+
markAsReadMutation.mutate({ notificationId });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleDelete = (notificationId: string) => {
|
|
87
|
+
deleteMutation.mutate({ notificationId });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleMarkAllAsRead = () => {
|
|
91
|
+
markAllAsReadMutation.mutate({});
|
|
98
92
|
};
|
|
99
93
|
|
|
100
94
|
const getImportanceBadge = (importance: Notification["importance"]) => {
|
|
@@ -182,9 +176,8 @@ export const NotificationsPage = () => {
|
|
|
182
176
|
<Button
|
|
183
177
|
variant="outline"
|
|
184
178
|
size="sm"
|
|
185
|
-
onClick={
|
|
186
|
-
|
|
187
|
-
}}
|
|
179
|
+
onClick={handleMarkAllAsRead}
|
|
180
|
+
disabled={markAllAsReadMutation.isPending}
|
|
188
181
|
>
|
|
189
182
|
<Check className="h-4 w-4 mr-1" /> Mark all read
|
|
190
183
|
</Button>
|
|
@@ -241,9 +234,8 @@ export const NotificationsPage = () => {
|
|
|
241
234
|
<Button
|
|
242
235
|
variant="ghost"
|
|
243
236
|
size="icon"
|
|
244
|
-
onClick={() =>
|
|
245
|
-
|
|
246
|
-
}}
|
|
237
|
+
onClick={() => handleMarkAsRead(notification.id)}
|
|
238
|
+
disabled={markAsReadMutation.isPending}
|
|
247
239
|
title="Mark as read"
|
|
248
240
|
>
|
|
249
241
|
<Check className="h-4 w-4" />
|
|
@@ -252,9 +244,8 @@ export const NotificationsPage = () => {
|
|
|
252
244
|
<Button
|
|
253
245
|
variant="ghost"
|
|
254
246
|
size="icon"
|
|
255
|
-
onClick={() =>
|
|
256
|
-
|
|
257
|
-
}}
|
|
247
|
+
onClick={() => handleDelete(notification.id)}
|
|
248
|
+
disabled={deleteMutation.isPending}
|
|
258
249
|
title="Delete"
|
|
259
250
|
>
|
|
260
251
|
<Trash2 className="h-4 w-4 text-destructive" />
|