strapi-plugin-notifier 1.0.3 → 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-C5mgbISF.js → Index-DVk2enQ2.js} +19 -1
- package/dist/_chunks/{Index-DOQrGurB.mjs → Index-_PTtaZsz.mjs} +19 -1
- package/dist/_chunks/{SettingsPage-CRsuB4cw.mjs → SettingsPage-CuTIslG0.mjs} +94 -2
- package/dist/_chunks/{SettingsPage-Cft7agRa.js → SettingsPage-DYxNBA3h.js} +92 -0
- package/dist/_chunks/{index-7hrPEwa_.mjs → index-BgPnE501.mjs} +2 -2
- package/dist/_chunks/{index-DrwLcZBZ.js → index-CKh_iBP2.js} +2 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +315 -92
- package/dist/server/index.mjs +315 -92
- package/package.json +5 -2
- 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/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
|
|
|
@@ -4,7 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const react = require("react");
|
|
5
5
|
const designSystem = require("@strapi/design-system");
|
|
6
6
|
const strapiAdmin = require("@strapi/admin/strapi-admin");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-CKh_iBP2.js");
|
|
8
8
|
const icons = require("@strapi/icons");
|
|
9
9
|
const toStoreItem = (n) => ({
|
|
10
10
|
id: String(n.id),
|
|
@@ -204,6 +204,24 @@ function NotificationItem({
|
|
|
204
204
|
children: notification.title
|
|
205
205
|
}
|
|
206
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
|
+
),
|
|
207
225
|
notification.url && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "↗" })
|
|
208
226
|
] }),
|
|
209
227
|
notification.message && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { marginTop: 2 }, children: notification.message }),
|
|
@@ -2,7 +2,7 @@ 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
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-
|
|
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";
|
|
6
6
|
import { Cross } from "@strapi/icons";
|
|
7
7
|
const toStoreItem = (n) => ({
|
|
8
8
|
id: String(n.id),
|
|
@@ -202,6 +202,24 @@ function NotificationItem({
|
|
|
202
202
|
children: notification.title
|
|
203
203
|
}
|
|
204
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
|
+
),
|
|
205
223
|
notification.url && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "↗" })
|
|
206
224
|
] }),
|
|
207
225
|
notification.message && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { marginTop: 2 }, children: notification.message }),
|
|
@@ -1,12 +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";
|
|
3
|
+
import { Box, Flex, Typography, Button, NumberInput, Toggle, Divider, Checkbox, TextInput } from "@strapi/design-system";
|
|
4
4
|
import { useFetchClient } from "@strapi/admin/strapi-admin";
|
|
5
5
|
const DEFAULT = {
|
|
6
6
|
retention: { maxDays: 90, maxPerUser: 500 },
|
|
7
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
|
+
},
|
|
8
15
|
ui: { theme: { accent: { info: "#4945ff", success: "#5cb85c", warning: "#f0ad4e", error: "#ee5e52" } } }
|
|
9
16
|
};
|
|
17
|
+
const MERGE_KEY_OPTIONS = [
|
|
18
|
+
{ value: "title", label: "Title" },
|
|
19
|
+
{ value: "type", label: "Type" },
|
|
20
|
+
{ value: "url", label: "URL" }
|
|
21
|
+
];
|
|
10
22
|
function SettingsPage() {
|
|
11
23
|
const { get, put, del } = useFetchClient();
|
|
12
24
|
const [settings, setSettings] = useState(DEFAULT);
|
|
@@ -51,6 +63,13 @@ function SettingsPage() {
|
|
|
51
63
|
return next;
|
|
52
64
|
});
|
|
53
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
|
+
};
|
|
54
73
|
return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
|
|
55
74
|
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 6, children: [
|
|
56
75
|
/* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "Notifier Settings" }),
|
|
@@ -110,6 +129,79 @@ function SettingsPage() {
|
|
|
110
129
|
)
|
|
111
130
|
] })
|
|
112
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
|
+
] }),
|
|
113
205
|
/* @__PURE__ */ jsxs(Box, { background: "neutral0", padding: 6, hasRadius: true, marginTop: 4, children: [
|
|
114
206
|
/* @__PURE__ */ jsx(Typography, { variant: "delta", paddingBottom: 4, as: "h2", children: "Theme" }),
|
|
115
207
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
@@ -7,8 +7,20 @@ const strapiAdmin = require("@strapi/admin/strapi-admin");
|
|
|
7
7
|
const DEFAULT = {
|
|
8
8
|
retention: { maxDays: 90, maxPerUser: 500 },
|
|
9
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
|
+
},
|
|
10
17
|
ui: { theme: { accent: { info: "#4945ff", success: "#5cb85c", warning: "#f0ad4e", error: "#ee5e52" } } }
|
|
11
18
|
};
|
|
19
|
+
const MERGE_KEY_OPTIONS = [
|
|
20
|
+
{ value: "title", label: "Title" },
|
|
21
|
+
{ value: "type", label: "Type" },
|
|
22
|
+
{ value: "url", label: "URL" }
|
|
23
|
+
];
|
|
12
24
|
function SettingsPage() {
|
|
13
25
|
const { get, put, del } = strapiAdmin.useFetchClient();
|
|
14
26
|
const [settings, setSettings] = react.useState(DEFAULT);
|
|
@@ -53,6 +65,13 @@ function SettingsPage() {
|
|
|
53
65
|
return next;
|
|
54
66
|
});
|
|
55
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
|
+
};
|
|
56
75
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 8, children: [
|
|
57
76
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 6, children: [
|
|
58
77
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", children: "Notifier Settings" }),
|
|
@@ -112,6 +131,79 @@ function SettingsPage() {
|
|
|
112
131
|
)
|
|
113
132
|
] })
|
|
114
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
|
+
] }),
|
|
115
207
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { background: "neutral0", padding: 6, hasRadius: true, marginTop: 4, children: [
|
|
116
208
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", paddingBottom: 4, as: "h2", children: "Theme" }),
|
|
117
209
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|
|
@@ -176,7 +176,7 @@ const index = {
|
|
|
176
176
|
defaultMessage: "Notifications"
|
|
177
177
|
},
|
|
178
178
|
permissions: [],
|
|
179
|
-
Component: () => import("./Index-
|
|
179
|
+
Component: () => import("./Index-_PTtaZsz.mjs")
|
|
180
180
|
});
|
|
181
181
|
app.createSettingSection(
|
|
182
182
|
{
|
|
@@ -194,7 +194,7 @@ const index = {
|
|
|
194
194
|
},
|
|
195
195
|
id: `${pluginId}.settings`,
|
|
196
196
|
to: `/settings/${pluginId}`,
|
|
197
|
-
Component: () => import("./SettingsPage-
|
|
197
|
+
Component: () => import("./SettingsPage-CuTIslG0.mjs"),
|
|
198
198
|
permissions: [
|
|
199
199
|
{ action: `plugin::${pluginId}.settings.read`, subject: null }
|
|
200
200
|
]
|
|
@@ -177,7 +177,7 @@ const index = {
|
|
|
177
177
|
defaultMessage: "Notifications"
|
|
178
178
|
},
|
|
179
179
|
permissions: [],
|
|
180
|
-
Component: () => Promise.resolve().then(() => require("./Index-
|
|
180
|
+
Component: () => Promise.resolve().then(() => require("./Index-DVk2enQ2.js"))
|
|
181
181
|
});
|
|
182
182
|
app.createSettingSection(
|
|
183
183
|
{
|
|
@@ -195,7 +195,7 @@ const index = {
|
|
|
195
195
|
},
|
|
196
196
|
id: `${pluginId}.settings`,
|
|
197
197
|
to: `/settings/${pluginId}`,
|
|
198
|
-
Component: () => Promise.resolve().then(() => require("./SettingsPage-
|
|
198
|
+
Component: () => Promise.resolve().then(() => require("./SettingsPage-DYxNBA3h.js")),
|
|
199
199
|
permissions: [
|
|
200
200
|
{ action: `plugin::${pluginId}.settings.read`, subject: null }
|
|
201
201
|
]
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED