@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.
Files changed (51) hide show
  1. package/dist/__tests__/components/comment-side-panel.test.js +93 -0
  2. package/dist/__tests__/components/comment-side-panel.test.js.map +1 -1
  3. package/dist/__tests__/components/mentionable-textarea.test.d.ts +2 -0
  4. package/dist/__tests__/components/mentionable-textarea.test.d.ts.map +1 -0
  5. package/dist/__tests__/components/mentionable-textarea.test.js +190 -0
  6. package/dist/__tests__/components/mentionable-textarea.test.js.map +1 -0
  7. package/dist/__tests__/components/notification-bell.test.js +146 -0
  8. package/dist/__tests__/components/notification-bell.test.js.map +1 -1
  9. package/dist/__tests__/lib/mention-picker.test.d.ts +2 -0
  10. package/dist/__tests__/lib/mention-picker.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/mention-picker.test.js +122 -0
  12. package/dist/__tests__/lib/mention-picker.test.js.map +1 -0
  13. package/dist/__tests__/lib/notifications-client.test.js +129 -1
  14. package/dist/__tests__/lib/notifications-client.test.js.map +1 -1
  15. package/dist/actuate-admin.css +1 -1
  16. package/dist/components/CommentSidePanel.d.ts +9 -1
  17. package/dist/components/CommentSidePanel.d.ts.map +1 -1
  18. package/dist/components/CommentSidePanel.js +8 -7
  19. package/dist/components/CommentSidePanel.js.map +1 -1
  20. package/dist/components/MentionableTextarea.d.ts +68 -0
  21. package/dist/components/MentionableTextarea.d.ts.map +1 -0
  22. package/dist/components/MentionableTextarea.js +181 -0
  23. package/dist/components/MentionableTextarea.js.map +1 -0
  24. package/dist/components/NotificationBell.d.ts +30 -9
  25. package/dist/components/NotificationBell.d.ts.map +1 -1
  26. package/dist/components/NotificationBell.js +57 -5
  27. package/dist/components/NotificationBell.js.map +1 -1
  28. package/dist/lib/api.d.ts +8 -0
  29. package/dist/lib/api.d.ts.map +1 -1
  30. package/dist/lib/api.js +10 -0
  31. package/dist/lib/api.js.map +1 -1
  32. package/dist/lib/mention-picker.d.ts +69 -0
  33. package/dist/lib/mention-picker.d.ts.map +1 -0
  34. package/dist/lib/mention-picker.js +138 -0
  35. package/dist/lib/mention-picker.js.map +1 -0
  36. package/dist/lib/notifications-client.d.ts +53 -0
  37. package/dist/lib/notifications-client.d.ts.map +1 -1
  38. package/dist/lib/notifications-client.js +89 -1
  39. package/dist/lib/notifications-client.js.map +1 -1
  40. package/package.json +2 -2
  41. package/src/__tests__/components/comment-side-panel.test.tsx +134 -0
  42. package/src/__tests__/components/mentionable-textarea.test.tsx +226 -0
  43. package/src/__tests__/components/notification-bell.test.tsx +167 -1
  44. package/src/__tests__/lib/mention-picker.test.ts +145 -0
  45. package/src/__tests__/lib/notifications-client.test.ts +145 -1
  46. package/src/components/CommentSidePanel.tsx +37 -14
  47. package/src/components/MentionableTextarea.tsx +328 -0
  48. package/src/components/NotificationBell.tsx +84 -12
  49. package/src/lib/api.ts +11 -0
  50. package/src/lib/mention-picker.ts +180 -0
  51. package/src/lib/notifications-client.ts +147 -1
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Mention picker client logic + REST helper.
3
+ *
4
+ * Two responsibilities, deliberately separated so each can be unit
5
+ * tested without rendering React or hitting fetch:
6
+ *
7
+ * 1. `searchMentionUsers(query)` — typed wrapper over the new
8
+ * `GET /api/cms/users/search` endpoint introduced in Phase 7.
9
+ * Returns up to 10 `{ id, name, email }` rows matching the query.
10
+ * 2. `detectMentionContext(value, caret)` — pure parser that scans a
11
+ * text input value backwards from the caret to decide whether the
12
+ * user is currently typing a `@`-mention, and if so what the
13
+ * query fragment is. Returns `null` when no active mention.
14
+ *
15
+ * Wire format note: the picker INSERTS `@<userId>` tokens at the
16
+ * cursor (matching the server-side `extractMentionedUserIds` regex
17
+ * in `cms-core/realtime/notifications.ts`). The user sees the chosen
18
+ * user's *name* in the picker but the textarea always holds the id.
19
+ * Rendering names back in the comment view is a separate slice that
20
+ * can ship later without breaking the wire format.
21
+ */
22
+ import { cmsApi } from './api.js';
23
+ export async function searchMentionUsers(query) {
24
+ const trimmed = query.trim();
25
+ if (trimmed.length === 0) {
26
+ // Mirror the server's contract: an empty query returns an empty
27
+ // array. We short-circuit here so the picker doesn't spend a
28
+ // round-trip on a guaranteed-empty response.
29
+ return { ok: true, result: [] };
30
+ }
31
+ const res = await cmsApi(`/users/search?q=${encodeURIComponent(trimmed)}`, {
32
+ method: 'GET',
33
+ });
34
+ if (res.error || !res.data) {
35
+ return { ok: false, error: res.error ?? `Request failed (${res.status})`, status: res.status };
36
+ }
37
+ return { ok: true, result: res.data };
38
+ }
39
+ /**
40
+ * Find an active mention at the caret. Mirrors the server-side
41
+ * `extractMentionedUserIds` regex
42
+ * (`/(^|\s)@[a-zA-Z0-9_-]{3,64}\b/g`) so the picker's intuition
43
+ * about what counts as a mention can never disagree with the
44
+ * server's notification routing.
45
+ *
46
+ * Returns `null` whenever:
47
+ * - the caret is at offset 0,
48
+ * - there is no `@` between the caret and the previous whitespace
49
+ * boundary,
50
+ * - the `@` is preceded by a non-whitespace, non-newline character
51
+ * (so an email address mid-word like `foo@bar` does NOT open
52
+ * the picker), or
53
+ * - the candidate query contains a character outside the mention
54
+ * charset (`[a-zA-Z0-9_-]`).
55
+ *
56
+ * The picker DOES open with a 0–2 character query — that's the
57
+ * "you've started typing a mention" UX. But it can NEVER commit
58
+ * such a token: `insertMentionToken` only fires for a `MentionUser`
59
+ * which always has an id of at least 3 chars, matching the server
60
+ * regex's min-length requirement.
61
+ */
62
+ export function detectMentionContext(value, caret) {
63
+ if (typeof value !== 'string')
64
+ return null;
65
+ if (caret <= 0 || caret > value.length)
66
+ return null;
67
+ // Walk back from the caret looking for a `@`. Stop early on any
68
+ // character that is neither a valid mention char nor `@`. This
69
+ // means a typed space, newline, punctuation, etc. terminates the
70
+ // mention candidate the user was building.
71
+ let i = caret - 1;
72
+ while (i >= 0) {
73
+ const ch = value.charCodeAt(i);
74
+ if (ch === 64 /* @ */)
75
+ break;
76
+ if (!isMentionChar(ch))
77
+ return null;
78
+ i -= 1;
79
+ }
80
+ if (i < 0)
81
+ return null;
82
+ // The `@` must sit at the start of the field or be preceded by
83
+ // whitespace / newline. Otherwise we're mid-word (think `foo@bar`),
84
+ // which is not a mention.
85
+ if (i > 0) {
86
+ const prev = value.charCodeAt(i - 1);
87
+ if (!isBoundary(prev))
88
+ return null;
89
+ }
90
+ const query = value.slice(i + 1, caret);
91
+ // Server caps queries at 64 chars; the picker simply stops
92
+ // suggesting once the typed query exceeds that — extra typing
93
+ // closes the picker so the user can complete an email or other
94
+ // long token without staring at a dead dropdown.
95
+ if (query.length > 64)
96
+ return null;
97
+ return { start: i, end: caret, query };
98
+ }
99
+ function isMentionChar(code) {
100
+ // [a-zA-Z0-9_-] — keep this in lock-step with the regex in
101
+ // `cms-core/realtime/notifications.ts:extractMentionedUserIds`.
102
+ if (code === 95 /* _ */ || code === 45 /* - */)
103
+ return true;
104
+ if (code >= 48 && code <= 57)
105
+ return true; // 0-9
106
+ if (code >= 65 && code <= 90)
107
+ return true; // A-Z
108
+ if (code >= 97 && code <= 122)
109
+ return true; // a-z
110
+ return false;
111
+ }
112
+ function isBoundary(code) {
113
+ // ASCII space, tab, line-feed, or carriage-return. We deliberately
114
+ // do NOT widen this to the full Unicode whitespace class — the
115
+ // server-side `extractMentionedUserIds` regex in
116
+ // `cms-core/realtime/notifications.ts` uses `\s` which DOES match
117
+ // Unicode whitespace, but its `^` anchor means the only realistic
118
+ // mid-string boundaries in practice are these four ASCII chars
119
+ // (a CMS comment body containing U+00A0 NBSP before an `@` would
120
+ // be both unusual and not worth opening the picker for).
121
+ return code === 32 || code === 9 || code === 10 || code === 13;
122
+ }
123
+ export function insertMentionToken(value, context, userId) {
124
+ const hasTrailingSpace = value.charAt(context.end) === ' ';
125
+ // If the user already has a space immediately after the mention
126
+ // region, reuse it (don't double-space) but still advance the
127
+ // caret past that space so the next keystroke continues naturally
128
+ // rather than landing back in front of the space.
129
+ const trailingSpace = hasTrailingSpace ? '' : ' ';
130
+ const token = `@${userId}${trailingSpace}`;
131
+ const before = value.slice(0, context.start);
132
+ const after = value.slice(context.end);
133
+ return {
134
+ value: `${before}${token}${after}`,
135
+ caret: context.start + token.length + (hasTrailingSpace ? 1 : 0),
136
+ };
137
+ }
138
+ //# sourceMappingURL=mention-picker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mention-picker.js","sourceRoot":"","sources":["../../src/lib/mention-picker.ts"],"names":[],"mappings":"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"}
@@ -58,6 +58,59 @@ export declare function listNotifications(opts?: ListNotificationsOptions): Prom
58
58
  export declare function unreadCount(): Promise<NotificationOutcome<number>>;
