strapi-plugin-notifier 1.0.2 → 1.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 +36 -0
- package/README.md +130 -20
- package/dist/_chunks/{Index-CiDBgkGf.js → Index-DVk2enQ2.js} +36 -19
- package/dist/_chunks/{Index-DI2t9tlH.mjs → Index-_PTtaZsz.mjs} +20 -3
- package/dist/_chunks/{SettingsPage-DIKfBfvo.mjs → SettingsPage-CuTIslG0.mjs} +95 -6
- package/dist/_chunks/{SettingsPage-BdBxZMqP.js → SettingsPage-DYxNBA3h.js} +100 -11
- package/dist/_chunks/index-BgPnE501.mjs +221 -0
- package/dist/_chunks/index-CKh_iBP2.js +220 -0
- package/dist/admin/index.js +1 -2
- package/dist/admin/index.mjs +1 -2
- package/dist/server/index.js +315 -92
- package/dist/server/index.mjs +315 -92
- package/package.json +9 -3
- package/tests/helpers.ts +145 -0
- package/tests/unit/config.test.ts +64 -0
- package/tests/unit/notification.test.ts +263 -0
- package/tests/unit/notifier.test.ts +348 -0
- package/tests/unit/preference.test.ts +122 -0
- package/vitest.config.ts +9 -0
- package/dist/_chunks/index-7WJGsVEY.js +0 -28282
- package/dist/_chunks/index-CNYabBMJ.mjs +0 -28264
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.1.0] — 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Batch sending** — `notifier.sendBatch(items[])` sends multiple notifications in one call; settings fetched once and opt-out/merge applied per item.
|
|
7
|
+
- **User opt-out** — new `notification-preference` content-type (`notifier_preferences` table). Each admin user can set `globalOptOut: true` to suppress all notifications, or `mutedTypes: ['info', ...]` to mute by type. Opt-out is applied at query time, covering broadcast and role-targeted notifications.
|
|
8
|
+
- New routes: `GET /notifier/preferences/me`, `PUT /notifier/preferences/me`
|
|
9
|
+
- **Notification merging** — when `merge.enabled` is `true` in settings, sending a notification whose key fields match an existing one (within `windowMinutes`) updates that notification's `mergeCount` instead of creating a new row.
|
|
10
|
+
- New notification fields: `mergeKey` (string), `mergeCount` (integer, default 1)
|
|
11
|
+
- New settings block: `merge.{ enabled, windowMinutes, keyFields, countBadge, rewriteMessage }`
|
|
12
|
+
- Inbox shows a coloured `×N` count badge when `countBadge` is on
|
|
13
|
+
- Merge settings editable at runtime via **Settings → Notifier → Merge**
|
|
14
|
+
- **Unit tests** — 61 tests covering `mergeWithDefaults`, preference service (isOptedOut, get, upsert), notification service (opt-out filtering, findMergeCandidate, mergeInto, createMany), and notifier service (send, sendBatch, merge paths, opt-out, convenience wrappers).
|
|
15
|
+
|
|
16
|
+
### Migration notes
|
|
17
|
+
- A new `notifier_preferences` table is created automatically on next Strapi start — no manual migration needed.
|
|
18
|
+
- Two new columns (`mergeKey`, `mergeCount`) are added to the existing `notifier_notifications` table — existing rows are unaffected (columns are nullable / have defaults).
|
|
19
|
+
- Existing stored settings in the plugin store are unaffected; the new `merge` config block defaults to `enabled: false`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [1.0.3] — 2026-06-11
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Settings link used `React.lazy()` wrapper — Strapi's `createSettingSection` validates `typeof Component === 'function'`; `lazy()` returns an object and fails that check. Changed to plain `() => import(...)`.
|
|
27
|
+
- `@strapi/admin` moved to `peerDependencies` so `pack-up` marks it external; previously `useFetchClient` was bundled into the plugin chunk, causing a blank admin panel when using a linked copy of the plugin.
|
|
28
|
+
- Admin route index exported an array instead of an object, preventing route registration.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## [1.0.2] — 2026-06-10
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Initial public release.
|
|
36
|
+
- Live bell icon, notification inbox, targeting (broadcast / role / user), configurable retention, delivery settings, UI accent theme, and Settings panel.
|
package/README.md
CHANGED
|
@@ -7,7 +7,10 @@ A first-class notification system for Strapi v5 admin panels. Adds a live bell i
|
|
|
7
7
|
- **Live bell icon** — badge count updated by polling the server (interval configurable)
|
|
8
8
|
- **Notification inbox** — filter by type, mark as read, dismiss, load more, clear all
|
|
9
9
|
- **Targeting** — broadcast to all admins, or target by user ID or role code
|
|
10
|
-
- **
|
|
10
|
+
- **Batch sending** — send multiple notifications in one call, settings fetched once
|
|
11
|
+
- **Merge** — collapse repeated notifications into one item with a count badge (fully configurable)
|
|
12
|
+
- **User opt-out** — each admin user can mute individual notification types or opt out entirely
|
|
13
|
+
- **Configurable** — poll interval, page size, retention policy, UI accent colours, and merge rules
|
|
11
14
|
- **Settings panel** — runtime configuration via Settings → Notifier (persisted in Strapi plugin store)
|
|
12
15
|
- **Retention cron** — daily cleanup at 3 AM, respects maxDays and maxPerUser limits
|
|
13
16
|
- **Strapi v5 only**
|
|
@@ -51,6 +54,13 @@ export default {
|
|
|
51
54
|
pollIntervalMs: 30_000, // how often the Bell polls (ms)
|
|
52
55
|
pageSize: 20, // notifications per page in inbox
|
|
53
56
|
},
|
|
57
|
+
merge: {
|
|
58
|
+
enabled: false, // enable notification merging
|
|
59
|
+
windowMinutes: 60, // look back this many minutes for a merge candidate
|
|
60
|
+
keyFields: ['title', 'type'], // fields that must match to merge
|
|
61
|
+
countBadge: true, // show "×N" badge on merged items in the inbox
|
|
62
|
+
rewriteMessage: false, // rewrite the message to "N× title" when merging
|
|
63
|
+
},
|
|
54
64
|
ui: {
|
|
55
65
|
theme: {
|
|
56
66
|
accent: {
|
|
@@ -102,30 +112,125 @@ notifier.send({
|
|
|
102
112
|
});
|
|
103
113
|
```
|
|
104
114
|
|
|
115
|
+
### Batch sending
|
|
116
|
+
|
|
117
|
+
Send multiple notifications in one call. Settings (including merge config) are fetched once and applied to all items — more efficient than looping over `send()`.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
await notifier.sendBatch([
|
|
121
|
+
{ title: 'New job application', type: 'info', to: { role: 'strapi-editor' } },
|
|
122
|
+
{ title: 'Export ready', type: 'success', to: { userId: 42 } },
|
|
123
|
+
{ title: 'Low disk space', type: 'warning' }, // broadcast
|
|
124
|
+
]);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Opt-out and merge rules are applied per item within the batch.
|
|
128
|
+
|
|
105
129
|
### Notification options
|
|
106
130
|
|
|
107
|
-
| Field | Type
|
|
108
|
-
|
|
109
|
-
| `title` | `string`
|
|
110
|
-
| `message` | `string`
|
|
111
|
-
| `type` | `'info' \| 'success' \| 'warning' \| 'error'` | No
|
|
112
|
-
| `url` | `string`
|
|
131
|
+
| Field | Type | Required | Default |
|
|
132
|
+
|-----------|-----------------------------------------------|----------|-----------|
|
|
133
|
+
| `title` | `string` | Yes | — |
|
|
134
|
+
| `message` | `string` | No | — |
|
|
135
|
+
| `type` | `'info' \| 'success' \| 'warning' \| 'error'` | No | `'info'` |
|
|
136
|
+
| `url` | `string` | No | — |
|
|
137
|
+
| `to` | `{ userId?: number; role?: string }` | No | broadcast |
|
|
138
|
+
|
|
139
|
+
## Notification merging
|
|
140
|
+
|
|
141
|
+
When `merge.enabled` is `true`, sending a notification whose key fields match an existing notification (created within `windowMinutes`) updates that notification instead of creating a new one.
|
|
142
|
+
|
|
143
|
+
**Example — a form that can be submitted many times:**
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// config/plugins.ts
|
|
147
|
+
merge: { enabled: true, windowMinutes: 60, keyFields: ['title', 'type'], countBadge: true }
|
|
148
|
+
|
|
149
|
+
// In your lifecycle / service:
|
|
150
|
+
notifier.toRole('strapi-editor', {
|
|
151
|
+
title: 'New job application',
|
|
152
|
+
type: 'info',
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
After five submissions the inbox shows a single item: **New job application ×5**.
|
|
157
|
+
|
|
158
|
+
**Merge key fields** (`title`, `type`, `url`) — any combination. Two notifications are considered the same if all selected fields match AND the recipient matches AND the most recent one was created within the window.
|
|
159
|
+
|
|
160
|
+
**Display options:**
|
|
161
|
+
|
|
162
|
+
| Option | Default | Effect |
|
|
163
|
+
|------------------|---------|--------------------------------------------------|
|
|
164
|
+
| `countBadge` | `true` | Shows a coloured "×N" pill next to the title |
|
|
165
|
+
| `rewriteMessage` | `false` | Replaces the message with `"N× <title>"` |
|
|
166
|
+
|
|
167
|
+
Both can be toggled from the **Settings → Notifier → Merge** panel at runtime.
|
|
168
|
+
|
|
169
|
+
## User opt-out
|
|
170
|
+
|
|
171
|
+
Each admin user can control which notifications they receive. Opt-out is applied at query time so it covers broadcast and role-targeted notifications too.
|
|
172
|
+
|
|
173
|
+
### Via the API
|
|
174
|
+
|
|
175
|
+
```http
|
|
176
|
+
GET /notifier/preferences/me
|
|
177
|
+
PUT /notifier/preferences/me
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Get current preferences:**
|
|
181
|
+
```http
|
|
182
|
+
GET /notifier/preferences/me
|
|
183
|
+
→ { "data": { "userId": 5, "globalOptOut": false, "mutedTypes": ["info"] } }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Mute info and success notifications:**
|
|
187
|
+
```http
|
|
188
|
+
PUT /notifier/preferences/me
|
|
189
|
+
Content-Type: application/json
|
|
190
|
+
|
|
191
|
+
{ "mutedTypes": ["info", "success"] }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Opt out entirely:**
|
|
195
|
+
```http
|
|
196
|
+
PUT /notifier/preferences/me
|
|
197
|
+
Content-Type: application/json
|
|
198
|
+
|
|
199
|
+
{ "globalOptOut": true }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Re-enable:**
|
|
203
|
+
```http
|
|
204
|
+
PUT /notifier/preferences/me
|
|
205
|
+
Content-Type: application/json
|
|
206
|
+
|
|
207
|
+
{ "globalOptOut": false, "mutedTypes": [] }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Preference fields
|
|
211
|
+
|
|
212
|
+
| Field | Type | Default | Description |
|
|
213
|
+
|----------------|--------------------|---------|------------------------------------------------|
|
|
214
|
+
| `globalOptOut` | `boolean` | `false` | Suppress all notifications for this user |
|
|
215
|
+
| `mutedTypes` | `NotificationType[]` | `[]` | Suppress only these notification types |
|
|
113
216
|
|
|
114
217
|
## API routes
|
|
115
218
|
|
|
116
219
|
All routes require `admin::isAuthenticatedAdmin`. The plugin mounts under `/notifier/`.
|
|
117
220
|
|
|
118
|
-
| Method | Path
|
|
119
|
-
|
|
120
|
-
| GET | `/notifier/notifications`
|
|
121
|
-
| PUT | `/notifier/notifications/read-all` | Mark all as read
|
|
122
|
-
| DELETE | `/notifier/notifications`
|
|
123
|
-
| PUT | `/notifier/notifications/:id/read` | Mark one as read
|
|
124
|
-
| DELETE | `/notifier/notifications/:id`
|
|
125
|
-
| GET | `/notifier/config`
|
|
126
|
-
| GET | `/notifier/settings`
|
|
127
|
-
| PUT | `/notifier/settings`
|
|
128
|
-
| DELETE | `/notifier/settings`
|
|
221
|
+
| Method | Path | Description |
|
|
222
|
+
|--------|------------------------------------|----------------------------------------|
|
|
223
|
+
| GET | `/notifier/notifications` | List notifications (paginated) |
|
|
224
|
+
| PUT | `/notifier/notifications/read-all` | Mark all as read |
|
|
225
|
+
| DELETE | `/notifier/notifications` | Clear all notifications |
|
|
226
|
+
| PUT | `/notifier/notifications/:id/read` | Mark one as read |
|
|
227
|
+
| DELETE | `/notifier/notifications/:id` | Clear one notification |
|
|
228
|
+
| GET | `/notifier/config` | Fetch UI config (safe for frontend) |
|
|
229
|
+
| GET | `/notifier/settings` | Get full settings |
|
|
230
|
+
| PUT | `/notifier/settings` | Update settings |
|
|
231
|
+
| DELETE | `/notifier/settings` | Reset settings to defaults |
|
|
232
|
+
| GET | `/notifier/preferences/me` | Get current user's notification prefs |
|
|
233
|
+
| PUT | `/notifier/preferences/me` | Update current user's notification prefs |
|
|
129
234
|
|
|
130
235
|
Query parameters for `GET /notifier/notifications`:
|
|
131
236
|
|
|
@@ -141,9 +246,14 @@ Two permissions are registered under the **Notifier** plugin section in **Settin
|
|
|
141
246
|
- `plugin::notifier.settings.read` — view the settings panel
|
|
142
247
|
- `plugin::notifier.settings.update` — save or reset settings
|
|
143
248
|
|
|
144
|
-
## Content
|
|
249
|
+
## Content types
|
|
250
|
+
|
|
251
|
+
| UID | Collection | Description |
|
|
252
|
+
|------------------------------------------|-----------------------------|--------------------------------------|
|
|
253
|
+
| `plugin::notifier.notification` | `notifier_notifications` | Notification records |
|
|
254
|
+
| `plugin::notifier.notification-preference` | `notifier_preferences` | Per-user opt-out preferences |
|
|
145
255
|
|
|
146
|
-
|
|
256
|
+
Both are hidden from Content Manager and Content-Type Builder by default.
|
|
147
257
|
|
|
148
258
|
## License
|
|
149
259
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
-
const
|
|
4
|
+
const react = require("react");
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
|
-
require("
|
|
6
|
+
const strapiAdmin = require("@strapi/admin/strapi-admin");
|
|
7
|
+
const index = require("./index-CKh_iBP2.js");
|
|
7
8
|
const icons = require("@strapi/icons");
|
|
8
|
-
const index = require("./index-7WJGsVEY.js");
|
|
9
|
-
require("@strapi/icons/symbols");
|
|
10
9
|
const toStoreItem = (n) => ({
|
|
11
10
|
id: String(n.id),
|
|
12
11
|
title: n.title,
|
|
@@ -17,11 +16,11 @@ const toStoreItem = (n) => ({
|
|
|
17
16
|
url: n.url
|
|
18
17
|
});
|
|
19
18
|
const useNotificationActions = () => {
|
|
20
|
-
const { get, put, del } =
|
|
21
|
-
const [isLoading, setIsLoading] =
|
|
22
|
-
const [pagination, setPagination] =
|
|
23
|
-
const currentPage =
|
|
24
|
-
const fetchPage =
|
|
19
|
+
const { get, put, del } = strapiAdmin.useFetchClient();
|
|
20
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
21
|
+
const [pagination, setPagination] = react.useState(null);
|
|
22
|
+
const currentPage = react.useRef(1);
|
|
23
|
+
const fetchPage = react.useCallback(
|
|
25
24
|
async (page, append = false) => {
|
|
26
25
|
const { pageSize } = index.getConfig();
|
|
27
26
|
setIsLoading(true);
|
|
@@ -45,12 +44,12 @@ const useNotificationActions = () => {
|
|
|
45
44
|
},
|
|
46
45
|
[get]
|
|
47
46
|
);
|
|
48
|
-
const fetchAll =
|
|
49
|
-
const loadMore =
|
|
47
|
+
const fetchAll = react.useCallback(() => fetchPage(1, false), [fetchPage]);
|
|
48
|
+
const loadMore = react.useCallback(() => {
|
|
50
49
|
if (!pagination || currentPage.current >= pagination.pageCount) return;
|
|
51
50
|
fetchPage(currentPage.current + 1, true);
|
|
52
51
|
}, [fetchPage, pagination]);
|
|
53
|
-
const handleMarkAsRead =
|
|
52
|
+
const handleMarkAsRead = react.useCallback(
|
|
54
53
|
async (id) => {
|
|
55
54
|
index.markAsRead(id);
|
|
56
55
|
try {
|
|
@@ -60,14 +59,14 @@ const useNotificationActions = () => {
|
|
|
60
59
|
},
|
|
61
60
|
[put]
|
|
62
61
|
);
|
|
63
|
-
const handleMarkAllAsRead =
|
|
62
|
+
const handleMarkAllAsRead = react.useCallback(async () => {
|
|
64
63
|
index.markAllAsRead();
|
|
65
64
|
try {
|
|
66
65
|
await put("/notifier/notifications/read-all");
|
|
67
66
|
} catch {
|
|
68
67
|
}
|
|
69
68
|
}, [put]);
|
|
70
|
-
const handleClear =
|
|
69
|
+
const handleClear = react.useCallback(
|
|
71
70
|
async (id) => {
|
|
72
71
|
index.clearNotification(id);
|
|
73
72
|
try {
|
|
@@ -77,7 +76,7 @@ const useNotificationActions = () => {
|
|
|
77
76
|
},
|
|
78
77
|
[del]
|
|
79
78
|
);
|
|
80
|
-
const handleClearAll =
|
|
79
|
+
const handleClearAll = react.useCallback(async () => {
|
|
81
80
|
index.clearAll();
|
|
82
81
|
try {
|
|
83
82
|
await del("/notifier/notifications");
|
|
@@ -150,8 +149,8 @@ function InboxHeader({
|
|
|
150
149
|
] });
|
|
151
150
|
}
|
|
152
151
|
const useNotifications = () => {
|
|
153
|
-
const [notifications, setNotifications] =
|
|
154
|
-
|
|
152
|
+
const [notifications, setNotifications] = react.useState(index.getNotifications);
|
|
153
|
+
react.useEffect(() => index.subscribeToNotifications(setNotifications), []);
|
|
155
154
|
return notifications;
|
|
156
155
|
};
|
|
157
156
|
const UNREAD_BG = {
|
|
@@ -205,6 +204,24 @@ function NotificationItem({
|
|
|
205
204
|
children: notification.title
|
|
206
205
|
}
|
|
207
206
|
),
|
|
207
|
+
notification.mergeCount != null && notification.mergeCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
208
|
+
designSystem.Typography,
|
|
209
|
+
{
|
|
210
|
+
variant: "pi",
|
|
211
|
+
style: {
|
|
212
|
+
background: accent,
|
|
213
|
+
color: "#fff",
|
|
214
|
+
borderRadius: "10px",
|
|
215
|
+
padding: "0 6px",
|
|
216
|
+
flexShrink: 0,
|
|
217
|
+
fontWeight: 600
|
|
218
|
+
},
|
|
219
|
+
children: [
|
|
220
|
+
"×",
|
|
221
|
+
notification.mergeCount
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
),
|
|
208
225
|
notification.url && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "↗" })
|
|
209
226
|
] }),
|
|
210
227
|
notification.message && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { marginTop: 2 }, children: notification.message }),
|
|
@@ -270,7 +287,7 @@ function NotificationList({
|
|
|
270
287
|
] });
|
|
271
288
|
}
|
|
272
289
|
function Index() {
|
|
273
|
-
const [filter, setFilter] =
|
|
290
|
+
const [filter, setFilter] = react.useState("all");
|
|
274
291
|
const {
|
|
275
292
|
fetchAll,
|
|
276
293
|
loadMore,
|
|
@@ -281,7 +298,7 @@ function Index() {
|
|
|
281
298
|
isLoading,
|
|
282
299
|
hasMore
|
|
283
300
|
} = useNotificationActions();
|
|
284
|
-
|
|
301
|
+
react.useEffect(() => {
|
|
285
302
|
fetchAll();
|
|
286
303
|
}, []);
|
|
287
304
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 8, style: { height: "100%" }, children: [
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
3
3
|
import { Box, Flex, Typography, Button, IconButton, Loader } from "@strapi/design-system";
|
|
4
|
-
import "
|
|
4
|
+
import { useFetchClient } from "@strapi/admin/strapi-admin";
|
|
5
|
+
import { g as getConfig, s as setNotifications, a as getNotifications, m as markAsRead, b as markAllAsRead, c as clearNotification, d as clearAll, e as subscribeToNotifications, u as usePluginConfig } from "./index-BgPnE501.mjs";
|
|
5
6
|
import { Cross } from "@strapi/icons";
|
|
6
|
-
import { u as useFetchClient, g as getConfig, s as setNotifications, a as getNotifications, m as markAsRead, b as markAllAsRead, c as clearNotification, d as clearAll, e as subscribeToNotifications, f as usePluginConfig } from "./index-CNYabBMJ.mjs";
|
|
7
|
-
import "@strapi/icons/symbols";
|
|
8
7
|
const toStoreItem = (n) => ({
|
|
9
8
|
id: String(n.id),
|
|
10
9
|
title: n.title,
|
|
@@ -203,6 +202,24 @@ function NotificationItem({
|
|
|
203
202
|
children: notification.title
|
|
204
203
|
}
|
|
205
204
|
),
|
|
205
|
+
notification.mergeCount != null && notification.mergeCount > 1 && /* @__PURE__ */ jsxs(
|
|
206
|
+
Typography,
|
|
207
|
+
{
|
|
208
|
+
variant: "pi",
|
|
209
|
+
style: {
|
|
210
|
+
background: accent,
|
|
211
|
+
color: "#fff",
|
|
212
|
+
borderRadius: "10px",
|
|
213
|
+
padding: "0 6px",
|
|
214
|
+
flexShrink: 0,
|
|
215
|
+
fontWeight: 600
|
|
216
|
+
},
|
|
217
|
+
children: [
|
|
218
|
+
"×",
|
|
219
|
+
notification.mergeCount
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
),
|
|
206
223
|
notification.url && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "↗" })
|
|
207
224
|
] }),
|
|
208
225
|
notification.message && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { marginTop: 2 }, children: notification.message }),
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { jsxs, jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from "react";
|
|
3
|
-
import { Box, Flex, Typography, Button, NumberInput, Divider, TextInput } from "@strapi/design-system";
|
|
4
|
-
import "
|
|
5
|
-
import "@strapi/icons";
|
|
6
|
-
import { u as useFetchClient } from "./index-CNYabBMJ.mjs";
|
|
7
|
-
import "@strapi/icons/symbols";
|
|
3
|
+
import { Box, Flex, Typography, Button, NumberInput, Toggle, Divider, Checkbox, TextInput } from "@strapi/design-system";
|
|
4
|
+
import { useFetchClient } from "@strapi/admin/strapi-admin";
|
|
8
5
|
const DEFAULT = {
|
|
9
6
|
retention: { maxDays: 90, maxPerUser: 500 },
|
|
10
7
|
delivery: { pollIntervalMs: 3e4, pageSize: 20 },
|
|
8
|
+
merge: {
|
|
9
|
+
enabled: false,
|
|
10
|
+
windowMinutes: 60,
|
|
11
|
+
keyFields: ["title", "type"],
|
|
12
|
+
countBadge: true,
|
|
13
|
+
rewriteMessage: false
|
|
14
|
+
},
|
|
11
15
|
ui: { theme: { accent: { info: "#4945ff", success: "#5cb85c", warning: "#f0ad4e", error: "#ee5e52" } } }
|
|
12
16
|
};
|
|
17
|
+
const MERGE_KEY_OPTIONS = [
|
|
18
|
+
{ value: "title", label: "Title" },
|
|
19
|
+
{ value: "type", label: "Type" },
|
|
20
|
+
{ value: "url", label: "URL" }
|
|
21
|
+
];
|
|
13
22
|
function SettingsPage() {
|
|
14
23
|
const { get, put, del } = useFetchClient();
|
|
15
24
|
const [settings, setSettings] = useState(DEFAULT);
|
|
@@ -54,6 +63,13 @@ function SettingsPage() {
|
|
|
54
63
|
return next;
|
|
55
64
|
});
|
|
56
65
|
};
|
|
66
|
+
const toggleMergeKeyField = (field) => {
|
|
67
|
+
setSettings((prev) => {
|
|
68
|
+
const current = prev.merge.keyFields;
|
|
69
|
+
const next = current.includes(field) ? current.filter((f) => f !== field) : [...current, field];
|
|
70
|
+
return { ...prev, merge: { ...prev.merge, keyFields: next } };
|
|
71
|
+
});
|
|
72
|
+
};
|
|
57
73
|
return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
|
|
58
74
|
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 6, children: [
|
|
59
75
|
/* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "Notifier Settings" }),
|
|
@@ -113,6 +129,79 @@ function SettingsPage() {
|
|
|
113
129
|
)
|
|
114
130
|
] })
|
|
115
131
|
] }),
|
|
132
|
+
/* @__PURE__ */ jsxs(Box, { background: "neutral0", padding: 6, hasRadius: true, marginTop: 4, children: [
|
|
133
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 4, children: [
|
|
134
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
135
|
+
/* @__PURE__ */ jsx(Typography, { variant: "delta", as: "h2", children: "Merge" }),
|
|
136
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Collapse repeated notifications into one, with a count badge." })
|
|
137
|
+
] }),
|
|
138
|
+
/* @__PURE__ */ jsx(
|
|
139
|
+
Toggle,
|
|
140
|
+
{
|
|
141
|
+
label: "Enable merging",
|
|
142
|
+
offLabel: "Off",
|
|
143
|
+
onLabel: "On",
|
|
144
|
+
checked: settings.merge.enabled,
|
|
145
|
+
onChange: (e) => set("merge.enabled", e.target.checked)
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
] }),
|
|
149
|
+
settings.merge.enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
150
|
+
/* @__PURE__ */ jsx(Divider, {}),
|
|
151
|
+
/* @__PURE__ */ jsxs(Box, { paddingTop: 4, children: [
|
|
152
|
+
/* @__PURE__ */ jsx(Flex, { gap: 4, wrap: "wrap", paddingBottom: 4, children: /* @__PURE__ */ jsx(
|
|
153
|
+
NumberInput,
|
|
154
|
+
{
|
|
155
|
+
label: "Window (minutes)",
|
|
156
|
+
hint: "How far back to look for a notification to merge into.",
|
|
157
|
+
value: settings.merge.windowMinutes,
|
|
158
|
+
onValueChange: (v) => set("merge.windowMinutes", v),
|
|
159
|
+
style: { flex: 1 }
|
|
160
|
+
}
|
|
161
|
+
) }),
|
|
162
|
+
/* @__PURE__ */ jsx(Typography, { variant: "sigma", paddingBottom: 2, children: "Merge key fields" }),
|
|
163
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", paddingBottom: 3, children: "Two notifications are considered the same if all selected fields match." }),
|
|
164
|
+
/* @__PURE__ */ jsx(Flex, { gap: 4, wrap: "wrap", paddingBottom: 4, children: MERGE_KEY_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsx(
|
|
165
|
+
Checkbox,
|
|
166
|
+
{
|
|
167
|
+
checked: settings.merge.keyFields.includes(value),
|
|
168
|
+
onCheckedChange: () => toggleMergeKeyField(value),
|
|
169
|
+
children: label
|
|
170
|
+
},
|
|
171
|
+
value
|
|
172
|
+
)) }),
|
|
173
|
+
/* @__PURE__ */ jsx(Typography, { variant: "sigma", paddingBottom: 2, children: "Display" }),
|
|
174
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 6, wrap: "wrap", children: [
|
|
175
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
176
|
+
/* @__PURE__ */ jsx(
|
|
177
|
+
Toggle,
|
|
178
|
+
{
|
|
179
|
+
label: "Count badge",
|
|
180
|
+
offLabel: "Off",
|
|
181
|
+
onLabel: "On",
|
|
182
|
+
checked: settings.merge.countBadge,
|
|
183
|
+
onChange: (e) => set("merge.countBadge", e.target.checked)
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
/* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Show ×N badge on merged items" }) })
|
|
187
|
+
] }),
|
|
188
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
189
|
+
/* @__PURE__ */ jsx(
|
|
190
|
+
Toggle,
|
|
191
|
+
{
|
|
192
|
+
label: "Rewrite message",
|
|
193
|
+
offLabel: "Off",
|
|
194
|
+
onLabel: "On",
|
|
195
|
+
checked: settings.merge.rewriteMessage,
|
|
196
|
+
onChange: (e) => set("merge.rewriteMessage", e.target.checked)
|
|
197
|
+
}
|
|
198
|
+
),
|
|
199
|
+
/* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: 'Replace message with "N× title"' }) })
|
|
200
|
+
] })
|
|
201
|
+
] })
|
|
202
|
+
] })
|
|
203
|
+
] })
|
|
204
|
+
] }),
|
|
116
205
|
/* @__PURE__ */ jsxs(Box, { background: "neutral0", padding: 6, hasRadius: true, marginTop: 4, children: [
|
|
117
206
|
/* @__PURE__ */ jsx(Typography, { variant: "delta", paddingBottom: 4, as: "h2", children: "Theme" }),
|
|
118
207
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
-
const
|
|
4
|
+
const react = require("react");
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
|
-
require("
|
|
7
|
-
require("@strapi/icons");
|
|
8
|
-
const index = require("./index-7WJGsVEY.js");
|
|
9
|
-
require("@strapi/icons/symbols");
|
|
6
|
+
const strapiAdmin = require("@strapi/admin/strapi-admin");
|
|
10
7
|
const DEFAULT = {
|
|
11
8
|
retention: { maxDays: 90, maxPerUser: 500 },
|
|
12
9
|
delivery: { pollIntervalMs: 3e4, pageSize: 20 },
|
|
10
|
+
merge: {
|
|
11
|
+
enabled: false,
|
|
12
|
+
windowMinutes: 60,
|
|
13
|
+
keyFields: ["title", "type"],
|
|
14
|
+
countBadge: true,
|
|
15
|
+
rewriteMessage: false
|
|
16
|
+
},
|
|
13
17
|
ui: { theme: { accent: { info: "#4945ff", success: "#5cb85c", warning: "#f0ad4e", error: "#ee5e52" } } }
|
|
14
18
|
};
|
|
19
|
+
const MERGE_KEY_OPTIONS = [
|
|
20
|
+
{ value: "title", label: "Title" },
|
|
21
|
+
{ value: "type", label: "Type" },
|
|
22
|
+
{ value: "url", label: "URL" }
|
|
23
|
+
];
|
|
15
24
|
function SettingsPage() {
|
|
16
|
-
const { get, put, del } =
|
|
17
|
-
const [settings, setSettings] =
|
|
18
|
-
const [isSaving, setIsSaving] =
|
|
19
|
-
const [isResetting, setIsResetting] =
|
|
20
|
-
const [saveError, setSaveError] =
|
|
21
|
-
|
|
25
|
+
const { get, put, del } = strapiAdmin.useFetchClient();
|
|
26
|
+
const [settings, setSettings] = react.useState(DEFAULT);
|
|
27
|
+
const [isSaving, setIsSaving] = react.useState(false);
|
|
28
|
+
const [isResetting, setIsResetting] = react.useState(false);
|
|
29
|
+
const [saveError, setSaveError] = react.useState(null);
|
|
30
|
+
react.useEffect(() => {
|
|
22
31
|
get("/notifier/settings").then(({ data }) => {
|
|
23
32
|
if (data) setSettings(data);
|
|
24
33
|
}).catch(() => {
|
|
@@ -56,6 +65,13 @@ function SettingsPage() {
|
|
|
56
65
|
return next;
|
|
57
66
|
});
|
|
58
67
|
};
|
|
68
|
+
const toggleMergeKeyField = (field) => {
|
|
69
|
+
setSettings((prev) => {
|
|
70
|
+
const current = prev.merge.keyFields;
|
|
71
|
+
const next = current.includes(field) ? current.filter((f) => f !== field) : [...current, field];
|
|
72
|
+
return { ...prev, merge: { ...prev.merge, keyFields: next } };
|
|
73
|
+
});
|
|
74
|
+
};
|
|
59
75
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 8, children: [
|
|
60
76
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 6, children: [
|
|
61
77
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", children: "Notifier Settings" }),
|
|
@@ -115,6 +131,79 @@ function SettingsPage() {
|
|
|
115
131
|
)
|
|
116
132
|
] })
|
|
117
133
|
] }),
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "neutral0", padding: 6, hasRadius: true, marginTop: 4, children: [
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 4, children: [
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
|
|
137
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", as: "h2", children: "Merge" }),
|
|
138
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Collapse repeated notifications into one, with a count badge." })
|
|
139
|
+
] }),
|
|
140
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
141
|
+
designSystem.Toggle,
|
|
142
|
+
{
|
|
143
|
+
label: "Enable merging",
|
|
144
|
+
offLabel: "Off",
|
|
145
|
+
onLabel: "On",
|
|
146
|
+
checked: settings.merge.enabled,
|
|
147
|
+
onChange: (e) => set("merge.enabled", e.target.checked)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
] }),
|
|
151
|
+
settings.merge.enabled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
152
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|
|
153
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingTop: 4, children: [
|
|
154
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 4, wrap: "wrap", paddingBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
155
|
+
designSystem.NumberInput,
|
|
156
|
+
{
|
|
157
|
+
label: "Window (minutes)",
|
|
158
|
+
hint: "How far back to look for a notification to merge into.",
|
|
159
|
+
value: settings.merge.windowMinutes,
|
|
160
|
+
onValueChange: (v) => set("merge.windowMinutes", v),
|
|
161
|
+
style: { flex: 1 }
|
|
162
|
+
}
|
|
163
|
+
) }),
|
|
164
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", paddingBottom: 2, children: "Merge key fields" }),
|
|
165
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", paddingBottom: 3, children: "Two notifications are considered the same if all selected fields match." }),
|
|
166
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 4, wrap: "wrap", paddingBottom: 4, children: MERGE_KEY_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
167
|
+
designSystem.Checkbox,
|
|
168
|
+
{
|
|
169
|
+
checked: settings.merge.keyFields.includes(value),
|
|
170
|
+
onCheckedChange: () => toggleMergeKeyField(value),
|
|
171
|
+
children: label
|
|
172
|
+
},
|
|
173
|
+
value
|
|
174
|
+
)) }),
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", paddingBottom: 2, children: "Display" }),
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 6, wrap: "wrap", children: [
|
|
177
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
178
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
179
|
+
designSystem.Toggle,
|
|
180
|
+
{
|
|
181
|
+
label: "Count badge",
|
|
182
|
+
offLabel: "Off",
|
|
183
|
+
onLabel: "On",
|
|
184
|
+
checked: settings.merge.countBadge,
|
|
185
|
+
onChange: (e) => set("merge.countBadge", e.target.checked)
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: "Show ×N badge on merged items" }) })
|
|
189
|
+
] }),
|
|
190
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
191
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
192
|
+
designSystem.Toggle,
|
|
193
|
+
{
|
|
194
|
+
label: "Rewrite message",
|
|
195
|
+
offLabel: "Off",
|
|
196
|
+
onLabel: "On",
|
|
197
|
+
checked: settings.merge.rewriteMessage,
|
|
198
|
+
onChange: (e) => set("merge.rewriteMessage", e.target.checked)
|
|
199
|
+
}
|
|
200
|
+
),
|
|
201
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: 'Replace message with "N× title"' }) })
|
|
202
|
+
] })
|
|
203
|
+
] })
|
|
204
|
+
] })
|
|
205
|
+
] })
|
|
206
|
+
] }),
|
|
118
207
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "neutral0", padding: 6, hasRadius: true, marginTop: 4, children: [
|
|
119
208
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", paddingBottom: 4, as: "h2", children: "Theme" }),
|
|
120
209
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|