@asteby/metacore-notifications 37.0.0 → 38.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/README.md +95 -0
- package/dist/dropdown.d.ts +5 -1
- package/dist/dropdown.d.ts.map +1 -1
- package/dist/dropdown.js +438 -83
- package/dist/dropdown.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/rich-text.d.ts +10 -1
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +15 -12
- package/dist/rich-text.js.map +1 -1
- package/dist/toast.d.ts +5 -0
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +3 -2
- package/dist/toast.js.map +1 -1
- package/dist/types.d.ts +111 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -51,6 +51,101 @@ import { NotificationsDropdown } from '@asteby/metacore-notifications'
|
|
|
51
51
|
- `metadata.addon_key` / `apartado` — module chip (Inventario, Almacén, POS, …)
|
|
52
52
|
- `metadata.color` — optional CSS/hex override for the icon disc
|
|
53
53
|
|
|
54
|
+
## Host with its own contract
|
|
55
|
+
|
|
56
|
+
Everything below is optional; without these props the dropdown behaves exactly
|
|
57
|
+
as before (`GET apiBasePath` → `{ data: [] }`, one `PATCH ${apiBasePath}/${id}`
|
|
58
|
+
per item, unread count computed over the loaded page, empty message while
|
|
59
|
+
loading or on a failed request). Only the live-toast id changed (`ntf-<id>`).
|
|
60
|
+
|
|
61
|
+
| Prop | Purpose |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `listParams` | Object or `(perPage) => object` replacing the default `orderBy/orderDir/per_page` query. |
|
|
64
|
+
| `parseList(body)` | Reads the response: `{ items, unreadCount?, total? }`. A server `unreadCount` wins over the local count. |
|
|
65
|
+
| `normalizeItem(raw)` | Maps each row **and each live payload** to a `NotificationItem` (`read_at` → `is_read`, `action_url` → `link`…). `NotificationItem.id` may be `string \| number`. |
|
|
66
|
+
| `onMarkRead(ids)` / `onMarkAllRead()` | Replace the default PATCHes. Optimistic update, rolled back if the promise rejects. With only `onMarkRead`, "mark all" calls it with the unread ids. |
|
|
67
|
+
| `onDelete(id)` | Shows a trash button per row (second click confirms; rolled back on failure). Labels: `delete`, `confirmDelete`. |
|
|
68
|
+
| `fetchUnreadCount` + `unreadPollMs` | Server counter, polled only while the tab is visible (and on becoming visible). `0`/unset = no polling. |
|
|
69
|
+
| `badgeMax`, `triggerAriaLabel(unread)` | Badge cap (default `99` → `99+`) and trigger `aria-label`. |
|
|
70
|
+
| `onIngest(payload, item)` | Called once per NEW live notification (after dedup) — sound, query invalidation, hidden-tab push belong to the host. |
|
|
71
|
+
| `footer`, `closeOnClick`, `renderAvatar`, `item.user` | Extra footer (`ReactNode` or `(close) => ReactNode`), menu close behaviour, custom avatar or `user: { name, avatar }` with initials fallback. |
|
|
72
|
+
| `richText`, `reloadAfterPermission` | See below; `reloadAfterPermission={false}` skips the page reload after granting push permission. |
|
|
73
|
+
| `showLoadStates` (default `false`) | Opt in to the "loading" and "error + retry" states (`labels.loading / error / retry`). Off, the list shows the usual empty message while loading and after a failed request, as before. |
|
|
74
|
+
|
|
75
|
+
`onNotificationClick` receives the full normalized item (metadata included).
|
|
76
|
+
When it is present nothing is opened by default; without it only absolute
|
|
77
|
+
`http(s)` links are opened (`window.open`), relative links are a no-op — pass
|
|
78
|
+
`onNotificationClick` to navigate inside a SPA. The live toast delegates its
|
|
79
|
+
click to the same handler.
|
|
80
|
+
|
|
81
|
+
`closeOnClick`: Radix already closes the menu when an item is selected, so the
|
|
82
|
+
unset default keeps closing. `true` closes explicitly, `false` keeps it open.
|
|
83
|
+
|
|
84
|
+
Deleting from the keyboard: focus the row (arrow keys) and press `Delete`
|
|
85
|
+
(Supr); a live region announces `labels.confirmDeleteKey` ("Pulsa Supr otra vez
|
|
86
|
+
para eliminar") and a second `Delete` on the same row confirms. Moving focus
|
|
87
|
+
away cancels. The trash button is a pointer-only affordance (`aria-hidden`,
|
|
88
|
+
`tabIndex=-1`) because an interactive control inside a `role="menuitem"` is not
|
|
89
|
+
valid ARIA and Radix only focuses items.
|
|
90
|
+
|
|
91
|
+
Concurrency: optimistic rollbacks are incremental (they only count rows still
|
|
92
|
+
present and currently read/unread, so live notifications that arrived meanwhile
|
|
93
|
+
are kept), a failed delete restores the row with its current read flag, and
|
|
94
|
+
`fetchUnreadCount` results that overlap an in-flight action are discarded (the
|
|
95
|
+
next poll reconciles). `listParams` as a function must be deterministic (no
|
|
96
|
+
`Date.now()`): it is serialized every render to detect changes. Links are opened
|
|
97
|
+
with `window.open(url, '_blank', 'noopener,noreferrer')`; live toasts use the id
|
|
98
|
+
`ntf-<id>` so numeric ids cannot collide with sonner's own.
|
|
99
|
+
|
|
100
|
+
### Example: `{ notifications, unread_count }` + `read_at`
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<NotificationsDropdown
|
|
104
|
+
apiClient={api}
|
|
105
|
+
apiBasePath='/notifications'
|
|
106
|
+
listParams={(n) => ({ limit: n })}
|
|
107
|
+
parseList={(body) => {
|
|
108
|
+
const b = body as { notifications: unknown[]; unread_count: number }
|
|
109
|
+
return { items: b.notifications, unreadCount: b.unread_count }
|
|
110
|
+
}}
|
|
111
|
+
normalizeItem={(raw) => {
|
|
112
|
+
const r = raw as Record<string, any>
|
|
113
|
+
return {
|
|
114
|
+
id: r.id, // numeric ids are fine
|
|
115
|
+
title: r.title,
|
|
116
|
+
message: r.body ?? '',
|
|
117
|
+
type: r.type ?? 'info',
|
|
118
|
+
is_read: r.read_at != null,
|
|
119
|
+
created_at: r.created_at,
|
|
120
|
+
link: r.action_url,
|
|
121
|
+
image: r.image_url,
|
|
122
|
+
metadata: r.meta_payload,
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
onMarkRead={(ids) => api.post('/notifications/mark-read', { notification_ids: ids }).then(() => {})}
|
|
126
|
+
onMarkAllRead={() => api.post('/notifications/mark-read', { mark_all: true }).then(() => {})}
|
|
127
|
+
onDelete={(id) => api.delete(`/notifications/${id}`).then(() => {})}
|
|
128
|
+
fetchUnreadCount={() => api.get('/notifications/unread-count').then((r) => r.data.count)}
|
|
129
|
+
unreadPollMs={60_000}
|
|
130
|
+
onNotificationClick={(n) => navigate({ to: n.link })}
|
|
131
|
+
onIngest={(payload, item) => playSound()}
|
|
132
|
+
footer={(close) => <Link to='/notifications' onClick={close}>Ver todas</Link>}
|
|
133
|
+
richText={false}
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Rich text and XSS
|
|
138
|
+
|
|
139
|
+
By default (`richText` true) bodies go through `formatNotificationBodyHtml`,
|
|
140
|
+
a small **regex/tokenizer sanitizer** (allowlist: `strong b em i u mark code br
|
|
141
|
+
span`, no attributes; any other `<` is escaped) rendered with
|
|
142
|
+
`dangerouslySetInnerHTML`. It has hostile-input tests (`<img onerror>`,
|
|
143
|
+
`javascript:` hrefs, `on*` attributes, `<svg onload>`, malformed nesting,
|
|
144
|
+
`<b/onclick=…>`), but it is not a DOM-based sanitizer. The body of campaigns
|
|
145
|
+
and other **user-authored content is untrusted**: pass `richText={false}` to
|
|
146
|
+
the dropdown (and to `showNotificationToast`) so it is painted as plain text
|
|
147
|
+
escaped by React.
|
|
148
|
+
|
|
54
149
|
## API
|
|
55
150
|
|
|
56
151
|
- `GET {apiBasePath}` → `{ data: NotificationItem[] }`
|
package/dist/dropdown.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { NotificationsDropdownProps } from './types';
|
|
|
2
2
|
/**
|
|
3
3
|
* Bell-icon dropdown: REST list + live ingest (SSE preferred, WebSocket
|
|
4
4
|
* fallback) + optional canonical card toast on every new item.
|
|
5
|
+
*
|
|
6
|
+
* Every prop beyond `apiClient` / `apiBasePath` is optional; without them the
|
|
7
|
+
* component keeps its original contract (`{ data: [] }` list, one PATCH per
|
|
8
|
+
* item, local unread count).
|
|
5
9
|
*/
|
|
6
|
-
export declare function NotificationsDropdown({ apiClient, apiBasePath, enableBadge, onNotificationClick, perPage, locale, labels: labelsOverride, resolveImageUrl, subscribeToNotifications, sseUrl, sseAccessToken, preferSse, showToastOnIngest, }: NotificationsDropdownProps): import("react").JSX.Element;
|
|
10
|
+
export declare function NotificationsDropdown({ apiClient, apiBasePath, enableBadge, onNotificationClick, perPage, locale, labels: labelsOverride, resolveImageUrl, subscribeToNotifications, sseUrl, sseAccessToken, preferSse, showToastOnIngest, listParams, parseList, normalizeItem, onMarkRead, onMarkAllRead, onDelete, fetchUnreadCount, unreadPollMs, badgeMax, triggerAriaLabel, onIngest, footer, closeOnClick, renderAvatar, richText, reloadAfterPermission, showLoadStates, }: NotificationsDropdownProps): import("react").JSX.Element;
|
|
7
11
|
//# sourceMappingURL=dropdown.d.ts.map
|
package/dist/dropdown.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown.d.ts","sourceRoot":"","sources":["../src/dropdown.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"dropdown.d.ts","sourceRoot":"","sources":["../src/dropdown.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAMV,0BAA0B,EAC3B,MAAM,SAAS,CAAA;AA+ChB;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,WAAW,EACX,WAAkB,EAClB,mBAAmB,EACnB,OAAY,EACZ,MAAW,EACX,MAAM,EAAE,cAAc,EACtB,eAAe,EACf,wBAAwB,EACxB,MAAM,EACN,cAAc,EACd,SAAS,EACT,iBAAwB,EACxB,UAAU,EACV,SAAS,EACT,aAAa,EACb,UAAU,EACV,aAAa,EACb,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,QAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,QAAe,EACf,qBAA4B,EAC5B,cAAsB,GACvB,EAAE,0BAA0B,+BAke5B"}
|