59
59
  export declare function markNotificationRead(id: string): Promise<NotificationOutcome<Notification>>;
60
60
  export declare function markAllNotificationsRead(): Promise<NotificationOutcome<number>>;
61
+ /**
62
+ * Subscribe to the per-user SSE push channel that ships notifications
63
+ * the instant they're persisted server-side. Replaces the 30-second
64
+ * poll loop in the notification bell.
65
+ *
66
+ * The browser's native `EventSource` handles reconnection for us — on
67
+ * a server-initiated close (the stream caps at ~4 minutes by design),
68
+ * the connection re-opens automatically. We DO NOT abort the poll
69
+ * fallback in callers: SSE is a latency-win, the poll is what keeps
70
+ * multi-node deployments correct (each node only pushes to its own
71
+ * subscribers).
72
+ *
73
+ * Returns a `close()` function that callers MUST invoke on unmount —
74
+ * otherwise the browser keeps the connection open and we leak event
75
+ * listeners.
76
+ *
77
+ * `EventSource` itself isn't available in the JSDOM test environment;
78
+ * callers can inject a `factory` to substitute a test double. In
79
+ * production we fall back to the global `EventSource` when no factory
80
+ * is supplied.
81
+ */
82
+ export interface NotificationStreamOptions {
83
+ /** Called for every `event: notification` frame. */
84
+ onNotification: (n: Notification) => void;
85
+ /** Called once on the `event: ready` frame. */
86
+ onReady?: (payload: {
87
+ userId: string;
88
+ }) => void;
89
+ /** Called on connection errors. Useful for falling back to the poll loop. */
90
+ onError?: (event: Event) => void;
91
+ /**
92
+ * Optional factory so unit tests can inject a fake `EventSource`,
93
+ * or a null sentinel when the environment lacks one (SSR, JSDOM
94
+ * without a polyfill). Production code can rely on the default
95
+ * (global `EventSource`).
96
+ */
97
+ factory?: (url: string) => EventSourceLike | null;
98
+ }
99
+ /**
100
+ * Minimal subset of the `EventSource` interface our subscriber relies
101
+ * on. Browsers ship the full DOM type; the admin only needs add/remove
102
+ * event listeners plus `close()`.
103
+ */
104
+ export interface EventSourceLike {
105
+ addEventListener: (type: string, listener: (event: MessageEvent | Event) => void) => void;
106
+ removeEventListener?: (type: string, listener: (event: MessageEvent | Event) => void) => void;
107
+ close: () => void;
108
+ }
109
+ export interface NotificationStreamHandle {
110
+ /** Tear down the EventSource and stop receiving events. Idempotent. */
111
+ close: () => void;
112
+ }
113
+ export declare function subscribeNotificationStream(opts: NotificationStreamOptions): NotificationStreamHandle;
61
114
  /**
62
115
  * Cheap single-line summary for the bell list — the server gives us a
63
116
  * structured payload, but the dropdown wants one human-readable string
@@ -1 +1 @@
1
- {"version":3,"file":"notifications-client.d.ts","sourceRoot":"","sources":["../../src/lib/notifications-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,kBAAkB,GAAG,iBAAiB,CAAA;AAEvF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,kBAAkB,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAA;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,mBAAmB,GAC3B,mBAAmB,GACnB,sBAAsB,GACtB,qBAAqB,CAAA;AAEzB,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,gBAAgB,CAAA;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,mBAAmB,CAAA;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GACvB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhD,MAAM,WAAW,wBAAwB;IACvC,yEAAyE;IACzE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAyBD,wBAAsB,iBAAiB,CACrC,IAAI,GAAE,wBAA6B,GAClC,OAAO,CAAC,mBAAmB,CAAC,YAAY,EAAE,CAAC,CAAC,CAK9C;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAQxE;AAED,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAOjG;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CASrF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,YAAY,GAAG,MAAM,CAYtE"}
1
+ {"version":3,"file":"notifications-client.d.ts","sourceRoot":"","sources":["../../src/lib/notifications-client.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@
10
10
  * shape here to avoid taking a hard dependency on the server module
11
11
  * (which carries Prisma types that don't belong in the admin bundle).
12
12
  */
