strapi-plugin-notifier 1.2.0 → 1.2.2
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/README.md +161 -4
- package/logo.png +0 -0
- package/package.json +1 -1
- package/scripts/gen-logo.js +68 -0
package/README.md
CHANGED
|
@@ -10,9 +10,10 @@ A first-class notification system for Strapi v5 admin panels. Adds a live bell i
|
|
|
10
10
|
- **Batch sending** — send multiple notifications in one call, settings fetched once
|
|
11
11
|
- **Merge** — collapse repeated notifications into one item with a count badge (fully configurable)
|
|
12
12
|
- **User opt-out** — each admin user can mute individual notification types or opt out entirely
|
|
13
|
-
- **
|
|
13
|
+
- **Declarative rules** — trigger notifications from lifecycle hooks, eventHub events, or cron schedules — no code required beyond `config/plugins.ts`
|
|
14
|
+
- **Configurable** — poll interval, page size, retention policy, UI accent colours, merge rules, and cleanup schedule
|
|
14
15
|
- **Settings panel** — runtime configuration via Settings → Notifier (persisted in Strapi plugin store)
|
|
15
|
-
- **Retention cron** —
|
|
16
|
+
- **Retention cron** — configurable cleanup schedule (default: 3 AM daily), respects maxDays and maxPerUser limits
|
|
16
17
|
- **Strapi v5 only**
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
@@ -47,8 +48,9 @@ export default {
|
|
|
47
48
|
enabled: true,
|
|
48
49
|
config: {
|
|
49
50
|
retention: {
|
|
50
|
-
maxDays: 90,
|
|
51
|
-
maxPerUser: 500,
|
|
51
|
+
maxDays: 90, // delete notifications older than N days
|
|
52
|
+
maxPerUser: 500, // cap notifications stored per user
|
|
53
|
+
cleanupCron: '0 3 * * *', // cron schedule for cleanup job (default: 3 AM daily)
|
|
52
54
|
},
|
|
53
55
|
delivery: {
|
|
54
56
|
pollIntervalMs: 30_000, // how often the Bell polls (ms)
|
|
@@ -214,6 +216,161 @@ Content-Type: application/json
|
|
|
214
216
|
| `globalOptOut` | `boolean` | `false` | Suppress all notifications for this user |
|
|
215
217
|
| `mutedTypes` | `NotificationType[]` | `[]` | Suppress only these notification types |
|
|
216
218
|
|
|
219
|
+
## Declarative rules
|
|
220
|
+
|
|
221
|
+
Instead of calling the notifier service in code, you can declare rules in `config/plugins.ts`. The plugin wires up the listeners at bootstrap — notifications fire automatically with no further code changes needed.
|
|
222
|
+
|
|
223
|
+
Three trigger types are supported: `lifecycle`, `event`, and `cron`.
|
|
224
|
+
|
|
225
|
+
### `on: 'lifecycle'` — content-type create/update/delete
|
|
226
|
+
|
|
227
|
+
Fires when Strapi's DB lifecycle executes for a given content-type and action.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// config/plugins.ts
|
|
231
|
+
notifier: {
|
|
232
|
+
enabled: true,
|
|
233
|
+
config: {
|
|
234
|
+
rules: [
|
|
235
|
+
{
|
|
236
|
+
on: 'lifecycle',
|
|
237
|
+
model: 'api::article.article',
|
|
238
|
+
action: 'afterCreate', // afterCreate | afterUpdate | afterDelete | afterPublish | afterUnpublish
|
|
239
|
+
notification: {
|
|
240
|
+
title: 'New article: {{entry.title}}',
|
|
241
|
+
message: 'Created by {{entry.createdBy.firstname}}',
|
|
242
|
+
type: 'info',
|
|
243
|
+
to: { role: 'strapi-editor' },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`entry` in templates is the DB record produced by the lifecycle action (`result` for after-hooks, `params.data` for before-hooks).
|
|
252
|
+
|
|
253
|
+
### `on: 'event'` — strapi.eventHub subscription
|
|
254
|
+
|
|
255
|
+
Fires on any named event published to Strapi's internal event hub (lifecycle events, media events, plugin events, or your own custom events emitted with `strapi.eventHub.emit()`).
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
{
|
|
259
|
+
on: 'event',
|
|
260
|
+
event: 'media.upload', // any strapi.eventHub event name
|
|
261
|
+
filter: { uid: 'api::article.article' }, // optional shallow key=value filter on payload
|
|
262
|
+
notification: {
|
|
263
|
+
title: 'File uploaded: {{media.name}}',
|
|
264
|
+
type: 'info',
|
|
265
|
+
to: 'broadcast',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Custom events from your own services:**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// In your service
|
|
274
|
+
strapi.eventHub.emit('order.placed', { order });
|
|
275
|
+
|
|
276
|
+
// In config/plugins.ts
|
|
277
|
+
{
|
|
278
|
+
on: 'event',
|
|
279
|
+
event: 'order.placed',
|
|
280
|
+
notification: {
|
|
281
|
+
title: 'New order: #{{order.id}}',
|
|
282
|
+
message: '{{order.customerName}} — €{{order.total}}',
|
|
283
|
+
type: 'success',
|
|
284
|
+
to: { role: 'strapi-super-admin' },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### `on: 'cron'` — time-based notifications
|
|
290
|
+
|
|
291
|
+
Fires on a cron schedule. Standard 5-field cron expressions.
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
{
|
|
295
|
+
on: 'cron',
|
|
296
|
+
schedule: '0 9 * * 1', // Every Monday at 9 AM
|
|
297
|
+
notification: {
|
|
298
|
+
title: 'Weekly reminder: review pending content',
|
|
299
|
+
type: 'warning',
|
|
300
|
+
to: { role: 'strapi-editor' },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Notification templates
|
|
306
|
+
|
|
307
|
+
Every field in `notification` accepts either a **static value** or a **function** for dynamic content.
|
|
308
|
+
|
|
309
|
+
**String interpolation** — use `{{path.to.field}}` dot-notation to resolve values from the event context:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
notification: {
|
|
313
|
+
title: 'New order from {{entry.customer.name}}',
|
|
314
|
+
message: 'Total: {{entry.total}} — shipped to {{entry.address.city}}',
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Function templates** — for logic that can't be expressed as a template string:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
notification: {
|
|
322
|
+
title: (ctx) => `${ctx.entry.priority === 'high' ? '🔴' : ''} Order #${ctx.entry.id}`,
|
|
323
|
+
type: (ctx) => ctx.entry.priority === 'high' ? 'error' : 'info',
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Targeting in rules
|
|
328
|
+
|
|
329
|
+
The `to` field supports the same options as the programmatic API, plus a `userIdFrom` path resolver:
|
|
330
|
+
|
|
331
|
+
| Value | Behaviour |
|
|
332
|
+
|-----------------------------|---------------------------------------------------------|
|
|
333
|
+
| `'broadcast'` (or omitted) | Send to all admin users |
|
|
334
|
+
| `{ role: 'strapi-editor' }` | Send to all users with this role code |
|
|
335
|
+
| `{ userId: 42 }` | Send to a specific admin user |
|
|
336
|
+
| `{ userIdFrom: 'entry.createdBy.id' }` | Resolve user ID via dot-path into the event context — useful for "notify the author" patterns |
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// Notify the author when their content is rejected
|
|
340
|
+
{
|
|
341
|
+
on: 'lifecycle',
|
|
342
|
+
model: 'api::article.article',
|
|
343
|
+
action: 'afterUpdate',
|
|
344
|
+
when: (ctx) => ctx.entry.status === 'rejected',
|
|
345
|
+
notification: {
|
|
346
|
+
title: 'Your article was rejected',
|
|
347
|
+
to: { userIdFrom: 'entry.createdBy.id' },
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### `when` guard
|
|
353
|
+
|
|
354
|
+
Add a `when` function to conditionally skip the notification. Return `false` to suppress it.
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
{
|
|
358
|
+
on: 'lifecycle',
|
|
359
|
+
model: 'api::article.article',
|
|
360
|
+
action: 'afterUpdate',
|
|
361
|
+
when: (ctx) => ctx.entry.publishedAt != null, // only fire when published
|
|
362
|
+
notification: {
|
|
363
|
+
title: 'Article published: {{entry.title}}',
|
|
364
|
+
type: 'success',
|
|
365
|
+
to: 'broadcast',
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`when` is not available on `cron` rules (there is no event context to filter on).
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
217
374
|
## API routes
|
|
218
375
|
|
|
219
376
|
All routes require `admin::isAuthenticatedAdmin`. The plugin mounts under `/notifier/`.
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Generates logo.png (500×500) using sharp from the previewer's node_modules.
|
|
2
|
+
// Run: node scripts/gen-logo.js
|
|
3
|
+
const sharp = require(
|
|
4
|
+
'C:/Users/TemitopeAlabi/projects/strapi/learning_strapi/previewer/node_modules/sharp'
|
|
5
|
+
);
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500" viewBox="0 0 500 500">
|
|
9
|
+
<defs>
|
|
10
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
11
|
+
<stop offset="0%" stop-color="#5B58FF"/>
|
|
12
|
+
<stop offset="100%" stop-color="#2320A8"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
15
|
+
<feDropShadow dx="0" dy="6" stdDeviation="10" flood-color="#000" flood-opacity="0.25"/>
|
|
16
|
+
</filter>
|
|
17
|
+
</defs>
|
|
18
|
+
|
|
19
|
+
<!-- Background -->
|
|
20
|
+
<rect width="500" height="500" fill="url(#bg)" rx="90"/>
|
|
21
|
+
|
|
22
|
+
<!-- Bell body -->
|
|
23
|
+
<g filter="url(#shadow)">
|
|
24
|
+
<!-- Knob / handle -->
|
|
25
|
+
<rect x="234" y="98" width="32" height="34" rx="10" fill="white"/>
|
|
26
|
+
|
|
27
|
+
<!-- Main bell shape -->
|
|
28
|
+
<path d="
|
|
29
|
+
M 250 128
|
|
30
|
+
C 198 128, 152 165, 150 215
|
|
31
|
+
L 136 332
|
|
32
|
+
C 133 350, 144 362, 160 362
|
|
33
|
+
L 340 362
|
|
34
|
+
C 356 362, 367 350, 364 332
|
|
35
|
+
L 350 215
|
|
36
|
+
C 348 165, 302 128, 250 128
|
|
37
|
+
Z
|
|
38
|
+
" fill="white"/>
|
|
39
|
+
|
|
40
|
+
<!-- Bottom clapper -->
|
|
41
|
+
<ellipse cx="250" cy="366" rx="40" ry="20" fill="white"/>
|
|
42
|
+
|
|
43
|
+
<!-- Clapper cutout (same colour as background area) so it looks recessed -->
|
|
44
|
+
<ellipse cx="250" cy="370" rx="25" ry="13" fill="#3A38D4"/>
|
|
45
|
+
</g>
|
|
46
|
+
|
|
47
|
+
<!-- Notification badge -->
|
|
48
|
+
<circle cx="348" cy="148" r="46" fill="#EE5E52"/>
|
|
49
|
+
<circle cx="348" cy="148" r="38" fill="#EE5E52"/>
|
|
50
|
+
<text
|
|
51
|
+
x="348" y="149"
|
|
52
|
+
text-anchor="middle"
|
|
53
|
+
dominant-baseline="central"
|
|
54
|
+
font-family="Arial, Helvetica, sans-serif"
|
|
55
|
+
font-size="34"
|
|
56
|
+
font-weight="bold"
|
|
57
|
+
fill="white"
|
|
58
|
+
letter-spacing="-1"
|
|
59
|
+
>N</text>
|
|
60
|
+
</svg>`;
|
|
61
|
+
|
|
62
|
+
const out = path.resolve(__dirname, '..', 'logo.png');
|
|
63
|
+
|
|
64
|
+
sharp(Buffer.from(svg))
|
|
65
|
+
.png()
|
|
66
|
+
.toFile(out)
|
|
67
|
+
.then(() => console.log('logo.png saved →', out))
|
|
68
|
+
.catch((err) => { console.error(err); process.exit(1); });
|