@actuate-media/cms-admin 0.12.1 → 0.14.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/dist/__tests__/components/comment-side-panel.test.js +93 -0
- package/dist/__tests__/components/comment-side-panel.test.js.map +1 -1
- package/dist/__tests__/components/mentionable-textarea.test.d.ts +2 -0
- package/dist/__tests__/components/mentionable-textarea.test.d.ts.map +1 -0
- package/dist/__tests__/components/mentionable-textarea.test.js +190 -0
- package/dist/__tests__/components/mentionable-textarea.test.js.map +1 -0
- package/dist/__tests__/components/notification-bell.test.js +146 -0
- package/dist/__tests__/components/notification-bell.test.js.map +1 -1
- package/dist/__tests__/lib/mention-picker.test.d.ts +2 -0
- package/dist/__tests__/lib/mention-picker.test.d.ts.map +1 -0
- package/dist/__tests__/lib/mention-picker.test.js +122 -0
- package/dist/__tests__/lib/mention-picker.test.js.map +1 -0
- package/dist/__tests__/lib/notifications-client.test.js +129 -1
- package/dist/__tests__/lib/notifications-client.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/CommentSidePanel.d.ts +9 -1
- package/dist/components/CommentSidePanel.d.ts.map +1 -1
- package/dist/components/CommentSidePanel.js +8 -7
- package/dist/components/CommentSidePanel.js.map +1 -1
- package/dist/components/MentionableTextarea.d.ts +68 -0
- package/dist/components/MentionableTextarea.d.ts.map +1 -0
- package/dist/components/MentionableTextarea.js +181 -0
- package/dist/components/MentionableTextarea.js.map +1 -0
- package/dist/components/NotificationBell.d.ts +30 -9
- package/dist/components/NotificationBell.d.ts.map +1 -1
- package/dist/components/NotificationBell.js +57 -5
- package/dist/components/NotificationBell.js.map +1 -1
- package/dist/lib/api.d.ts +8 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +10 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/mention-picker.d.ts +69 -0
- package/dist/lib/mention-picker.d.ts.map +1 -0
- package/dist/lib/mention-picker.js +138 -0
- package/dist/lib/mention-picker.js.map +1 -0
- package/dist/lib/notifications-client.d.ts +53 -0
- package/dist/lib/notifications-client.d.ts.map +1 -1
- package/dist/lib/notifications-client.js +89 -1
- package/dist/lib/notifications-client.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/components/comment-side-panel.test.tsx +134 -0
- package/src/__tests__/components/mentionable-textarea.test.tsx +226 -0
- package/src/__tests__/components/notification-bell.test.tsx +167 -1
- package/src/__tests__/lib/mention-picker.test.ts +145 -0
- package/src/__tests__/lib/notifications-client.test.ts +145 -1
- package/src/components/CommentSidePanel.tsx +37 -14
- package/src/components/MentionableTextarea.tsx +328 -0
- package/src/components/NotificationBell.tsx +84 -12
- package/src/lib/api.ts +11 -0
- package/src/lib/mention-picker.ts +180 -0
- package/src/lib/notifications-client.ts +147 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type MentionUser } from '../lib/mention-picker.js';
|
|
3
|
+
/**
|
|
4
|
+
* `<MentionableTextarea />` — drop-in replacement for `<textarea>` /
|
|
5
|
+
* `<input type="text">` that opens a Radix-styled mention picker
|
|
6
|
+
* whenever the user types `@`.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour
|
|
9
|
+
* ─────────
|
|
10
|
+
* - The picker appears anchored to the textarea, below the input.
|
|
11
|
+
* We intentionally do NOT track the caret pixel position (that
|
|
12
|
+
* requires a textarea-mirror trick which doesn't add enough UX to
|
|
13
|
+
* justify the complexity); GitHub, Linear, and Asana all anchor
|
|
14
|
+
* their pickers this way on simple textareas.
|
|
15
|
+
* - Arrow Up / Arrow Down move the highlight. Enter / Tab commit the
|
|
16
|
+
* selection. Escape closes the picker without inserting anything.
|
|
17
|
+
* Mouse click on a row also commits.
|
|
18
|
+
* - The picker only fires server queries while a real mention is
|
|
19
|
+
* being typed; it short-circuits an empty `query` so the dropdown
|
|
20
|
+
* shows up immediately after `@` is typed (with an empty state)
|
|
21
|
+
* and only flips to "no matches" once the user has typed at least
|
|
22
|
+
* one character.
|
|
23
|
+
* - The textarea value is fully controlled by the parent — we never
|
|
24
|
+
* keep a separate copy. `onChange` is called with the next value
|
|
25
|
+
* on every keystroke AND every mention insertion.
|
|
26
|
+
*
|
|
27
|
+
* Dependency-injection seam
|
|
28
|
+
* ─────────────────────────
|
|
29
|
+
* The `search` prop accepts an override so tests can substitute a
|
|
30
|
+
* synchronous fake. Production callers leave it unset and the
|
|
31
|
+
* component uses the real `searchMentionUsers()` REST helper.
|
|
32
|
+
*/
|
|
33
|
+
export interface MentionableTextareaProps {
|
|
34
|
+
value: string;
|
|
35
|
+
onChange: (next: string) => void;
|
|
36
|
+
/** Forwarded to the underlying textarea. */
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
/** Forwarded to the underlying textarea. */
|
|
39
|
+
rows?: number;
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Forwarded — useful for keyboard shortcuts the parent owns. */
|
|
42
|
+
onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
43
|
+
/** Forwarded — useful for click-to-stop-propagation in nested cards. */
|
|
44
|
+
onClick?: (event: React.MouseEvent<HTMLTextAreaElement>) => void;
|
|
45
|
+
/** Test hook — required when rendering with React state assertions. */
|
|
46
|
+
'data-testid'?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Override the user search. The default uses the REST helper.
|
|
49
|
+
* Tests inject an in-memory fake so dropdown contents are
|
|
50
|
+
* deterministic without HTTP mocking.
|
|
51
|
+
*/
|
|
52
|
+
search?: (query: string) => Promise<MentionUser[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional fixed id for the picker dropdown — pass when the parent
|
|
55
|
+
* wires up `aria-controls`. We generate one with `useId()` when
|
|
56
|
+
* unset.
|
|
57
|
+
*/
|
|
58
|
+
pickerId?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Format the displayed token. By default we show `@<name>` while
|
|
61
|
+
* the underlying value stores `@<userId>`. A future slice can swap
|
|
62
|
+
* to a rich-text editor where these diverge; the prop lets that
|
|
63
|
+
* land without changing the picker.
|
|
64
|
+
*/
|
|
65
|
+
formatLabel?: (user: MentionUser) => string;
|
|
66
|
+
}
|
|
67
|
+
export declare function MentionableTextarea({ value, onChange, placeholder, rows, className, onKeyDown, onClick, 'data-testid': testId, search, pickerId: pickerIdProp, formatLabel, }: MentionableTextareaProps): React.JSX.Element;
|
|
68
|
+
//# sourceMappingURL=MentionableTextarea.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MentionableTextarea.d.ts","sourceRoot":"","sources":["../../src/components/MentionableTextarea.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA0D,MAAM,OAAO,CAAA;AAE9E,OAAO,EACL,KAAK,WAAW,EAIjB,MAAM,0BAA0B,CAAA;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAA;IACrE,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAA;IAChE,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;IAClD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,MAAM,CAAA;CAC5C;AAgBD,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,WAAW,EACX,IAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,EACP,aAAa,EAAE,MAAM,EACrB,MAAyE,EACzE,QAAQ,EAAE,YAAY,EACtB,WAAiC,GAClC,EAAE,wBAAwB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAiO9C"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { detectMentionContext, insertMentionToken, searchMentionUsers, } from '../lib/mention-picker.js';
|
|
5
|
+
const initialPicker = {
|
|
6
|
+
open: false,
|
|
7
|
+
query: '',
|
|
8
|
+
highlight: 0,
|
|
9
|
+
users: [],
|
|
10
|
+
};
|
|
11
|
+
export function MentionableTextarea({ value, onChange, placeholder, rows = 3, className, onKeyDown, onClick, 'data-testid': testId, search = (q) => searchMentionUsers(q).then((r) => (r.ok ? r.result : [])), pickerId: pickerIdProp, formatLabel = (u) => `@${u.name}`, }) {
|
|
12
|
+
const generatedId = useId();
|
|
13
|
+
const pickerId = pickerIdProp ?? `actuate-mention-picker-${generatedId}`;
|
|
14
|
+
const textareaRef = useRef(null);
|
|
15
|
+
const [picker, setPicker] = useState(initialPicker);
|
|
16
|
+
// `pickerRef` mirrors `picker` for the keydown handler so the
|
|
17
|
+
// arrow-key / enter listeners don't go stale between renders.
|
|
18
|
+
const pickerRef = useRef(picker);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
pickerRef.current = picker;
|
|
21
|
+
}, [picker]);
|
|
22
|
+
const requestSeq = useRef(0);
|
|
23
|
+
const mountedRef = useRef(true);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
return () => {
|
|
26
|
+
mountedRef.current = false;
|
|
27
|
+
};
|
|
28
|
+
}, []);
|
|
29
|
+
const closePicker = useCallback(() => {
|
|
30
|
+
setPicker((p) => (p.open ? initialPicker : p));
|
|
31
|
+
}, []);
|
|
32
|
+
// Recompute the active mention context whenever the value or
|
|
33
|
+
// selection changes. Centralising this in one effect keeps the
|
|
34
|
+
// open/close logic in a single place — every textarea event
|
|
35
|
+
// (input, click, keyup) just calls `recompute()`.
|
|
36
|
+
const recompute = useCallback(async (nextValue, caret) => {
|
|
37
|
+
const ctx = detectMentionContext(nextValue, caret);
|
|
38
|
+
if (!ctx) {
|
|
39
|
+
closePicker();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const id = ++requestSeq.current;
|
|
43
|
+
// Open immediately so the user sees a dropdown the moment they
|
|
44
|
+
// type `@`. Clear the `users` array on every new query so the
|
|
45
|
+
// user can never press Enter / Tab and commit a stale row
|
|
46
|
+
// from the previous query while the new request is in flight
|
|
47
|
+
// (PR #159 Copilot finding — type `@a` then `@bo` quickly and
|
|
48
|
+
// hit Enter before the `@bo` results arrive: without this
|
|
49
|
+
// reset, the picker would commit a row from `@a`).
|
|
50
|
+
setPicker((p) => ({
|
|
51
|
+
open: true,
|
|
52
|
+
query: ctx.query,
|
|
53
|
+
highlight: 0,
|
|
54
|
+
users: p.query === ctx.query ? p.users : [],
|
|
55
|
+
}));
|
|
56
|
+
try {
|
|
57
|
+
const users = await search(ctx.query);
|
|
58
|
+
if (!mountedRef.current)
|
|
59
|
+
return;
|
|
60
|
+
// Drop stale responses — only commit if our request is still
|
|
61
|
+
// the latest one. Otherwise the user has typed more and a
|
|
62
|
+
// newer query is in flight.
|
|
63
|
+
if (id !== requestSeq.current)
|
|
64
|
+
return;
|
|
65
|
+
setPicker((p) =>
|
|
66
|
+
// If the picker was closed (Escape) between request and
|
|
67
|
+
// response, don't reopen it.
|
|
68
|
+
p.open ? { ...p, users, highlight: 0 } : p);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
if (!mountedRef.current)
|
|
72
|
+
return;
|
|
73
|
+
if (id !== requestSeq.current)
|
|
74
|
+
return;
|
|
75
|
+
// Surface no rows on failure — the textarea must keep
|
|
76
|
+
// working when the user is offline. The poll loop / native
|
|
77
|
+
// submit path is unaffected.
|
|
78
|
+
setPicker((p) => (p.open ? { ...p, users: [] } : p));
|
|
79
|
+
}
|
|
80
|
+
}, [closePicker, search]);
|
|
81
|
+
const commitMention = useCallback((user) => {
|
|
82
|
+
const el = textareaRef.current;
|
|
83
|
+
if (!el)
|
|
84
|
+
return;
|
|
85
|
+
const caret = el.selectionStart ?? value.length;
|
|
86
|
+
const ctx = detectMentionContext(value, caret);
|
|
87
|
+
if (!ctx) {
|
|
88
|
+
closePicker();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const next = insertMentionToken(value, ctx, user.id);
|
|
92
|
+
onChange(next.value);
|
|
93
|
+
closePicker();
|
|
94
|
+
// Move the caret past the inserted token AFTER React paints
|
|
95
|
+
// the new value, so the selection isn't clobbered by the
|
|
96
|
+
// controlled update.
|
|
97
|
+
requestAnimationFrame(() => {
|
|
98
|
+
const live = textareaRef.current;
|
|
99
|
+
if (!live)
|
|
100
|
+
return;
|
|
101
|
+
live.focus();
|
|
102
|
+
live.setSelectionRange(next.caret, next.caret);
|
|
103
|
+
});
|
|
104
|
+
}, [closePicker, onChange, value]);
|
|
105
|
+
const handleChange = (event) => {
|
|
106
|
+
const next = event.target.value;
|
|
107
|
+
const caret = event.target.selectionStart ?? next.length;
|
|
108
|
+
onChange(next);
|
|
109
|
+
void recompute(next, caret);
|
|
110
|
+
};
|
|
111
|
+
const handleKeyDown = (event) => {
|
|
112
|
+
const p = pickerRef.current;
|
|
113
|
+
if (p.open && p.users.length > 0) {
|
|
114
|
+
if (event.key === 'ArrowDown') {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
setPicker((s) => ({ ...s, highlight: (s.highlight + 1) % s.users.length }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (event.key === 'ArrowUp') {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
setPicker((s) => ({
|
|
122
|
+
...s,
|
|
123
|
+
highlight: (s.highlight - 1 + s.users.length) % s.users.length,
|
|
124
|
+
}));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
// Defensive: highlight could theoretically point past the
|
|
130
|
+
// end of `users` if state went stale between renders. Guard
|
|
131
|
+
// it so we never `undefined`-bang.
|
|
132
|
+
const target = p.users[p.highlight];
|
|
133
|
+
if (target)
|
|
134
|
+
commitMention(target);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (event.key === 'Escape' && p.open) {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
closePicker();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
onKeyDown?.(event);
|
|
144
|
+
};
|
|
145
|
+
const handleClick = (event) => {
|
|
146
|
+
onClick?.(event);
|
|
147
|
+
const el = textareaRef.current;
|
|
148
|
+
if (!el)
|
|
149
|
+
return;
|
|
150
|
+
void recompute(el.value, el.selectionStart ?? el.value.length);
|
|
151
|
+
};
|
|
152
|
+
// Closing on outside click — when the user clicks anywhere outside
|
|
153
|
+
// the textarea or the picker, the dropdown should disappear so it
|
|
154
|
+
// doesn't sit on top of unrelated UI.
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!picker.open)
|
|
157
|
+
return;
|
|
158
|
+
const onDocMouseDown = (ev) => {
|
|
159
|
+
const t = ev.target;
|
|
160
|
+
if (!t)
|
|
161
|
+
return;
|
|
162
|
+
const parent = textareaRef.current?.parentElement;
|
|
163
|
+
if (parent && parent.contains(t))
|
|
164
|
+
return;
|
|
165
|
+
closePicker();
|
|
166
|
+
};
|
|
167
|
+
document.addEventListener('mousedown', onDocMouseDown);
|
|
168
|
+
return () => document.removeEventListener('mousedown', onDocMouseDown);
|
|
169
|
+
}, [picker.open, closePicker]);
|
|
170
|
+
return (_jsxs("div", { className: "relative", children: [_jsx("textarea", { ref: textareaRef, value: value, onChange: handleChange, onKeyDown: handleKeyDown, onClick: handleClick, placeholder: placeholder, rows: rows, className: className, "aria-autocomplete": "list", "aria-controls": picker.open ? pickerId : undefined, "aria-expanded": picker.open, "data-testid": testId }), picker.open && (_jsx("div", { id: pickerId, role: "listbox", "aria-label": "Mention a teammate", className: "absolute top-full left-0 z-30 mt-1 max-h-56 w-64 overflow-y-auto rounded border border-gray-200 bg-white shadow-lg", "data-testid": testId ? `${testId}-picker` : 'mention-picker', children: picker.users.length === 0 ? (_jsx("p", { className: "px-3 py-2 text-xs text-gray-500", children: picker.query ? 'No matches' : 'Type a name…' })) : (_jsx("ul", { children: picker.users.map((u, idx) => (_jsx("li", { children: _jsxs("button", { type: "button", role: "option", "aria-selected": idx === picker.highlight, onMouseDown: (e) => {
|
|
171
|
+
// mousedown (not click) — prevents the
|
|
172
|
+
// textarea from losing focus before we
|
|
173
|
+
// commit, which would otherwise hide the
|
|
174
|
+
// selection and feel janky.
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
commitMention(u);
|
|
177
|
+
}, onMouseEnter: () => setPicker((s) => ({ ...s, highlight: idx })), className: `block w-full px-3 py-1.5 text-left text-xs ${idx === picker.highlight
|
|
178
|
+
? 'bg-blue-50 text-blue-900'
|
|
179
|
+
: 'text-gray-800 hover:bg-gray-50'}`, "data-testid": testId ? `${testId}-picker-row-${u.id}` : `mention-row-${u.id}`, children: [_jsx("div", { className: "font-medium", children: formatLabel(u) }), _jsx("div", { className: "text-[10px] text-gray-500", children: u.email })] }) }, u.id))) })) }))] }));
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=MentionableTextarea.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MentionableTextarea.js","sourceRoot":"","sources":["../../src/components/MentionableTextarea.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { type Notification, type NotificationOutcome } from '../lib/notifications-client.js';
|
|
2
|
+
import { type Notification, type NotificationOutcome, type NotificationStreamHandle, type NotificationStreamOptions } from '../lib/notifications-client.js';
|
|
3
3
|
/**
|
|
4
4
|
* Top-bar notification bell + dropdown panel. Slice 6 of Phase 3
|
|
5
5
|
* surfaces every collaboration event that affects the current user —
|
|
@@ -8,19 +8,25 @@ import { type Notification, type NotificationOutcome } from '../lib/notification
|
|
|
8
8
|
*
|
|
9
9
|
* Design choices:
|
|
10
10
|
*
|
|
11
|
-
* - **
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* - **SSE push, with poll fallback (Phase 7).** A persistent
|
|
12
|
+
* `EventSource` subscribes to `/api/cms/notifications/stream` for
|
|
13
|
+
* instant delivery; new notifications bump the badge immediately
|
|
14
|
+
* instead of after the next 30 s poll tick. The poll loop is kept
|
|
15
|
+
* alive at a back-pressure-friendly cadence (default 60 s — twice
|
|
16
|
+
* the legacy 30 s value because SSE handles the common case) and
|
|
17
|
+
* acts as a backstop for:
|
|
18
|
+
* 1. multi-node deployments where the in-process bus only
|
|
19
|
+
* publishes to subscribers on the same node, and
|
|
20
|
+
* 2. proxies / browsers that silently drop the EventSource
|
|
21
|
+
* without firing `onerror`.
|
|
17
22
|
* - **Optimistic read state.** When the user clicks a notification we
|
|
18
23
|
* mark it read locally before the server confirms — so the bell
|
|
19
24
|
* counter feels instant. Server failures roll the state back via
|
|
20
25
|
* the `onError` toast.
|
|
21
26
|
* - **API injection.** The dependency-injection seam matches the rest
|
|
22
27
|
* of the admin (`CommentSidePanel.api`) so unit tests can substitute
|
|
23
|
-
* in-memory fakes
|
|
28
|
+
* in-memory fakes — including the EventSource subscriber, which
|
|
29
|
+
* isn't available in JSDOM by default.
|
|
24
30
|
*/
|
|
25
31
|
export interface NotificationsApi {
|
|
26
32
|
list: (opts: {
|
|
@@ -29,9 +35,24 @@ export interface NotificationsApi {
|
|
|
29
35
|
unread: () => Promise<NotificationOutcome<number>>;
|
|
30
36
|
markRead: (id: string) => Promise<NotificationOutcome<Notification>>;
|
|
31
37
|
markAllRead: () => Promise<NotificationOutcome<number>>;
|
|
38
|
+
/**
|
|
39
|
+
* Open a per-user SSE subscription. Implementations MUST always
|
|
40
|
+
* return a handle — even in environments without `EventSource`
|
|
41
|
+
* (SSR, JSDOM without a polyfill), where a no-op `{ close() {} }`
|
|
42
|
+
* is the right answer. The bell's effect calls `handle.close()`
|
|
43
|
+
* unconditionally on unmount; a `null` return would crash.
|
|
44
|
+
* Omitting the member entirely is the supported way to opt out of
|
|
45
|
+
* SSE (the bell then runs poll-only).
|
|
46
|
+
*/
|
|
47
|
+
subscribe?: (opts: NotificationStreamOptions) => NotificationStreamHandle;
|
|
32
48
|
}
|
|
33
49
|
export interface NotificationBellProps {
|
|
34
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Polling cadence in milliseconds for the fallback loop. Default
|
|
52
|
+
* 60,000 (twice the pre-SSE value — push handles the common case).
|
|
53
|
+
* Pass 0 to disable polling entirely (relies on SSE only — only
|
|
54
|
+
* safe on single-node deployments).
|
|
55
|
+
*/
|
|
35
56
|
pollIntervalMs?: number;
|
|
36
57
|
/** Optional click hook — called with the notification when a row is clicked. */
|
|
37
58
|
onSelect?: (notification: Notification) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationBell.d.ts","sourceRoot":"","sources":["../../src/components/NotificationBell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAA;AAEvE,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"NotificationBell.d.ts","sourceRoot":"","sources":["../../src/components/NotificationBell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAA;AAEvE,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAO/B,MAAM,gCAAgC,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,CAAC,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;IACvF,MAAM,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAA;IACpE,WAAW,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAA;IACvD;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,yBAAyB,KAAK,wBAAwB,CAAA;CAC1E;AAUD,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,YAAY,KAAK,IAAI,CAAA;IAC/C,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,uCAAuC;IACvC,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,cAAuB,EACvB,QAAQ,EACR,OAAO,EACP,GAAgB,EAChB,SAAS,GACV,EAAE,qBAAqB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyM3C"}
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { listNotifications as defaultListNotifications, markAllNotificationsRead as defaultMarkAllRead, markNotificationRead as defaultMarkRead, notificationSummary, unreadCount as defaultUnreadCount, } from '../lib/notifications-client.js';
|
|
4
|
+
import { listNotifications as defaultListNotifications, markAllNotificationsRead as defaultMarkAllRead, markNotificationRead as defaultMarkRead, notificationSummary, subscribeNotificationStream as defaultSubscribeStream, unreadCount as defaultUnreadCount, } from '../lib/notifications-client.js';
|
|
5
5
|
const defaultApi = {
|
|
6
6
|
list: (opts) => defaultListNotifications(opts),
|
|
7
7
|
unread: () => defaultUnreadCount(),
|
|
8
8
|
markRead: (id) => defaultMarkRead(id),
|
|
9
9
|
markAllRead: () => defaultMarkAllRead(),
|
|
10
|
+
subscribe: (opts) => defaultSubscribeStream(opts),
|
|
10
11
|
};
|
|
11
|
-
export function NotificationBell({ pollIntervalMs =
|
|
12
|
+
export function NotificationBell({ pollIntervalMs = 60_000, onSelect, onError, api = defaultApi, className, }) {
|
|
12
13
|
const [open, setOpen] = useState(false);
|
|
13
14
|
const [unread, setUnread] = useState(0);
|
|
14
15
|
const [items, setItems] = useState([]);
|
|
15
16
|
const [loading, setLoading] = useState(false);
|
|
16
17
|
const mountedRef = useRef(true);
|
|
18
|
+
// Set of notification IDs we've already rendered. The SSE handler
|
|
19
|
+
// consults this synchronously so duplicate deliveries (REST refresh
|
|
20
|
+
// + SSE push, or two SSE retries) don't double-bump the badge.
|
|
21
|
+
// Kept in sync with `items` via an effect — the items state is the
|
|
22
|
+
// source of truth; this ref is a derived O(1) lookup index.
|
|
23
|
+
const seenIdsRef = useRef(new Set());
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
seenIdsRef.current = new Set(items.map((n) => n.id));
|
|
26
|
+
}, [items]);
|
|
17
27
|
useEffect(() => {
|
|
18
28
|
return () => {
|
|
19
29
|
mountedRef.current = false;
|
|
@@ -50,6 +60,37 @@ export function NotificationBell({ pollIntervalMs = 30_000, onSelect, onError, a
|
|
|
50
60
|
const id = setInterval(() => void refreshUnread(), pollIntervalMs);
|
|
51
61
|
return () => clearInterval(id);
|
|
52
62
|
}, [refreshUnread, pollIntervalMs]);
|
|
63
|
+
// SSE push subscription. Bumps the badge + prepends the new row to
|
|
64
|
+
// the open dropdown the instant the server publishes. Falls back
|
|
65
|
+
// silently when the api/runtime doesn't support EventSource — the
|
|
66
|
+
// poll loop above keeps the bell correct in that case.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!api.subscribe)
|
|
69
|
+
return;
|
|
70
|
+
const handle = api.subscribe({
|
|
71
|
+
onNotification: (n) => {
|
|
72
|
+
if (!mountedRef.current)
|
|
73
|
+
return;
|
|
74
|
+
// Defensive: SSE events can race with REST refreshes (or with
|
|
75
|
+
// their own retries). The seen-id ref is updated
|
|
76
|
+
// synchronously so a second delivery of the same row is a
|
|
77
|
+
// no-op for BOTH `items` and the unread badge — guarding only
|
|
78
|
+
// setItems would still let the counter drift.
|
|
79
|
+
if (seenIdsRef.current.has(n.id))
|
|
80
|
+
return;
|
|
81
|
+
seenIdsRef.current.add(n.id);
|
|
82
|
+
setItems((arr) => [n, ...arr]);
|
|
83
|
+
if (!n.readAt)
|
|
84
|
+
setUnread((v) => v + 1);
|
|
85
|
+
},
|
|
86
|
+
onError: () => {
|
|
87
|
+
// Don't surface every transient SSE disconnect as a UI error —
|
|
88
|
+
// the browser auto-reconnects, and the poll loop keeps state
|
|
89
|
+
// converging. We deliberately stay quiet here.
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return () => handle.close();
|
|
93
|
+
}, [api]);
|
|
53
94
|
useEffect(() => {
|
|
54
95
|
if (!open)
|
|
55
96
|
return;
|
|
@@ -71,14 +112,25 @@ export function NotificationBell({ pollIntervalMs = 30_000, onSelect, onError, a
|
|
|
71
112
|
onSelect?.(n);
|
|
72
113
|
}, [api, onError, onSelect]);
|
|
73
114
|
const handleMarkAll = useCallback(async () => {
|
|
74
|
-
|
|
115
|
+
// Snapshot ONLY the previously-unread IDs (and their original
|
|
116
|
+
// readAt values, which are null by definition). On rollback we
|
|
117
|
+
// restore those specific rows and leave anything that arrived
|
|
118
|
+
// via SSE while the request was in flight alone — otherwise a
|
|
119
|
+
// failed mark-all-read would drop fresh notifications.
|
|
120
|
+
const previouslyUnreadIds = items.filter((n) => n.readAt === null).map((n) => n.id);
|
|
121
|
+
if (previouslyUnreadIds.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
const ids = new Set(previouslyUnreadIds);
|
|
75
124
|
const now = new Date().toISOString();
|
|
76
|
-
setItems(
|
|
125
|
+
setItems((arr) => arr.map((n) => (ids.has(n.id) ? { ...n, readAt: n.readAt ?? now } : n)));
|
|
77
126
|
setUnread(0);
|
|
78
127
|
const out = await api.markAllRead();
|
|
79
128
|
if (!out.ok) {
|
|
80
129
|
onError?.(out.error);
|
|
81
|
-
|
|
130
|
+
// Functional rollback: only revert the rows we optimistically
|
|
131
|
+
// marked. Anything mid-flight (SSE arrivals, locally-added) is
|
|
132
|
+
// untouched.
|
|
133
|
+
setItems((arr) => arr.map((n) => (ids.has(n.id) ? { ...n, readAt: null } : n)));
|
|
82
134
|
await refreshUnread();
|
|
83
135
|
}
|
|
84
136
|
}, [api, items, onError, refreshUnread]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationBell.js","sourceRoot":"","sources":["../../src/components/NotificationBell.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAc,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEvE,OAAO,
|
|
1
|
+
{"version":3,"file":"NotificationBell.js","sourceRoot":"","sources":["../../src/components/NotificationBell.tsx"],"names":[],"mappings":"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"}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export declare function setApiBase(path: string): void;
|
|
2
|
+
/**
|
|
3
|
+
* Current API base path. Exposed so non-`fetch` consumers (in
|
|
4
|
+
* particular `EventSource` for SSE channels) can resolve URLs against
|
|
5
|
+
* the same prefix configured via `setApiBase()`. Keep this read-only
|
|
6
|
+
* — mutation must always flow through `setApiBase()` so we never have
|
|
7
|
+
* two callers with diverging views of the base.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getApiBase(): string;
|
|
2
10
|
export declare function ensureCsrfToken(): Promise<void>;
|
|
3
11
|
export declare function cmsApi<T = unknown>(endpoint: string, options?: RequestInit): Promise<{
|
|
4
12
|
data?: T;
|
package/dist/lib/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,QAEtC;AAmBD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAcrD;AAcD,wBAAsB,MAAM,CAAC,CAAC,GAAG,OAAO,EACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAsCvD"}
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,QAEtC;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAmBD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAcrD;AAcD,wBAAsB,MAAM,CAAC,CAAC,GAAG,OAAO,EACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAsCvD"}
|
package/dist/lib/api.js
CHANGED
|
@@ -3,6 +3,16 @@ let basePath = DEFAULT_BASE;
|
|
|
3
3
|
export function setApiBase(path) {
|
|
4
4
|
basePath = path;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Current API base path. Exposed so non-`fetch` consumers (in
|
|
8
|
+
* particular `EventSource` for SSE channels) can resolve URLs against
|
|
9
|
+
* the same prefix configured via `setApiBase()`. Keep this read-only
|
|
10
|
+
* — mutation must always flow through `setApiBase()` so we never have
|
|
11
|
+
* two callers with diverging views of the base.
|
|
12
|
+
*/
|
|
13
|
+
export function getApiBase() {
|
|
14
|
+
return basePath;
|
|
15
|
+
}
|
|
6
16
|
function getCsrfToken() {
|
|
7
17
|
if (typeof document === 'undefined')
|
|
8
18
|
return '';
|
package/dist/lib/api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,UAAU,CAAA;AAE/B,IAAI,QAAQ,GAAG,YAAY,CAAA;AAE3B,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,QAAQ,GAAG,IAAI,CAAA;AACjB,CAAC;AAED,SAAS,YAAY;IACnB,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,EAAE,CAAA;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAA;IACrE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;AACzB,CAAC;AAED;;;;;;;;GAQG;AACH,IAAI,YAAY,GAAyB,IAAI,CAAA;AAE7C,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,YAAY,EAAE;QAAE,OAAM;IAC1B,IAAI,YAAY;QAAE,OAAO,YAAY,CAAA;IACrC,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;QACzB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,GAAG,QAAQ,YAAY,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAA;QAClE,CAAC;QAAC,MAAM,CAAC;YACP,oEAAoE;YACpE,mEAAmE;QACrE,CAAC;gBAAS,CAAC;YACT,YAAY,GAAG,IAAI,CAAA;QACrB,CAAC;IACH,CAAC,CAAC,EAAE,CAAA;IACJ,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,SAAS,eAAe;IACtB,IAAI,OAAO,YAAY,KAAK,WAAW;QAAE,OAAO,IAAI,CAAA;IACpD,OAAO,YAAY,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;AAC/C,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB;IACzC,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;IAChC,IAAI,CAAC,MAAM;QAAE,OAAO,QAAQ,CAAA;IAC5B,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IACpD,OAAO,GAAG,QAAQ,GAAG,SAAS,UAAU,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAA;AACtE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,QAAgB,EAChB,UAAuB,EAAE;IAEzB,MAAM,GAAG,GAAG,GAAG,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,EAAE,CAAA;IACvD,MAAM,UAAU,GAAG,OAAO,QAAQ,KAAK,WAAW,IAAI,OAAO,CAAC,IAAI,YAAY,QAAQ,CAAA;IAEtF,MAAM,OAAO,GAA2B;QACtC,GAAG,CAAE,OAAO,CAAC,OAAkC,IAAI,EAAE,CAAC;KACvD,CAAA;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAA;IAC9C,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAA;IACtD,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QAC1C,sEAAsE;QACtE,uEAAuE;QACvE,wEAAwE;QACxE,wEAAwE;QACxE,sBAAsB;QACtB,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACpB,MAAM,eAAe,EAAE,CAAA;QACzB,CAAC;QACD,MAAM,IAAI,GAAG,YAAY,EAAE,CAAA;QAC3B,IAAI,IAAI;YAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAAA;IAC1C,CAAC;IAED,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAA;QAC7E,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAE/C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,mBAAmB,GAAG,CAAC,MAAM,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAA;QACtF,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAA;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;IACnF,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface MentionUser {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
}
|
|
6
|
+
export type MentionSearchOutcome = {
|
|
7
|
+
ok: true;
|
|
8
|
+
result: MentionUser[];
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: string;
|
|
12
|
+
status: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function searchMentionUsers(query: string): Promise<MentionSearchOutcome>;
|
|
15
|
+
/**
|
|
16
|
+
* Description of an active `@`-mention being typed in a textarea / input.
|
|
17
|
+
*
|
|
18
|
+
* - `start` and `end` are character offsets into the field value: the
|
|
19
|
+
* `@` itself sits at `start`, and `end` is the caret position
|
|
20
|
+
* (exclusive end of the query slice).
|
|
21
|
+
* - `query` is the substring between the `@` and the caret — exactly
|
|
22
|
+
* what we should pass to `searchMentionUsers()`.
|
|
23
|
+
*
|
|
24
|
+
* Callers replace `value.slice(start, end)` with the chosen token
|
|
25
|
+
* (`@<userId> `, including the trailing space) to commit a selection.
|
|
26
|
+
*/
|
|
27
|
+
export interface MentionContext {
|
|
28
|
+
start: number;
|
|
29
|
+
end: number;
|
|
30
|
+
query: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Find an active mention at the caret. Mirrors the server-side
|
|
34
|
+
* `extractMentionedUserIds` regex
|
|
35
|
+
* (`/(^|\s)@[a-zA-Z0-9_-]{3,64}\b/g`) so the picker's intuition
|
|
36
|
+
* about what counts as a mention can never disagree with the
|
|
37
|
+
* server's notification routing.
|
|
38
|
+
*
|
|
39
|
+
* Returns `null` whenever:
|
|
40
|
+
* - the caret is at offset 0,
|
|
41
|
+
* - there is no `@` between the caret and the previous whitespace
|
|
42
|
+
* boundary,
|
|
43
|
+
* - the `@` is preceded by a non-whitespace, non-newline character
|
|
44
|
+
* (so an email address mid-word like `foo@bar` does NOT open
|
|
45
|
+
* the picker), or
|
|
46
|
+
* - the candidate query contains a character outside the mention
|
|
47
|
+
* charset (`[a-zA-Z0-9_-]`).
|
|
48
|
+
*
|
|
49
|
+
* The picker DOES open with a 0–2 character query — that's the
|
|
50
|
+
* "you've started typing a mention" UX. But it can NEVER commit
|
|
51
|
+
* such a token: `insertMentionToken` only fires for a `MentionUser`
|
|
52
|
+
* which always has an id of at least 3 chars, matching the server
|
|
53
|
+
* regex's min-length requirement.
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectMentionContext(value: string, caret: number): MentionContext | null;
|
|
56
|
+
/**
|
|
57
|
+
* Apply a mention selection to a text value. Returns the new value
|
|
58
|
+
* plus the caret position where the next keystroke should land.
|
|
59
|
+
*
|
|
60
|
+
* Inserts `@<userId> ` (trailing space) so the user can keep typing
|
|
61
|
+
* without manually moving the cursor past the token. If the caret
|
|
62
|
+
* already sits before a space, we don't insert a duplicate space.
|
|
63
|
+
*/
|
|
64
|
+
export interface MentionInsertion {
|
|
65
|
+
value: string;
|
|
66
|
+
caret: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function insertMentionToken(value: string, context: MentionContext, userId: string): MentionInsertion;
|
|
69
|
+
//# sourceMappingURL=mention-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mention-picker.d.ts","sourceRoot":"","sources":["../../src/lib/mention-picker.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,EAAE,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAerF;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAiCxF;AAwBD;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,MAAM,GACb,gBAAgB,CAclB"}
|