13
- import { cmsApi } from './api.js';
13
+ import { cmsApi, getApiBase } from './api.js';
14
14
  function failure(error, status) {
15
15
  return { ok: false, error, status };
16
16
  }
@@ -63,6 +63,94 @@ export async function markAllNotificationsRead() {
63
63
  }
64
64
  return { ok: true, result: res.data.count };
65
65
  }
66
+ export function subscribeNotificationStream(opts) {
67
+ // The `cmsApi()` helper handles base-path / origin resolution for
68
+ // fetch calls, but EventSource takes a raw URL. We mirror its
69
+ // logic locally — `/api/cms/notifications/stream` is correct for
70
+ // both the local dev server and any production deploy that mounts
71
+ // the CMS API at the default prefix. Custom prefixes are handled by
72
+ // the consumer override (factory).
73
+ // Resolve against `setApiBase()` so a consumer mounting the CMS at
74
+ // a non-default prefix (e.g. `/cms-api`) still hits the right SSE
75
+ // endpoint — `EventSource` doesn't go through `cmsApi()`, so we
76
+ // can't rely on its url-construction logic.
77
+ const url = `${getApiBase()}/notifications/stream`;
78
+ const make = opts.factory ?? defaultEventSourceFactory;
79
+ const source = make(url);
80
+ if (!source) {
81
+ // No EventSource available (SSR? JSDOM without polyfill? Or the
82
+ // injected factory deliberately returns null in tests). Return a
83
+ // no-op handle so callers can compose without conditional logic.
84
+ return { close: () => { } };
85
+ }
86
+ let closed = false;
87
+ const onReady = (event) => {
88
+ if (!opts.onReady)
89
+ return;
90
+ const data = parseEventData(event);
91
+ if (data && typeof data.userId === 'string') {
92
+ opts.onReady({ userId: data.userId });
93
+ }
94
+ };
95
+ const onNotification = (event) => {
96
+ const data = parseEventData(event);
97
+ if (data && isNotification(data)) {
98
+ opts.onNotification(data);
99
+ }
100
+ };
101
+ const onError = (event) => {
102
+ opts.onError?.(event);
103
+ };
104
+ source.addEventListener('ready', onReady);
105
+ source.addEventListener('notification', onNotification);
106
+ source.addEventListener('error', onError);
107
+ return {
108
+ close: () => {
109
+ if (closed)
110
+ return;
111
+ closed = true;
112
+ source.removeEventListener?.('ready', onReady);
113
+ source.removeEventListener?.('notification', onNotification);
114
+ source.removeEventListener?.('error', onError);
115
+ source.close();
116
+ },
117
+ };
118
+ }
119
+ function defaultEventSourceFactory(url) {
120
+ if (typeof EventSource === 'undefined')
121
+ return null;
122
+ // `withCredentials: true` so the admin session cookie travels with
123
+ // the connection upgrade — every notification stream requires auth
124
+ // and the same-origin cookie is the only credential we trust here.
125
+ return new EventSource(url, { withCredentials: true });
126
+ }
127
+ function parseEventData(event) {
128
+ // EventSource emits MessageEvents with `data: string`. Plain
129
+ // `Event` (e.g. the `error` event) has no payload; ignore.
130
+ const data = event.data;
131
+ if (typeof data !== 'string')
132
+ return null;
133
+ try {
134
+ return JSON.parse(data);
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function isNotification(value) {
141
+ if (!value || typeof value !== 'object')
142
+ return false;
143
+ const v = value;
144
+ // Defensive: don't trust the wire blindly — verify the discriminant
145
+ // properties our consumers will read. A drift in the server-side
146
+ // wire format must surface as "missing fields" in the test suite,
147
+ // not as runtime errors deep in the bell's render path.
148
+ return (typeof v.id === 'string' &&
149
+ typeof v.userId === 'string' &&
150
+ typeof v.createdAt === 'string' &&
151
+ !!v.payload &&
152
+ typeof v.payload.kind === 'string');
153
+ }
66
154
  /**
67
155
  * Cheap single-line summary for the bell list — the server gives us a
68
156
  * structured payload, but the dropdown wants one human-readable string
@@ -1 +1 @@
1
- {"version":3,"file":"notifications-client.js","sourceRoot":"","sources":["../../src/lib/notifications-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"notifications-client.js","sourceRoot":"","sources":["../../src/lib/notifications-client.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-admin",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",
@@ -87,7 +87,7 @@
87
87
  "tailwindcss": "^4.0.0",
88
88
  "typescript": "^5.7.0",
89
89
  "vitest": "^3.0.0",
90
- "@actuate-media/cms-core": "0.22.0",
90
+ "@actuate-media/cms-core": "0.25.0",
91
91
  "@actuate-media/component-blocks": "0.2.0"
92
92
  },
93
93
  "scripts": {
@@ -341,3 +341,137 @@ describe('CommentSidePanel — reply, edit, resolve, delete', () => {
341
341
  expect(onActive).toHaveBeenCalledWith('c1')
342
342
  })
343
343
  })
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Mention picker integration — Phase 7 carry-over
347
+ // ---------------------------------------------------------------------------
348
+
349
+ describe('CommentSidePanel — mention picker integration', () => {
350
+ /**
351
+ * Drive a `<textarea>` the same way the picker test does. The
352
+ * comment panel's three composer surfaces (new thread, reply,
353
+ * edit) are all `<MentionableTextarea>` now, so this helper works
354
+ * uniformly across them.
355
+ */
356
+ async function typeAt(el: HTMLTextAreaElement, value: string, caret = value.length) {
357
+ await act(async () => {
358
+ el.focus()
359
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set
360
+ if (setter) setter.call(el, value)
361
+ else el.value = value
362
+ el.setSelectionRange(caret, caret)
363
+ el.dispatchEvent(new Event('input', { bubbles: true }))
364
+ })
365
+ }
366
+
367
+ function fakeMentionSearch() {
368
+ return vi.fn(async (q: string) => {
369
+ const all = [
370
+ { id: 'u_alice', name: 'Alice Anderson', email: 'alice@example.com' },
371
+ { id: 'u_bob', name: 'Bob Brown', email: 'bob@example.com' },
372
+ ]
373
+ const ql = q.toLowerCase()
374
+ return all.filter(
375
+ (u) => u.name.toLowerCase().includes(ql) || u.email.toLowerCase().includes(ql),
376
+ )
377
+ })
378
+ }
379
+
380
+ it('opens the mention picker from the new-thread composer and submits @<userId>', async () => {
381
+ const fake = createFakeApi()
382
+ const mentionSearch = fakeMentionSearch()
383
+ render(
384
+ <CommentSidePanel
385
+ documentId="doc-1"
386
+ currentUserId="current"
387
+ api={fake.api}
388
+ mentionSearch={mentionSearch}
389
+ />,
390
+ )
391
+ await waitFor(() => expect(screen.getByText('No comments yet.')).toBeTruthy())
392
+
393
+ const composer = screen.getByTestId('comment-composer') as HTMLTextAreaElement
394
+ await typeAt(composer, '@ali')
395
+ await waitFor(() => expect(screen.getByTestId('comment-composer-picker')).toBeTruthy())
396
+ await act(async () => {
397
+ fireEvent.mouseDown(screen.getByTestId('comment-composer-picker-row-u_alice'))
398
+ })
399
+ expect((composer as HTMLTextAreaElement).value).toBe('@u_alice ')
400
+
401
+ // Continue typing then submit — the resulting comment body must
402
+ // carry the `@<userId>` token so `extractMentionedUserIds` picks
403
+ // it up server-side.
404
+ await typeAt(composer, '@u_alice please review')
405
+ await act(async () => {
406
+ fireEvent.click(screen.getByTestId('comment-submit'))
407
+ })
408
+ expect(fake.api.create).toHaveBeenCalledWith(
409
+ expect.objectContaining({ body: '@u_alice please review' }),
410
+ )
411
+ })
412
+
413
+ it('opens the mention picker from a reply input and inserts @<userId>', async () => {
414
+ const fake = createFakeApi()
415
+ fake.rows.push(comment({ id: 'root', body: 'root' }))
416
+ const mentionSearch = fakeMentionSearch()
417
+ render(
418
+ <CommentSidePanel
419
+ documentId="doc-1"
420
+ currentUserId="current"
421
+ api={fake.api}
422
+ mentionSearch={mentionSearch}
423
+ />,
424
+ )
425
+ await waitFor(() => expect(screen.getByTestId('comment-thread-root')).toBeTruthy())
426
+
427
+ const reply = screen.getByTestId('reply-input-root') as HTMLTextAreaElement
428
+ await typeAt(reply, '@bo')
429
+ await waitFor(() => expect(screen.getByTestId('reply-input-root-picker')).toBeTruthy())
430
+ fireEvent.keyDown(reply, { key: 'Enter' })
431
+ await waitFor(() => expect(reply.value).toBe('@u_bob '))
432
+ })
433
+
434
+ it('opens the mention picker from the edit-textarea while editing a comment', async () => {
435
+ const fake = createFakeApi()
436
+ fake.rows.push(comment({ id: 'c1', userId: 'current', body: 'before' }))
437
+ const mentionSearch = fakeMentionSearch()
438
+ render(
439
+ <CommentSidePanel
440
+ documentId="doc-1"
441
+ currentUserId="current"
442
+ api={fake.api}
443
+ mentionSearch={mentionSearch}
444
+ />,
445
+ )
446
+ await waitFor(() => expect(screen.getByTestId('comment-thread-c1')).toBeTruthy())
447
+ fireEvent.click(screen.getByTestId('edit-c1'))
448
+ const edit = screen.getByTestId('edit-textarea-c1') as HTMLTextAreaElement
449
+ await typeAt(edit, '@al')
450
+ await waitFor(() => expect(screen.getByTestId('edit-textarea-c1-picker')).toBeTruthy())
451
+ await act(async () => {
452
+ fireEvent.mouseDown(screen.getByTestId('edit-textarea-c1-picker-row-u_alice'))
453
+ })
454
+ expect(edit.value).toBe('@u_alice ')
455
+ })
456
+
457
+ it('escape closes the picker without committing a selection', async () => {
458
+ const fake = createFakeApi()
459
+ const mentionSearch = fakeMentionSearch()
460
+ render(
461
+ <CommentSidePanel
462
+ documentId="doc-1"
463
+ currentUserId="current"
464
+ api={fake.api}
465
+ mentionSearch={mentionSearch}
466
+ />,
467
+ )
468
+ await waitFor(() => expect(screen.getByText('No comments yet.')).toBeTruthy())
469
+
470
+ const composer = screen.getByTestId('comment-composer') as HTMLTextAreaElement
471
+ await typeAt(composer, '@al')
472
+ await waitFor(() => expect(screen.getByTestId('comment-composer-picker')).toBeTruthy())
473
+ fireEvent.keyDown(composer, { key: 'Escape' })
474
+ expect(screen.queryByTestId('comment-composer-picker')).toBeNull()
475
+ expect((composer as HTMLTextAreaElement).value).toBe('@al')
476
+ })
477
+ })