@checkmate-monitor/notification-frontend 0.1.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 +87 -0
- package/package.json +31 -0
- package/src/components/NotificationBell.tsx +283 -0
- package/src/components/StrategyCard.tsx +159 -0
- package/src/components/UserChannelCard.tsx +310 -0
- package/src/components/UserMenuItems.tsx +15 -0
- package/src/index.tsx +39 -0
- package/src/pages/NotificationSettingsPage.tsx +501 -0
- package/src/pages/NotificationsPage.tsx +300 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @checkmate-monitor/notification-frontend
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b55fae6: Added realtime Signal Service for backend-to-frontend push notifications via WebSockets.
|
|
8
|
+
|
|
9
|
+
## New Packages
|
|
10
|
+
|
|
11
|
+
- **@checkmate-monitor/signal-common**: Shared types including `Signal`, `SignalService`, `createSignal()`, and WebSocket protocol messages
|
|
12
|
+
- **@checkmate-monitor/signal-backend**: `SignalServiceImpl` with EventBus integration and Bun WebSocket handler using native pub/sub
|
|
13
|
+
- **@checkmate-monitor/signal-frontend**: React `SignalProvider` and `useSignal()` hook for consuming typed signals
|
|
14
|
+
|
|
15
|
+
## Changes
|
|
16
|
+
|
|
17
|
+
- **@checkmate-monitor/backend-api**: Added `coreServices.signalService` reference for plugins to emit signals
|
|
18
|
+
- **@checkmate-monitor/backend**: Integrated WebSocket server at `/api/signals/ws` with session-based authentication
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Backend plugins can emit signals:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { coreServices } from "@checkmate-monitor/backend-api";
|
|
26
|
+
import { NOTIFICATION_RECEIVED } from "@checkmate-monitor/notification-common";
|
|
27
|
+
|
|
28
|
+
const signalService = context.signalService;
|
|
29
|
+
await signalService.sendToUser(NOTIFICATION_RECEIVED, userId, { ... });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Frontend components subscribe to signals:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { useSignal } from "@checkmate-monitor/signal-frontend";
|
|
36
|
+
import { NOTIFICATION_RECEIVED } from "@checkmate-monitor/notification-common";
|
|
37
|
+
|
|
38
|
+
useSignal(NOTIFICATION_RECEIVED, (payload) => {
|
|
39
|
+
// Handle realtime notification
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
|
|
44
|
+
|
|
45
|
+
## Strategy Instructions Interface
|
|
46
|
+
|
|
47
|
+
Added `adminInstructions` and `userInstructions` optional fields to the `NotificationStrategy` interface. These allow strategies to export markdown-formatted setup guides that are displayed in the configuration UI:
|
|
48
|
+
|
|
49
|
+
- **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
|
|
50
|
+
- **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
|
|
51
|
+
|
|
52
|
+
### Updated Components
|
|
53
|
+
|
|
54
|
+
- `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
|
|
55
|
+
- `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
|
|
56
|
+
- `UserChannelCard` renders `userInstructions` when users need to connect
|
|
57
|
+
|
|
58
|
+
## New Telegram Notification Plugin
|
|
59
|
+
|
|
60
|
+
Added `@checkmate-monitor/notification-telegram-backend` plugin for sending notifications via Telegram:
|
|
61
|
+
|
|
62
|
+
- Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
|
|
63
|
+
- Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
|
|
64
|
+
- Includes comprehensive admin instructions for bot setup via @BotFather
|
|
65
|
+
- Includes user instructions for account linking
|
|
66
|
+
|
|
67
|
+
### Configuration
|
|
68
|
+
|
|
69
|
+
Admins need to configure a Telegram Bot Token obtained from @BotFather.
|
|
70
|
+
|
|
71
|
+
### User Linking
|
|
72
|
+
|
|
73
|
+
The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
|
|
74
|
+
|
|
75
|
+
### Patch Changes
|
|
76
|
+
|
|
77
|
+
- Updated dependencies [eff5b4e]
|
|
78
|
+
- Updated dependencies [ffc28f6]
|
|
79
|
+
- Updated dependencies [32f2535]
|
|
80
|
+
- Updated dependencies [b55fae6]
|
|
81
|
+
- Updated dependencies [b354ab3]
|
|
82
|
+
- @checkmate-monitor/ui@0.1.0
|
|
83
|
+
- @checkmate-monitor/common@0.1.0
|
|
84
|
+
- @checkmate-monitor/notification-common@0.1.0
|
|
85
|
+
- @checkmate-monitor/auth-frontend@0.1.0
|
|
86
|
+
- @checkmate-monitor/signal-frontend@0.1.0
|
|
87
|
+
- @checkmate-monitor/frontend-api@0.0.2
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkmate-monitor/notification-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"checkmate": {
|
|
7
|
+
"type": "frontend"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "bun run lint:code",
|
|
12
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@checkmate-monitor/notification-common": "workspace:*",
|
|
16
|
+
"@checkmate-monitor/frontend-api": "workspace:*",
|
|
17
|
+
"@checkmate-monitor/auth-frontend": "workspace:*",
|
|
18
|
+
"@checkmate-monitor/signal-frontend": "workspace:*",
|
|
19
|
+
"@checkmate-monitor/common": "workspace:*",
|
|
20
|
+
"@checkmate-monitor/ui": "workspace:*",
|
|
21
|
+
"react": "^18.2.0",
|
|
22
|
+
"react-router-dom": "^6.22.0",
|
|
23
|
+
"lucide-react": "^0.344.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"@types/react": "^18.2.0",
|
|
28
|
+
"@checkmate-monitor/tsconfig": "workspace:*",
|
|
29
|
+
"@checkmate-monitor/scripts": "workspace:*"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { Bell, CheckCheck } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Badge,
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
Button,
|
|
12
|
+
stripMarkdown,
|
|
13
|
+
} from "@checkmate-monitor/ui";
|
|
14
|
+
import { useApi, rpcApiRef } from "@checkmate-monitor/frontend-api";
|
|
15
|
+
import { useSignal } from "@checkmate-monitor/signal-frontend";
|
|
16
|
+
import { resolveRoute } from "@checkmate-monitor/common";
|
|
17
|
+
import type { Notification } from "@checkmate-monitor/notification-common";
|
|
18
|
+
import {
|
|
19
|
+
NotificationApi,
|
|
20
|
+
NOTIFICATION_RECEIVED,
|
|
21
|
+
NOTIFICATION_COUNT_CHANGED,
|
|
22
|
+
NOTIFICATION_READ,
|
|
23
|
+
notificationRoutes,
|
|
24
|
+
} from "@checkmate-monitor/notification-common";
|
|
25
|
+
import { authApiRef } from "@checkmate-monitor/auth-frontend/api";
|
|
26
|
+
|
|
27
|
+
export const NotificationBell = () => {
|
|
28
|
+
const authApi = useApi(authApiRef);
|
|
29
|
+
const { data: session, isPending: isAuthLoading } = authApi.useSession();
|
|
30
|
+
const rpcApi = useApi(rpcApiRef);
|
|
31
|
+
const notificationClient = rpcApi.forPlugin(NotificationApi);
|
|
32
|
+
|
|
33
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
34
|
+
const [recentNotifications, setRecentNotifications] = useState<
|
|
35
|
+
Notification[]
|
|
36
|
+
>([]);
|
|
37
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
38
|
+
const [loading, setLoading] = useState(true);
|
|
39
|
+
|
|
40
|
+
const fetchUnreadCount = useCallback(async () => {
|
|
41
|
+
// Skip fetch if not authenticated
|
|
42
|
+
if (!session) return;
|
|
43
|
+
try {
|
|
44
|
+
const { count } = await notificationClient.getUnreadCount();
|
|
45
|
+
setUnreadCount(count);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("Failed to fetch unread count:", error);
|
|
48
|
+
}
|
|
49
|
+
}, [notificationClient, session]);
|
|
50
|
+
|
|
51
|
+
const fetchRecentNotifications = useCallback(async () => {
|
|
52
|
+
// Skip fetch if not authenticated
|
|
53
|
+
if (!session) return;
|
|
54
|
+
try {
|
|
55
|
+
const { notifications } = await notificationClient.getNotifications({
|
|
56
|
+
limit: 5,
|
|
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]);
|
|
65
|
+
|
|
66
|
+
// Initial fetch
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!session) {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const init = async () => {
|
|
73
|
+
await Promise.all([fetchUnreadCount(), fetchRecentNotifications()]);
|
|
74
|
+
setLoading(false);
|
|
75
|
+
};
|
|
76
|
+
void init();
|
|
77
|
+
}, [fetchUnreadCount, fetchRecentNotifications, session]);
|
|
78
|
+
|
|
79
|
+
// ==========================================================================
|
|
80
|
+
// REALTIME SIGNAL SUBSCRIPTIONS (replaces polling)
|
|
81
|
+
// ==========================================================================
|
|
82
|
+
|
|
83
|
+
// Handle new notification received
|
|
84
|
+
useSignal(
|
|
85
|
+
NOTIFICATION_RECEIVED,
|
|
86
|
+
useCallback((payload) => {
|
|
87
|
+
// Increment unread count
|
|
88
|
+
setUnreadCount((prev) => prev + 1);
|
|
89
|
+
|
|
90
|
+
// Add to recent notifications if dropdown is open
|
|
91
|
+
setRecentNotifications((prev) => [
|
|
92
|
+
{
|
|
93
|
+
id: payload.id,
|
|
94
|
+
title: payload.title,
|
|
95
|
+
body: payload.body,
|
|
96
|
+
importance: payload.importance,
|
|
97
|
+
userId: "", // Not needed for display
|
|
98
|
+
isRead: false,
|
|
99
|
+
createdAt: new Date(),
|
|
100
|
+
},
|
|
101
|
+
...prev.slice(0, 4), // Keep only 5 items
|
|
102
|
+
]);
|
|
103
|
+
}, [])
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Handle count changes from other sources
|
|
107
|
+
useSignal(
|
|
108
|
+
NOTIFICATION_COUNT_CHANGED,
|
|
109
|
+
useCallback((payload) => {
|
|
110
|
+
setUnreadCount(payload.unreadCount);
|
|
111
|
+
}, [])
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Handle notification marked as read
|
|
115
|
+
useSignal(
|
|
116
|
+
NOTIFICATION_READ,
|
|
117
|
+
useCallback((payload) => {
|
|
118
|
+
if (payload.notificationId) {
|
|
119
|
+
// Single notification marked as read - remove from list
|
|
120
|
+
setRecentNotifications((prev) =>
|
|
121
|
+
prev.filter((n) => n.id !== payload.notificationId)
|
|
122
|
+
);
|
|
123
|
+
setUnreadCount((prev) => Math.max(0, prev - 1));
|
|
124
|
+
} else {
|
|
125
|
+
// All marked as read - clear the list
|
|
126
|
+
setRecentNotifications([]);
|
|
127
|
+
setUnreadCount(0);
|
|
128
|
+
}
|
|
129
|
+
}, [])
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ==========================================================================
|
|
133
|
+
|
|
134
|
+
// Fetch notifications when dropdown opens
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (isOpen) {
|
|
137
|
+
void fetchRecentNotifications();
|
|
138
|
+
}
|
|
139
|
+
}, [isOpen, fetchRecentNotifications]);
|
|
140
|
+
|
|
141
|
+
const handleMarkAllAsRead = async () => {
|
|
142
|
+
try {
|
|
143
|
+
await notificationClient.markAsRead({});
|
|
144
|
+
setUnreadCount(0);
|
|
145
|
+
setRecentNotifications([]);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("Failed to mark all as read:", error);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleClose = useCallback(() => {
|
|
152
|
+
setIsOpen(false);
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
// Hide notification bell for unauthenticated users
|
|
156
|
+
if (isAuthLoading || !session) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (loading) {
|
|
161
|
+
return (
|
|
162
|
+
<Button variant="ghost" size="icon" className="relative" disabled>
|
|
163
|
+
<Bell className="h-5 w-5" />
|
|
164
|
+
</Button>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<DropdownMenu>
|
|
170
|
+
<DropdownMenuTrigger
|
|
171
|
+
onClick={() => {
|
|
172
|
+
setIsOpen(!isOpen);
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<Button variant="ghost" size="icon" className="relative group">
|
|
176
|
+
<Bell className="h-5 w-5 transition-transform group-hover:scale-110" />
|
|
177
|
+
{unreadCount > 0 && (
|
|
178
|
+
<span className="absolute -top-1 -right-1 flex h-5 min-w-[20px] items-center justify-center">
|
|
179
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-destructive opacity-75" />
|
|
180
|
+
<Badge
|
|
181
|
+
variant="destructive"
|
|
182
|
+
className="relative h-5 min-w-[20px] flex items-center justify-center p-0 text-xs font-bold"
|
|
183
|
+
>
|
|
184
|
+
{unreadCount > 99 ? "99+" : unreadCount}
|
|
185
|
+
</Badge>
|
|
186
|
+
</span>
|
|
187
|
+
)}
|
|
188
|
+
</Button>
|
|
189
|
+
</DropdownMenuTrigger>
|
|
190
|
+
<DropdownMenuContent
|
|
191
|
+
isOpen={isOpen}
|
|
192
|
+
onClose={handleClose}
|
|
193
|
+
className="w-80"
|
|
194
|
+
>
|
|
195
|
+
{/* Header */}
|
|
196
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
197
|
+
<span className="font-semibold text-sm">Notifications</span>
|
|
198
|
+
{unreadCount > 0 && (
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="sm"
|
|
202
|
+
className="h-6 text-xs"
|
|
203
|
+
onClick={() => {
|
|
204
|
+
void handleMarkAllAsRead();
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<CheckCheck className="h-3 w-3 mr-1" />
|
|
208
|
+
Mark all read
|
|
209
|
+
</Button>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Notification List */}
|
|
214
|
+
<div className="max-h-[400px] overflow-y-auto">
|
|
215
|
+
{recentNotifications.length === 0 ? (
|
|
216
|
+
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
|
|
217
|
+
No unread notifications
|
|
218
|
+
</div>
|
|
219
|
+
) : (
|
|
220
|
+
<>
|
|
221
|
+
{recentNotifications.map((notification) => (
|
|
222
|
+
<DropdownMenuItem
|
|
223
|
+
key={notification.id}
|
|
224
|
+
className={`flex flex-col items-start gap-1 px-3 py-2 cursor-pointer ${
|
|
225
|
+
notification.importance === "critical"
|
|
226
|
+
? "border-l-2 border-l-destructive"
|
|
227
|
+
: notification.importance === "warning"
|
|
228
|
+
? "border-l-2 border-l-warning"
|
|
229
|
+
: ""
|
|
230
|
+
}`}
|
|
231
|
+
>
|
|
232
|
+
<div
|
|
233
|
+
className={`font-medium text-sm ${
|
|
234
|
+
notification.importance === "critical"
|
|
235
|
+
? "text-destructive"
|
|
236
|
+
: notification.importance === "warning"
|
|
237
|
+
? "text-warning"
|
|
238
|
+
: "text-foreground"
|
|
239
|
+
}`}
|
|
240
|
+
>
|
|
241
|
+
{notification.title}
|
|
242
|
+
</div>
|
|
243
|
+
<div className="text-xs text-muted-foreground line-clamp-2">
|
|
244
|
+
{stripMarkdown(notification.body)}
|
|
245
|
+
</div>
|
|
246
|
+
{notification.action && (
|
|
247
|
+
<div className="flex gap-2 mt-1">
|
|
248
|
+
<Link
|
|
249
|
+
to={notification.action.url}
|
|
250
|
+
className="text-xs text-primary hover:underline"
|
|
251
|
+
onClick={(e: React.MouseEvent) => {
|
|
252
|
+
e.stopPropagation();
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{notification.action.label}
|
|
256
|
+
</Link>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</DropdownMenuItem>
|
|
260
|
+
))}
|
|
261
|
+
</>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<DropdownMenuSeparator />
|
|
266
|
+
|
|
267
|
+
{/* Footer */}
|
|
268
|
+
<DropdownMenuItem
|
|
269
|
+
onClick={() => {
|
|
270
|
+
handleClose();
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
<Link
|
|
274
|
+
to={resolveRoute(notificationRoutes.routes.home)}
|
|
275
|
+
className="w-full text-center text-sm text-primary"
|
|
276
|
+
>
|
|
277
|
+
View all notifications
|
|
278
|
+
</Link>
|
|
279
|
+
</DropdownMenuItem>
|
|
280
|
+
</DropdownMenuContent>
|
|
281
|
+
</DropdownMenu>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { StrategyConfigCard, type ConfigSection } from "@checkmate-monitor/ui";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Strategy data from getDeliveryStrategies endpoint
|
|
5
|
+
*/
|
|
6
|
+
export interface DeliveryStrategy {
|
|
7
|
+
qualifiedId: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
icon?: string;
|
|
11
|
+
ownerPluginId: string;
|
|
12
|
+
contactResolution: {
|
|
13
|
+
type:
|
|
14
|
+
| "auth-email"
|
|
15
|
+
| "auth-provider"
|
|
16
|
+
| "user-config"
|
|
17
|
+
| "oauth-link"
|
|
18
|
+
| "custom";
|
|
19
|
+
provider?: string;
|
|
20
|
+
field?: string;
|
|
21
|
+
};
|
|
22
|
+
requiresUserConfig: boolean;
|
|
23
|
+
requiresOAuthLink: boolean;
|
|
24
|
+
configSchema: Record<string, unknown>;
|
|
25
|
+
userConfigSchema?: Record<string, unknown>;
|
|
26
|
+
/** Layout config schema for admin customization (logo, colors, etc.) */
|
|
27
|
+
layoutConfigSchema?: Record<string, unknown>;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
config?: Record<string, unknown>;
|
|
30
|
+
/** Current layout config values */
|
|
31
|
+
layoutConfig?: Record<string, unknown>;
|
|
32
|
+
/** Markdown instructions for admins (setup guides, etc.) */
|
|
33
|
+
adminInstructions?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StrategyCardProps {
|
|
37
|
+
strategy: DeliveryStrategy;
|
|
38
|
+
onUpdate: (
|
|
39
|
+
strategyId: string,
|
|
40
|
+
enabled: boolean,
|
|
41
|
+
config?: Record<string, unknown>,
|
|
42
|
+
layoutConfig?: Record<string, unknown>
|
|
43
|
+
) => Promise<void>;
|
|
44
|
+
saving?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get contact resolution type badge for notification strategies
|
|
49
|
+
*/
|
|
50
|
+
function getResolutionBadge(
|
|
51
|
+
type: DeliveryStrategy["contactResolution"]["type"]
|
|
52
|
+
) {
|
|
53
|
+
switch (type) {
|
|
54
|
+
case "auth-email": {
|
|
55
|
+
return { label: "User Email", variant: "secondary" as const };
|
|
56
|
+
}
|
|
57
|
+
case "auth-provider": {
|
|
58
|
+
return { label: "Auth Provider", variant: "secondary" as const };
|
|
59
|
+
}
|
|
60
|
+
case "user-config": {
|
|
61
|
+
return { label: "User Config Required", variant: "outline" as const };
|
|
62
|
+
}
|
|
63
|
+
case "oauth-link": {
|
|
64
|
+
return { label: "OAuth Link", variant: "default" as const };
|
|
65
|
+
}
|
|
66
|
+
default: {
|
|
67
|
+
return { label: "Custom", variant: "outline" as const };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Admin card for configuring a delivery strategy.
|
|
74
|
+
* Uses the shared StrategyConfigCard component.
|
|
75
|
+
*/
|
|
76
|
+
export function StrategyCard({
|
|
77
|
+
strategy,
|
|
78
|
+
onUpdate,
|
|
79
|
+
saving,
|
|
80
|
+
}: StrategyCardProps) {
|
|
81
|
+
// Build badges array from strategy properties
|
|
82
|
+
const badges = [
|
|
83
|
+
getResolutionBadge(strategy.contactResolution.type),
|
|
84
|
+
...(strategy.requiresOAuthLink
|
|
85
|
+
? [{ label: "OAuth", variant: "outline" as const, className: "text-xs" }]
|
|
86
|
+
: []),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// Check if config is missing - has schema properties but no saved config
|
|
90
|
+
const hasConfigSchema =
|
|
91
|
+
strategy.configSchema &&
|
|
92
|
+
"properties" in strategy.configSchema &&
|
|
93
|
+
Object.keys(strategy.configSchema.properties as Record<string, unknown>)
|
|
94
|
+
.length > 0;
|
|
95
|
+
const configMissing = hasConfigSchema && strategy.config === undefined;
|
|
96
|
+
|
|
97
|
+
const handleToggle = async (id: string, enabled: boolean) => {
|
|
98
|
+
await onUpdate(id, enabled, strategy.config, strategy.layoutConfig);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Build config sections array
|
|
102
|
+
const configSections: ConfigSection[] = [];
|
|
103
|
+
|
|
104
|
+
// Main configuration section
|
|
105
|
+
if (hasConfigSchema) {
|
|
106
|
+
configSections.push({
|
|
107
|
+
id: "config",
|
|
108
|
+
title: "Configuration",
|
|
109
|
+
schema: strategy.configSchema,
|
|
110
|
+
value: strategy.config,
|
|
111
|
+
onSave: async (config) => {
|
|
112
|
+
await onUpdate(
|
|
113
|
+
strategy.qualifiedId,
|
|
114
|
+
strategy.enabled,
|
|
115
|
+
config,
|
|
116
|
+
strategy.layoutConfig
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Layout configuration section (if strategy supports it)
|
|
123
|
+
if (strategy.layoutConfigSchema) {
|
|
124
|
+
configSections.push({
|
|
125
|
+
id: "layout",
|
|
126
|
+
title: "Email Layout",
|
|
127
|
+
schema: strategy.layoutConfigSchema,
|
|
128
|
+
value: strategy.layoutConfig,
|
|
129
|
+
onSave: async (layoutConfig) => {
|
|
130
|
+
await onUpdate(
|
|
131
|
+
strategy.qualifiedId,
|
|
132
|
+
strategy.enabled,
|
|
133
|
+
strategy.config,
|
|
134
|
+
layoutConfig
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<StrategyConfigCard
|
|
142
|
+
strategy={{
|
|
143
|
+
id: strategy.qualifiedId,
|
|
144
|
+
displayName: strategy.displayName,
|
|
145
|
+
description: strategy.description,
|
|
146
|
+
icon: strategy.icon,
|
|
147
|
+
enabled: strategy.enabled,
|
|
148
|
+
}}
|
|
149
|
+
configSections={configSections}
|
|
150
|
+
onToggle={handleToggle}
|
|
151
|
+
saving={saving}
|
|
152
|
+
badges={badges}
|
|
153
|
+
subtitle={`From: ${strategy.ownerPluginId}`}
|
|
154
|
+
disabledWarning="Enable this channel to allow users to receive notifications"
|
|
155
|
+
configMissing={configMissing}
|
|
156
|
+
instructions={strategy.adminInstructions}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|