@actuate-media/cms-admin 0.12.0 → 0.13.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-mark.test.d.ts +2 -0
- package/dist/__tests__/components/comment-mark.test.d.ts.map +1 -0
- package/dist/__tests__/components/comment-mark.test.js +89 -0
- package/dist/__tests__/components/comment-mark.test.js.map +1 -0
- package/dist/__tests__/components/comment-side-panel.test.d.ts +2 -0
- package/dist/__tests__/components/comment-side-panel.test.d.ts.map +1 -0
- package/dist/__tests__/components/comment-side-panel.test.js +291 -0
- package/dist/__tests__/components/comment-side-panel.test.js.map +1 -0
- package/dist/__tests__/components/notification-bell.test.d.ts +2 -0
- package/dist/__tests__/components/notification-bell.test.d.ts.map +1 -0
- package/dist/__tests__/components/notification-bell.test.js +346 -0
- package/dist/__tests__/components/notification-bell.test.js.map +1 -0
- package/dist/__tests__/components/offline-status.test.d.ts +2 -0
- package/dist/__tests__/components/offline-status.test.d.ts.map +1 -0
- package/dist/__tests__/components/offline-status.test.js +56 -0
- package/dist/__tests__/components/offline-status.test.js.map +1 -0
- package/dist/__tests__/components/presence-chips-helpers.test.d.ts +2 -0
- package/dist/__tests__/components/presence-chips-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/components/presence-chips-helpers.test.js +35 -0
- package/dist/__tests__/components/presence-chips-helpers.test.js.map +1 -0
- package/dist/__tests__/lib/active-editor.test.d.ts +2 -0
- package/dist/__tests__/lib/active-editor.test.d.ts.map +1 -0
- package/dist/__tests__/lib/active-editor.test.js +57 -0
- package/dist/__tests__/lib/active-editor.test.js.map +1 -0
- package/dist/__tests__/lib/coauthor-client.test.d.ts +2 -0
- package/dist/__tests__/lib/coauthor-client.test.d.ts.map +1 -0
- package/dist/__tests__/lib/coauthor-client.test.js +173 -0
- package/dist/__tests__/lib/coauthor-client.test.js.map +1 -0
- package/dist/__tests__/lib/collaboration-provider.test.d.ts +2 -0
- package/dist/__tests__/lib/collaboration-provider.test.d.ts.map +1 -0
- package/dist/__tests__/lib/collaboration-provider.test.js +335 -0
- package/dist/__tests__/lib/collaboration-provider.test.js.map +1 -0
- package/dist/__tests__/lib/comment-anchor.test.d.ts +2 -0
- package/dist/__tests__/lib/comment-anchor.test.d.ts.map +1 -0
- package/dist/__tests__/lib/comment-anchor.test.js +173 -0
- package/dist/__tests__/lib/comment-anchor.test.js.map +1 -0
- package/dist/__tests__/lib/comments-client.test.d.ts +2 -0
- package/dist/__tests__/lib/comments-client.test.d.ts.map +1 -0
- package/dist/__tests__/lib/comments-client.test.js +202 -0
- package/dist/__tests__/lib/comments-client.test.js.map +1 -0
- package/dist/__tests__/lib/notifications-client.test.d.ts +2 -0
- package/dist/__tests__/lib/notifications-client.test.d.ts.map +1 -0
- package/dist/__tests__/lib/notifications-client.test.js +283 -0
- package/dist/__tests__/lib/notifications-client.test.js.map +1 -0
- package/dist/__tests__/lib/slash-command.test.d.ts +2 -0
- package/dist/__tests__/lib/slash-command.test.d.ts.map +1 -0
- package/dist/__tests__/lib/slash-command.test.js +123 -0
- package/dist/__tests__/lib/slash-command.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/AICoauthorBubbleMenu.d.ts +18 -0
- package/dist/components/AICoauthorBubbleMenu.d.ts.map +1 -0
- package/dist/components/AICoauthorBubbleMenu.js +113 -0
- package/dist/components/AICoauthorBubbleMenu.js.map +1 -0
- package/dist/components/CoauthorResultPopover.d.ts +28 -0
- package/dist/components/CoauthorResultPopover.d.ts.map +1 -0
- package/dist/components/CoauthorResultPopover.js +42 -0
- package/dist/components/CoauthorResultPopover.js.map +1 -0
- package/dist/components/CommandPalette.d.ts +1 -1
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +138 -40
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CommentMark.d.ts +54 -0
- package/dist/components/CommentMark.d.ts.map +1 -0
- package/dist/components/CommentMark.js +104 -0
- package/dist/components/CommentMark.js.map +1 -0
- package/dist/components/CommentSidePanel.d.ts +86 -0
- package/dist/components/CommentSidePanel.d.ts.map +1 -0
- package/dist/components/CommentSidePanel.js +165 -0
- package/dist/components/CommentSidePanel.js.map +1 -0
- package/dist/components/LivePreview.d.ts.map +1 -1
- package/dist/components/LivePreview.js +80 -16
- package/dist/components/LivePreview.js.map +1 -1
- package/dist/components/NotificationBell.d.ts +66 -0
- package/dist/components/NotificationBell.d.ts.map +1 -0
- package/dist/components/NotificationBell.js +142 -0
- package/dist/components/NotificationBell.js.map +1 -0
- package/dist/components/OfflineStatus.d.ts +39 -0
- package/dist/components/OfflineStatus.d.ts.map +1 -0
- package/dist/components/OfflineStatus.js +47 -0
- package/dist/components/OfflineStatus.js.map +1 -0
- package/dist/components/PresenceChips.d.ts +31 -0
- package/dist/components/PresenceChips.d.ts.map +1 -0
- package/dist/components/PresenceChips.js +58 -0
- package/dist/components/PresenceChips.js.map +1 -0
- package/dist/components/TipTapEditor.d.ts +61 -1
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/TipTapEditor.js +211 -8
- package/dist/components/TipTapEditor.js.map +1 -1
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/active-editor.d.ts +6 -0
- package/dist/lib/active-editor.d.ts.map +1 -0
- package/dist/lib/active-editor.js +35 -0
- package/dist/lib/active-editor.js.map +1 -0
- 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/coauthor-client.d.ts +72 -0
- package/dist/lib/coauthor-client.d.ts.map +1 -0
- package/dist/lib/coauthor-client.js +98 -0
- package/dist/lib/coauthor-client.js.map +1 -0
- package/dist/lib/collaboration-provider.d.ts +142 -0
- package/dist/lib/collaboration-provider.d.ts.map +1 -0
- package/dist/lib/collaboration-provider.js +168 -0
- package/dist/lib/collaboration-provider.js.map +1 -0
- package/dist/lib/comment-anchor.d.ts +86 -0
- package/dist/lib/comment-anchor.d.ts.map +1 -0
- package/dist/lib/comment-anchor.js +139 -0
- package/dist/lib/comment-anchor.js.map +1 -0
- package/dist/lib/comments-client.d.ts +106 -0
- package/dist/lib/comments-client.d.ts.map +1 -0
- package/dist/lib/comments-client.js +138 -0
- package/dist/lib/comments-client.js.map +1 -0
- package/dist/lib/notifications-client.d.ts +121 -0
- package/dist/lib/notifications-client.d.ts.map +1 -0
- package/dist/lib/notifications-client.js +173 -0
- package/dist/lib/notifications-client.js.map +1 -0
- package/dist/lib/slash-command.d.ts +39 -0
- package/dist/lib/slash-command.d.ts.map +1 -0
- package/dist/lib/slash-command.js +50 -0
- package/dist/lib/slash-command.js.map +1 -0
- package/package.json +12 -3
- package/src/__tests__/components/comment-mark.test.ts +99 -0
- package/src/__tests__/components/comment-side-panel.test.tsx +343 -0
- package/src/__tests__/components/notification-bell.test.tsx +379 -0
- package/src/__tests__/components/offline-status.test.tsx +66 -0
- package/src/__tests__/components/presence-chips-helpers.test.ts +44 -0
- package/src/__tests__/lib/active-editor.test.ts +70 -0
- package/src/__tests__/lib/coauthor-client.test.ts +213 -0
- package/src/__tests__/lib/collaboration-provider.test.ts +425 -0
- package/src/__tests__/lib/comment-anchor.test.ts +188 -0
- package/src/__tests__/lib/comments-client.test.ts +239 -0
- package/src/__tests__/lib/notifications-client.test.ts +339 -0
- package/src/__tests__/lib/slash-command.test.ts +146 -0
- package/src/components/AICoauthorBubbleMenu.tsx +187 -0
- package/src/components/CoauthorResultPopover.tsx +193 -0
- package/src/components/CommandPalette.tsx +397 -209
- package/src/components/CommentMark.ts +138 -0
- package/src/components/CommentSidePanel.tsx +521 -0
- package/src/components/LivePreview.tsx +117 -11
- package/src/components/NotificationBell.tsx +314 -0
- package/src/components/OfflineStatus.tsx +90 -0
- package/src/components/PresenceChips.tsx +150 -0
- package/src/components/TipTapEditor.tsx +310 -12
- package/src/index.ts +20 -0
- package/src/lib/active-editor.ts +39 -0
- package/src/lib/api.ts +11 -0
- package/src/lib/coauthor-client.ts +160 -0
- package/src/lib/collaboration-provider.ts +306 -0
- package/src/lib/comment-anchor.ts +179 -0
- package/src/lib/comments-client.ts +208 -0
- package/src/lib/notifications-client.ts +292 -0
- package/src/lib/slash-command.ts +67 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type Comment, type CommentAnchor, type CommentOutcome } from '../lib/comments-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Side-panel view of the slice-4 comments API. Renders threaded comments
|
|
5
|
+
* for a document, lets the current user compose new threads and replies,
|
|
6
|
+
* and offers per-comment actions: edit, resolve / reopen, delete.
|
|
7
|
+
*
|
|
8
|
+
* The component is *pure I/O* against the comments-client module —
|
|
9
|
+
* anchor encoding/decoding, editor mark painting, and bubble-menu wiring
|
|
10
|
+
* are layered on top in `TipTapEditor.tsx` through the `comments` prop.
|
|
11
|
+
* Keeping the panel logic agnostic of TipTap means the unit tests can
|
|
12
|
+
* verify thread ordering / state transitions without spinning up an
|
|
13
|
+
* editor.
|
|
14
|
+
*
|
|
15
|
+
* Behaviour
|
|
16
|
+
* ─────────
|
|
17
|
+
* - Active comments (not resolved, not deleted) are shown by default.
|
|
18
|
+
* - A toggle reveals resolved threads in the same list, sorted by
|
|
19
|
+
* resolved-at descending so the most recently closed appears first.
|
|
20
|
+
* - Composer accepts free-form text. When `onComposeAnchor` returns a
|
|
21
|
+
* non-null anchor the new comment is bound to that range; otherwise
|
|
22
|
+
* it is a document-level comment.
|
|
23
|
+
* - The list refreshes after every successful mutation so optimistic
|
|
24
|
+
* state can't drift from the server.
|
|
25
|
+
*
|
|
26
|
+
* Errors fall through to the `onError` callback so the host shell can
|
|
27
|
+
* surface them with its own toast / banner system (we don't import
|
|
28
|
+
* sonner here to keep the component testable in plain JSDOM).
|
|
29
|
+
*/
|
|
30
|
+
export interface CommentSidePanelProps {
|
|
31
|
+
documentId: string;
|
|
32
|
+
/** Current user id — used to gate edit / delete affordances. */
|
|
33
|
+
currentUserId: string;
|
|
34
|
+
/** Set to `true` to show admin-only affordances (delete others'). */
|
|
35
|
+
isAdmin?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Called when the user clicks "Comment on selection". Should return
|
|
38
|
+
* the encoded anchor for the active editor selection, or `null` for a
|
|
39
|
+
* doc-level comment. Wiring lives in `TipTapEditor` so this component
|
|
40
|
+
* stays editor-agnostic.
|
|
41
|
+
*/
|
|
42
|
+
onComposeAnchor?: () => CommentAnchor | null;
|
|
43
|
+
/**
|
|
44
|
+
* Called whenever the active comment changes (focus shift / open
|
|
45
|
+
* thread). The editor uses this to scroll the highlighted range into
|
|
46
|
+
* view and paint the `actuate-comment--active` modifier.
|
|
47
|
+
*/
|
|
48
|
+
onActiveCommentChange?: (commentId: string | null) => void;
|
|
49
|
+
/** Error sink — bubble up server failures to a toast / banner. */
|
|
50
|
+
onError?: (message: string) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Override the comments-client API. Tests inject a fake; real callers
|
|
53
|
+
* should leave this unset.
|
|
54
|
+
*/
|
|
55
|
+
api?: CommentsApi;
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Tiny adapter type that lets tests swap the comments-client module out.
|
|
60
|
+
* Mirrors the slice-4 client surface 1:1 — keeping it explicit prevents
|
|
61
|
+
* accidental drift when new endpoints are added later.
|
|
62
|
+
*/
|
|
63
|
+
export interface CommentsApi {
|
|
64
|
+
list: (documentId: string, opts?: {
|
|
65
|
+
includeResolved?: boolean;
|
|
66
|
+
}) => Promise<CommentOutcome<Comment[]>>;
|
|
67
|
+
create: (input: {
|
|
68
|
+
documentId: string;
|
|
69
|
+
body: string;
|
|
70
|
+
anchor?: CommentAnchor | null;
|
|
71
|
+
parentId?: string | null;
|
|
72
|
+
}) => Promise<CommentOutcome<Comment>>;
|
|
73
|
+
update: (id: string, body: string) => Promise<CommentOutcome<Comment>>;
|
|
74
|
+
resolve: (id: string) => Promise<CommentOutcome<Comment>>;
|
|
75
|
+
reopen: (id: string) => Promise<CommentOutcome<Comment>>;
|
|
76
|
+
remove: (id: string) => Promise<CommentOutcome<Comment>>;
|
|
77
|
+
}
|
|
78
|
+
export declare function CommentSidePanel({ documentId, currentUserId, isAdmin, onComposeAnchor, onActiveCommentChange, onError, api, className, }: CommentSidePanelProps): React.JSX.Element;
|
|
79
|
+
/**
|
|
80
|
+
* Cheap "n seconds / minutes / hours / days ago" formatter. We avoid
|
|
81
|
+
* importing a full date-fns dependency just for this view; the panel is
|
|
82
|
+
* fine being approximate.
|
|
83
|
+
*/
|
|
84
|
+
declare function formatRelative(iso: string): string;
|
|
85
|
+
export { formatRelative as _formatRelative };
|
|
86
|
+
//# sourceMappingURL=CommentSidePanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommentSidePanel.d.ts","sourceRoot":"","sources":["../../src/components/CommentSidePanel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA4D,MAAM,OAAO,CAAA;AAEhF,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,aAAa,EASlB,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,aAAa,GAAG,IAAI,CAAA;IAC5C;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1D,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;;;OAGG;IACH,GAAG,CAAC,EAAE,WAAW,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,CACJ,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,KACjC,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;IACvC,MAAM,EAAE,CAAC,KAAK,EAAE;QACd,UAAU,EAAE,MAAM,CAAA;QAClB,IAAI,EAAE,MAAM,CAAA;QACZ,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;QAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KACzB,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAA;IACtC,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAA;IACtE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAA;IACzD,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAA;IACxD,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAA;CACzD;AAWD,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,aAAa,EACb,OAAe,EACf,eAAe,EACf,qBAAqB,EACrB,OAAO,EACP,GAAgB,EAChB,SAAS,GACV,EAAE,qBAAqB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA2K3C;AAwND;;;;GAIG;AACH,iBAAS,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAW3C;AAGD,OAAO,EAAE,cAAc,IAAI,eAAe,EAAE,CAAA"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { createComment as apiCreateComment, deleteComment as apiDeleteComment, groupCommentsIntoThreads, listComments as apiListComments, reopenComment as apiReopenComment, resolveComment as apiResolveComment, updateComment as apiUpdateComment, } from '../lib/comments-client.js';
|
|
5
|
+
const defaultApi = {
|
|
6
|
+
list: (documentId, opts) => apiListComments(documentId, opts),
|
|
7
|
+
create: (input) => apiCreateComment(input),
|
|
8
|
+
update: (id, body) => apiUpdateComment(id, body),
|
|
9
|
+
resolve: (id) => apiResolveComment(id),
|
|
10
|
+
reopen: (id) => apiReopenComment(id),
|
|
11
|
+
remove: (id) => apiDeleteComment(id),
|
|
12
|
+
};
|
|
13
|
+
export function CommentSidePanel({ documentId, currentUserId, isAdmin = false, onComposeAnchor, onActiveCommentChange, onError, api = defaultApi, className, }) {
|
|
14
|
+
const [comments, setComments] = useState([]);
|
|
15
|
+
const [includeResolved, setIncludeResolved] = useState(false);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [draft, setDraft] = useState('');
|
|
18
|
+
const [activeCommentId, setActiveCommentId] = useState(null);
|
|
19
|
+
const mountedRef = useRef(true);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
return () => {
|
|
22
|
+
mountedRef.current = false;
|
|
23
|
+
};
|
|
24
|
+
}, []);
|
|
25
|
+
const setActive = useCallback((id) => {
|
|
26
|
+
setActiveCommentId(id);
|
|
27
|
+
onActiveCommentChange?.(id);
|
|
28
|
+
}, [onActiveCommentChange]);
|
|
29
|
+
const refresh = useCallback(async () => {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
const out = await api.list(documentId, { includeResolved });
|
|
32
|
+
if (!mountedRef.current)
|
|
33
|
+
return;
|
|
34
|
+
if (!out.ok) {
|
|
35
|
+
onError?.(out.error);
|
|
36
|
+
setComments([]);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
setComments(out.result);
|
|
40
|
+
}
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}, [api, documentId, includeResolved, onError]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
void refresh();
|
|
45
|
+
}, [refresh]);
|
|
46
|
+
const threads = useMemo(() => groupCommentsIntoThreads(comments), [comments]);
|
|
47
|
+
const handleCompose = useCallback(async () => {
|
|
48
|
+
const body = draft.trim();
|
|
49
|
+
if (!body)
|
|
50
|
+
return;
|
|
51
|
+
const anchor = onComposeAnchor?.() ?? null;
|
|
52
|
+
const out = await api.create({ documentId, body, anchor });
|
|
53
|
+
if (!out.ok) {
|
|
54
|
+
onError?.(out.error);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setDraft('');
|
|
58
|
+
setActive(out.result.id);
|
|
59
|
+
await refresh();
|
|
60
|
+
}, [draft, onComposeAnchor, api, documentId, onError, setActive, refresh]);
|
|
61
|
+
const handleReply = useCallback(async (parentId, body) => {
|
|
62
|
+
const trimmed = body.trim();
|
|
63
|
+
if (!trimmed)
|
|
64
|
+
return;
|
|
65
|
+
const out = await api.create({ documentId, body: trimmed, parentId });
|
|
66
|
+
if (!out.ok) {
|
|
67
|
+
onError?.(out.error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await refresh();
|
|
71
|
+
}, [api, documentId, onError, refresh]);
|
|
72
|
+
const handleEdit = useCallback(async (id, body) => {
|
|
73
|
+
const out = await api.update(id, body);
|
|
74
|
+
if (!out.ok) {
|
|
75
|
+
onError?.(out.error);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
await refresh();
|
|
79
|
+
return true;
|
|
80
|
+
}, [api, onError, refresh]);
|
|
81
|
+
const handleResolve = useCallback(async (id, alreadyResolved) => {
|
|
82
|
+
const out = alreadyResolved ? await api.reopen(id) : await api.resolve(id);
|
|
83
|
+
if (!out.ok) {
|
|
84
|
+
onError?.(out.error);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await refresh();
|
|
88
|
+
}, [api, onError, refresh]);
|
|
89
|
+
const handleDelete = useCallback(async (id) => {
|
|
90
|
+
const out = await api.remove(id);
|
|
91
|
+
if (!out.ok) {
|
|
92
|
+
onError?.(out.error);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (activeCommentId === id)
|
|
96
|
+
setActive(null);
|
|
97
|
+
await refresh();
|
|
98
|
+
}, [api, onError, refresh, activeCommentId, setActive]);
|
|
99
|
+
const wrapperClass = `flex h-full flex-col gap-3 border-l border-gray-200 bg-white p-3 ${className ?? ''}`;
|
|
100
|
+
return (_jsxs("aside", { className: wrapperClass, "data-testid": "comment-side-panel", children: [_jsxs("header", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-sm font-semibold text-gray-900", children: "Comments" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-1 text-xs text-gray-600", children: [_jsx("input", { type: "checkbox", checked: includeResolved, onChange: (e) => setIncludeResolved(e.target.checked), "data-testid": "include-resolved-toggle" }), "Show resolved"] })] }), _jsxs("section", { className: "flex flex-col gap-1.5", children: [_jsx("textarea", { className: "w-full resize-none rounded border border-gray-300 p-2 text-sm focus:border-blue-500 focus:outline-none", rows: 3, placeholder: "Write a comment\u2026", value: draft, onChange: (e) => setDraft(e.target.value), "data-testid": "comment-composer" }), _jsx("div", { className: "flex justify-end", children: _jsx("button", { type: "button", className: "rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-500 disabled:opacity-50", disabled: !draft.trim(), onClick: handleCompose, "data-testid": "comment-submit", children: "Comment" }) })] }), _jsx("section", { className: "flex-1 overflow-y-auto", "aria-busy": loading, "data-testid": "comment-list", children: loading ? (_jsx("p", { className: "text-xs text-gray-500", children: "Loading\u2026" })) : threads.length === 0 ? (_jsx("p", { className: "text-xs text-gray-500", children: "No comments yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: threads.map((thread) => (_jsx(CommentThreadView, { thread: thread, currentUserId: currentUserId, isAdmin: isAdmin, isActive: activeCommentId === thread.root.id, onFocus: () => setActive(thread.root.id), onReply: (body) => handleReply(thread.root.id, body), onEdit: (id, body) => handleEdit(id, body), onToggleResolve: (id, alreadyResolved) => handleResolve(id, alreadyResolved), onDelete: (id) => handleDelete(id) }, thread.root.id))) })) })] }));
|
|
101
|
+
}
|
|
102
|
+
function CommentThreadView({ thread, currentUserId, isAdmin, isActive, onFocus, onReply, onEdit, onToggleResolve, onDelete, }) {
|
|
103
|
+
const [replyDraft, setReplyDraft] = useState('');
|
|
104
|
+
const cardClass = `rounded border bg-gray-50 p-2 ${isActive ? 'border-yellow-400 ring-1 ring-yellow-300' : 'border-gray-200'}`;
|
|
105
|
+
return (_jsxs("li", { className: cardClass, onClick: onFocus, "data-testid": `comment-thread-${thread.root.id}`, "data-active": isActive ? 'true' : 'false', children: [_jsx(CommentRow, { comment: thread.root, currentUserId: currentUserId, isAdmin: isAdmin, onEdit: onEdit, onToggleResolve: () => onToggleResolve(thread.root.id, thread.root.isResolved), onDelete: () => onDelete(thread.root.id) }), thread.replies.length > 0 && (_jsx("ul", { className: "mt-2 flex flex-col gap-1 border-l-2 border-gray-200 pl-2", children: thread.replies.map((reply) => (_jsx("li", { children: _jsx(CommentRow, { comment: reply, currentUserId: currentUserId, isAdmin: isAdmin, onEdit: onEdit,
|
|
106
|
+
// Replies inherit the thread's resolve action; we only
|
|
107
|
+
// expose delete here.
|
|
108
|
+
onToggleResolve: null, onDelete: () => onDelete(reply.id) }) }, reply.id))) })), _jsxs("div", { className: "mt-2 flex gap-1", children: [_jsx("input", { className: "flex-1 rounded border border-gray-300 px-2 py-1 text-xs focus:border-blue-500 focus:outline-none", placeholder: "Reply\u2026", value: replyDraft, onChange: (e) => setReplyDraft(e.target.value), "data-testid": `reply-input-${thread.root.id}`, onClick: (e) => e.stopPropagation() }), _jsx("button", { type: "button", className: "rounded bg-gray-700 px-2 py-1 text-xs font-medium text-white hover:bg-gray-600 disabled:opacity-50", disabled: !replyDraft.trim(), onClick: (e) => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
onReply(replyDraft);
|
|
111
|
+
setReplyDraft('');
|
|
112
|
+
}, "data-testid": `reply-submit-${thread.root.id}`, children: "Reply" })] })] }));
|
|
113
|
+
}
|
|
114
|
+
function CommentRow({ comment, currentUserId, isAdmin, onEdit, onToggleResolve, onDelete, }) {
|
|
115
|
+
const [editing, setEditing] = useState(false);
|
|
116
|
+
const [draft, setDraft] = useState(comment.body);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
setDraft(comment.body);
|
|
119
|
+
}, [comment.body]);
|
|
120
|
+
const canMutate = isAdmin || comment.userId === currentUserId;
|
|
121
|
+
const meta = (_jsxs("div", { className: "text-xs text-gray-500", children: [comment.userId ?? 'deleted user', " \u00B7 ", formatRelative(comment.createdAt), comment.isResolved ? ' · resolved' : null] }));
|
|
122
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [meta, editing ? (_jsxs(_Fragment, { children: [_jsx("textarea", { className: "w-full resize-none rounded border border-gray-300 p-1 text-sm", rows: 3, value: draft, onChange: (e) => setDraft(e.target.value), "data-testid": `edit-textarea-${comment.id}`, onClick: (e) => e.stopPropagation() }), _jsxs("div", { className: "flex gap-1", children: [_jsx("button", { type: "button", className: "rounded bg-blue-600 px-2 py-1 text-xs font-medium text-white", onClick: async (e) => {
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
const ok = await onEdit(comment.id, draft);
|
|
125
|
+
if (ok)
|
|
126
|
+
setEditing(false);
|
|
127
|
+
}, "data-testid": `edit-save-${comment.id}`, children: "Save" }), _jsx("button", { type: "button", className: "rounded border border-gray-300 px-2 py-1 text-xs", onClick: (e) => {
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
setDraft(comment.body);
|
|
130
|
+
setEditing(false);
|
|
131
|
+
}, "data-testid": `edit-cancel-${comment.id}`, children: "Cancel" })] })] })) : (_jsx("p", { className: "text-sm whitespace-pre-wrap text-gray-900", "data-testid": `comment-body-${comment.id}`, children: comment.body })), !editing && (_jsxs("div", { className: "flex flex-wrap gap-1 text-xs", children: [canMutate && (_jsx("button", { type: "button", className: "text-gray-600 hover:text-gray-900", onClick: (e) => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
setEditing(true);
|
|
134
|
+
}, "data-testid": `edit-${comment.id}`, children: "Edit" })), onToggleResolve && (_jsx("button", { type: "button", className: "text-gray-600 hover:text-gray-900", onClick: (e) => {
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
onToggleResolve();
|
|
137
|
+
}, "data-testid": `resolve-${comment.id}`, children: comment.isResolved ? 'Reopen' : 'Resolve' })), canMutate && (_jsx("button", { type: "button", className: "text-red-600 hover:text-red-700", onClick: (e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
onDelete();
|
|
140
|
+
}, "data-testid": `delete-${comment.id}`, children: "Delete" }))] }))] }));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Cheap "n seconds / minutes / hours / days ago" formatter. We avoid
|
|
144
|
+
* importing a full date-fns dependency just for this view; the panel is
|
|
145
|
+
* fine being approximate.
|
|
146
|
+
*/
|
|
147
|
+
function formatRelative(iso) {
|
|
148
|
+
const t = Date.parse(iso);
|
|
149
|
+
if (Number.isNaN(t))
|
|
150
|
+
return iso;
|
|
151
|
+
const deltaSec = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
152
|
+
if (deltaSec < 60)
|
|
153
|
+
return `${deltaSec}s ago`;
|
|
154
|
+
const m = Math.round(deltaSec / 60);
|
|
155
|
+
if (m < 60)
|
|
156
|
+
return `${m}m ago`;
|
|
157
|
+
const h = Math.round(m / 60);
|
|
158
|
+
if (h < 24)
|
|
159
|
+
return `${h}h ago`;
|
|
160
|
+
const d = Math.round(h / 24);
|
|
161
|
+
return `${d}d ago`;
|
|
162
|
+
}
|
|
163
|
+
// Re-export for tests that build their own CommentsApi.
|
|
164
|
+
export { formatRelative as _formatRelative };
|
|
165
|
+
//# sourceMappingURL=CommentSidePanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommentSidePanel.js","sourceRoot":"","sources":["../../src/components/CommentSidePanel.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LivePreview.d.ts","sourceRoot":"","sources":["../../src/components/LivePreview.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LivePreview.d.ts","sourceRoot":"","sources":["../../src/components/LivePreview.tsx"],"names":[],"mappings":"AAeA,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AA4BD,wBAAgB,WAAW,CAAC,EAC1B,UAAU,EACV,UAAU,EACV,UAAU,EACV,MAAM,EACN,OAAO,GACR,EAAE,gBAAgB,2CAqNlB"}
|
|
@@ -1,47 +1,105 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
-
import { Monitor, Tablet, Smartphone, RefreshCw, ExternalLink, X } from 'lucide-react';
|
|
4
|
+
import { Monitor, Tablet, Smartphone, RefreshCw, ExternalLink, X, Wifi, WifiOff, } from 'lucide-react';
|
|
5
5
|
import { cmsApi } from '../lib/api.js';
|
|
6
6
|
const VIEWPORT_WIDTHS = {
|
|
7
7
|
desktop: '100%',
|
|
8
8
|
tablet: '768px',
|
|
9
9
|
mobile: '375px',
|
|
10
10
|
};
|
|
11
|
+
const PREVIEW_STREAM_PATH = '/api/cms/preview/stream';
|
|
12
|
+
function postToIframe(iframe, src, data) {
|
|
13
|
+
if (!iframe?.contentWindow || !src)
|
|
14
|
+
return;
|
|
15
|
+
let targetOrigin;
|
|
16
|
+
try {
|
|
17
|
+
targetOrigin = new URL(src).origin;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
targetOrigin = window.location.origin;
|
|
21
|
+
}
|
|
22
|
+
iframe.contentWindow.postMessage({ type: 'actuate-preview-update', data }, targetOrigin);
|
|
23
|
+
}
|
|
11
24
|
export function LivePreview({ collection, documentId, previewUrl, values, onClose, }) {
|
|
12
25
|
const iframeRef = useRef(null);
|
|
13
26
|
const [viewport, setViewport] = useState('desktop');
|
|
14
27
|
const [loading, setLoading] = useState(true);
|
|
15
28
|
const [previewSrc, setPreviewSrc] = useState(null);
|
|
29
|
+
const [previewToken, setPreviewToken] = useState(null);
|
|
30
|
+
const [streamStatus, setStreamStatus] = useState('idle');
|
|
16
31
|
useEffect(() => {
|
|
17
32
|
if (!documentId || !previewUrl) {
|
|
18
33
|
setPreviewSrc(null);
|
|
34
|
+
setPreviewToken(null);
|
|
19
35
|
return;
|
|
20
36
|
}
|
|
37
|
+
let cancelled = false;
|
|
21
38
|
async function fetchToken() {
|
|
22
39
|
const res = await cmsApi('/preview/token', {
|
|
23
40
|
method: 'POST',
|
|
24
41
|
body: JSON.stringify({ collection, documentId }),
|
|
25
42
|
});
|
|
43
|
+
if (cancelled)
|
|
44
|
+
return;
|
|
26
45
|
if (res.data?.token) {
|
|
27
46
|
const sep = previewUrl.includes('?') ? '&' : '?';
|
|
47
|
+
setPreviewToken(res.data.token);
|
|
28
48
|
setPreviewSrc(`${previewUrl}${sep}preview=true#token=${res.data.token}`);
|
|
29
49
|
}
|
|
30
50
|
}
|
|
31
51
|
fetchToken();
|
|
52
|
+
return () => {
|
|
53
|
+
cancelled = true;
|
|
54
|
+
};
|
|
32
55
|
}, [collection, documentId, previewUrl]);
|
|
33
56
|
useEffect(() => {
|
|
34
|
-
|
|
57
|
+
postToIframe(iframeRef.current, previewSrc, values);
|
|
58
|
+
}, [values, previewSrc]);
|
|
59
|
+
/**
|
|
60
|
+
* SSE pipe — when the server pushes a draft update for the same
|
|
61
|
+
* `{collection, documentId}`, forward the new `values` straight to the
|
|
62
|
+
* iframe via `postMessage`. The bus key matches the token's payload, so
|
|
63
|
+
* a second admin session updating the same draft (e.g. an AI agent)
|
|
64
|
+
* appears live in this preview without polling.
|
|
65
|
+
*/
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (typeof window === 'undefined' || typeof EventSource === 'undefined')
|
|
68
|
+
return;
|
|
69
|
+
if (!documentId || !previewToken) {
|
|
70
|
+
setStreamStatus('idle');
|
|
35
71
|
return;
|
|
36
|
-
let targetOrigin;
|
|
37
|
-
try {
|
|
38
|
-
targetOrigin = new URL(previewSrc).origin;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
targetOrigin = window.location.origin;
|
|
42
72
|
}
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
setStreamStatus('connecting');
|
|
74
|
+
const source = new EventSource(`${PREVIEW_STREAM_PATH}?token=${encodeURIComponent(previewToken)}`, { withCredentials: true });
|
|
75
|
+
const onReady = () => setStreamStatus('connected');
|
|
76
|
+
const onUpdate = (event) => {
|
|
77
|
+
try {
|
|
78
|
+
const payload = JSON.parse(event.data);
|
|
79
|
+
if (payload.values && typeof payload.values === 'object') {
|
|
80
|
+
postToIframe(iframeRef.current, previewSrc, payload.values);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ignore malformed events — the SSE wire format guarantees JSON,
|
|
85
|
+
// but a future schema bump shouldn't break this consumer.
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const onError = () => setStreamStatus('offline');
|
|
89
|
+
const onOpen = () => setStreamStatus((s) => (s === 'offline' ? 'connecting' : s));
|
|
90
|
+
source.addEventListener('ready', onReady);
|
|
91
|
+
source.addEventListener('update', onUpdate);
|
|
92
|
+
source.addEventListener('error', onError);
|
|
93
|
+
source.addEventListener('open', onOpen);
|
|
94
|
+
return () => {
|
|
95
|
+
source.removeEventListener('ready', onReady);
|
|
96
|
+
source.removeEventListener('update', onUpdate);
|
|
97
|
+
source.removeEventListener('error', onError);
|
|
98
|
+
source.removeEventListener('open', onOpen);
|
|
99
|
+
source.close();
|
|
100
|
+
setStreamStatus('idle');
|
|
101
|
+
};
|
|
102
|
+
}, [documentId, previewToken, previewSrc]);
|
|
45
103
|
const handleRefresh = useCallback(() => {
|
|
46
104
|
if (iframeRef.current) {
|
|
47
105
|
setLoading(true);
|
|
@@ -57,10 +115,16 @@ export function LivePreview({ collection, documentId, previewUrl, values, onClos
|
|
|
57
115
|
? 'Save the document first to enable preview'
|
|
58
116
|
: 'Configure a preview URL in the collection settings to enable live preview' }), _jsx("button", { onClick: onClose, className: "mt-4 text-xs text-gray-400 hover:text-gray-600", children: "Close Preview" })] }));
|
|
59
117
|
}
|
|
60
|
-
return (_jsxs("div", { className: "flex h-full flex-col border-l border-gray-200 bg-white", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-2", children: [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
118
|
+
return (_jsxs("div", { className: "flex h-full flex-col border-l border-gray-200 bg-white", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-2", children: [_jsxs("div", { className: "flex items-center gap-1", children: [[
|
|
119
|
+
['desktop', Monitor],
|
|
120
|
+
['tablet', Tablet],
|
|
121
|
+
['mobile', Smartphone],
|
|
122
|
+
].map(([vp, Icon]) => (_jsx("button", { onClick: () => setViewport(vp), className: `rounded p-1.5 ${viewport === vp ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`, title: vp, children: _jsx(Icon, { className: "h-4 w-4" }) }, vp))), _jsx("span", { className: "ml-2 inline-flex items-center gap-1 rounded-full bg-white px-2 py-0.5 text-[10px] font-medium text-gray-600", title: streamStatus === 'connected'
|
|
123
|
+
? 'Live preview is streaming updates from the server.'
|
|
124
|
+
: streamStatus === 'connecting'
|
|
125
|
+
? 'Connecting to the live preview stream…'
|
|
126
|
+
: streamStatus === 'offline'
|
|
127
|
+
? 'Live stream disconnected. Updates from this admin still work; cross-session updates will reconnect automatically.'
|
|
128
|
+
: 'Live preview stream idle.', children: streamStatus === 'connected' ? (_jsxs(_Fragment, { children: [_jsx(Wifi, { className: "h-3 w-3 text-green-600", "aria-hidden": true }), " Live"] })) : streamStatus === 'connecting' ? (_jsxs(_Fragment, { children: [_jsx(Wifi, { className: "h-3 w-3 animate-pulse text-blue-500", "aria-hidden": true }), " Connecting"] })) : streamStatus === 'offline' ? (_jsxs(_Fragment, { children: [_jsx(WifiOff, { className: "h-3 w-3 text-amber-500", "aria-hidden": true }), " Reconnecting"] })) : (_jsxs(_Fragment, { children: [_jsx(WifiOff, { className: "h-3 w-3 text-gray-400", "aria-hidden": true }), " Idle"] })) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { onClick: handleRefresh, className: "rounded p-1.5 text-gray-400 hover:text-gray-600", title: "Refresh", children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), _jsx("button", { onClick: handleOpenExternal, className: "rounded p-1.5 text-gray-400 hover:text-gray-600", title: "Open in new tab", children: _jsx(ExternalLink, { className: "h-4 w-4" }) }), _jsx("button", { onClick: onClose, className: "rounded p-1.5 text-gray-400 hover:text-gray-600", title: "Close", children: _jsx(X, { className: "h-4 w-4" }) })] })] }), _jsx("div", { className: "flex flex-1 justify-center overflow-auto bg-gray-100 p-4", children: _jsxs("div", { style: { width: VIEWPORT_WIDTHS[viewport], maxWidth: '100%', transition: 'width 0.3s' }, className: "relative", children: [loading && (_jsx("div", { className: "absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-white/80", children: _jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600" }) })), _jsx("iframe", { ref: iframeRef, src: previewSrc, className: "h-full w-full rounded-lg border border-gray-200 bg-white shadow-lg", style: { minHeight: '600px' }, onLoad: () => setLoading(false), title: "Page Preview" })] }) })] }));
|
|
65
129
|
}
|
|
66
130
|
//# sourceMappingURL=LivePreview.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LivePreview.js","sourceRoot":"","sources":["../../src/components/LivePreview.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAChE,OAAO,
|
|
1
|
+
{"version":3,"file":"LivePreview.js","sourceRoot":"","sources":["../../src/components/LivePreview.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type Notification, type NotificationOutcome, type NotificationStreamHandle, type NotificationStreamOptions } from '../lib/notifications-client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Top-bar notification bell + dropdown panel. Slice 6 of Phase 3
|
|
5
|
+
* surfaces every collaboration event that affects the current user —
|
|
6
|
+
* comment replies, resolves, and mentions — without forcing the
|
|
7
|
+
* editor to round-trip through the comments side panel.
|
|
8
|
+
*
|
|
9
|
+
* Design choices:
|
|
10
|
+
*
|
|
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`.
|
|
22
|
+
* - **Optimistic read state.** When the user clicks a notification we
|
|
23
|
+
* mark it read locally before the server confirms — so the bell
|
|
24
|
+
* counter feels instant. Server failures roll the state back via
|
|
25
|
+
* the `onError` toast.
|
|
26
|
+
* - **API injection.** The dependency-injection seam matches the rest
|
|
27
|
+
* of the admin (`CommentSidePanel.api`) so unit tests can substitute
|
|
28
|
+
* in-memory fakes — including the EventSource subscriber, which
|
|
29
|
+
* isn't available in JSDOM by default.
|
|
30
|
+
*/
|
|
31
|
+
export interface NotificationsApi {
|
|
32
|
+
list: (opts: {
|
|
33
|
+
includeRead?: boolean;
|
|
34
|
+
}) => Promise<NotificationOutcome<Notification[]>>;
|
|
35
|
+
unread: () => Promise<NotificationOutcome<number>>;
|
|
36
|
+
markRead: (id: string) => Promise<NotificationOutcome<Notification>>;
|
|
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;
|
|
48
|
+
}
|
|
49
|
+
export interface NotificationBellProps {
|
|
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
|
+
*/
|
|
56
|
+
pollIntervalMs?: number;
|
|
57
|
+
/** Optional click hook — called with the notification when a row is clicked. */
|
|
58
|
+
onSelect?: (notification: Notification) => void;
|
|
59
|
+
/** Error sink — bubble server failures to a toast / banner. */
|
|
60
|
+
onError?: (message: string) => void;
|
|
61
|
+
/** Override the API client (tests). */
|
|
62
|
+
api?: NotificationsApi;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function NotificationBell({ pollIntervalMs, onSelect, onError, api, className, }: NotificationBellProps): React.JSX.Element;
|
|
66
|
+
//# sourceMappingURL=NotificationBell.d.ts.map
|
|
@@ -0,0 +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,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"}
|