feat: CMS entry collaboration (comments) + notifications inbox - #5473
feat: CMS entry collaboration (comments) + notifications inbox#5473SvenAlHamad wants to merge 12 commits into
Conversation
…ields Add a collaboration feature that lets a team leave threaded comments on Headless CMS entries — either on the whole entry or anchored to a specific field — with replies, resolve/reopen, soft-delete, edit, and @mentions. New packages: - @webiny/api-collaboration: a private CMS model (wbyCollabThread) with nested messages (no raw DynamoDB), use cases (create/get/list/reply/resolve/reopen/ edit-message/delete-message/delete-thread), GraphQL under the `collaboration` namespace, and an app-agnostic LocatorResolver seam. Ships a CMS locator resolver that resolves field breadcrumbs / entry-level anchors and enforces read access via the existing entry use cases. Comment access == read access to the target; soft-delete only. - @webiny/app-collaboration: the admin UI — a header Comments toggle, an animated side panel, per-field markers, a composer, thread cards (reply/resolve/reopen/edit/delete), @mention autocomplete, entry-level vs field-anchored comments, and jump-to-field. Supporting changes: - app-admin FormModel: expose `qualifiedName` on IFieldVM and add a decoratable per-field wrapper (both additive) so a feature can anchor UI to a field. - Register createCollaboration() in the API graphql templates and mount <CollaborationAdminApp/> in app-serverless-cms. Out of scope (follow-up PRs): tasks + assignee inbox, the APW publish gate, and the unified activity feed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…orkflows Adds a per-user notifications inbox surfaced in the admin, fed by both the collaboration (comments) feature and the Advanced Publishing Workflow. New packages - @webiny/api-notifications: core. Private CMS model `wbyNotification` (recipient-scoped: recipientId, type, actor, title, snippet, link, read, archived). Repository wraps all CMS access in `withoutAuthorization`; use cases enforce access by recipientId === current identity. Use cases: Create, List, Counts, MarkRead/MarkAllRead, Archive/Unarchive. GraphQL under the `notifications` namespace. - @webiny/api-notifications-integrations: event-driven bridge. Subscribes to collaboration domain events (thread created -> mention notifications; reply added -> reply + mention notifications) and workflow events (approved / rejected). Single bridge package for both sources. - @webiny/app-notifications: admin UI. Bell registered into the top bar via a UserMenu decorator (no fixed positioning) with an unread badge, plus a slide-in inbox panel (Inbox/Archive tabs, unread-only filter, time grouping, typed items, mark-all-read). Clicking an item deep-links to the entry. Collaboration changes (support + polish) - Emit `CollabThreadCreatedEvent` / `CollabReplyAddedEvent` from the create-thread / reply use cases (via EventPublisher) so the bridge can react. - CmsLocatorResolver resolves the target entry title (getEntryTitle) and returns it as `contentTitle`; notification titles now reference the entry, not the anchored field. - Deep-linking to a specific comment thread now travels via URL query params (`commentThread` / `commentField`) instead of sessionStorage, making such links shareable; the CMS side consumes them once (open panel, highlight thread, scroll to field) then strips them from the URL. - Re-added "Copy link to thread" in the thread menu (builds a shareable deep-link URL). - Clicking a field's comment-count marker filters the panel to that field's threads, with a "Show all" affordance. - CollabThreadMapper coerces a null locator to "" (entry-level comments). Wiring - Registers createNotifications() + createNotificationsIntegrations() in the API GraphQL templates (base + OpenSearch + sqlite overlays) and mounts NotificationsAdminApp in app-serverless-cms. Note: collaboration + notifications are gated to tenant-only for development (WCP/APW capability gate ungated with TODO(before release) markers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ations # Conflicts: # packages/cli/files/references.json
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
brunozoric
left a comment
There was a problem hiding this comment.
Generally API looks ok. I would say that there is a bit too much logic/methods in React side.
| maxHeight = 220, | ||
| onKeyDown | ||
| }: Props) => { | ||
| const ref = useRef<HTMLTextAreaElement>(null); |
There was a problem hiding this comment.
Too much logic in React...?
There was a problem hiding this comment.
Still too much logic in react.
|
|
||
| // Highlights @mentions. Known display names (matched longest-first) are highlighted in full, | ||
| // e.g. "@John Doe"; anything else falls back to the single `@token`. | ||
| const renderBody = (body: string, names: string[]) => { |
There was a problem hiding this comment.
This looks like it can be simplified?
There was a problem hiding this comment.
Still not simplified. Resolve comment if you think it cant be simplified.
… toggle on a saved entry - Scope per-field comment markers to the entry form's DI container via a new CommentMarkersContext, so they no longer leak onto the nested form of a referenced-entry create/edit drawer (rendered in a child container) or onto unrelated forms that reuse the shared FormFieldWrapper. - Hide the Comments header toggle until the entry is saved (a new/unsaved entry has no id, so there is nothing to anchor a thread to). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix ListThreads pagination: exclude soft-deleted threads in the CMS query instead of in JS after the limit, so limit/meta stay consistent. - Modernize GraphQL: migrate collaboration + notifications schemas from GraphQLSchemaPlugin to the GraphQLSchemaFactory pattern (DI-injected use cases, named arg types); identical SDL. - Move composer submit logic + draft state into the presenter; route reply and message-edit submits through presenter methods. - Rebase AutoTextarea/MentionTextarea on admin-ui TextareaPrimitive. - Suppress comment markers on fields nested inside array/list fields (their locator can't uniquely anchor a thread yet) — scoped guard, no core change. - Extract a ThreadList component; simplify cmsContentId to split(); replace inline types with named interfaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@brunozoric - thanks for the review. Here's the summary of changes: On "too much logic/methods on the React side" — agreed. I moved the submit orchestration into the presenter:
GraphQL (collaboration.ts, notifications.ts) — migrated both off GraphQLSchemaPlugin to the GraphQLSchemaFactory pattern per the WebhookDeliverySchema reference: use cases are DI-injected via dependencies, args are named interfaces, registration is now context.container.register(...). SDL is unchanged. admin-ui textareas (AutoTextarea, MentionTextarea) — both now render @webiny/admin-ui's TextareaPrimitive instead of a raw <textarea> (cc @adrians5j — this addresses your "rely on the DS" point too). Mention autocomplete + auto-grow are layered on top of the primitive rather than being a bespoke input. ListThreadsUseCase (0 items but more to load) — good catch, real bug. Soft-deleted threads were filtered in JS after the CMS limit, so meta and the page could disagree. Now deleted: false is pushed into the CMS where, so the limit applies to already-filtered results. author.phone.type where phone is an array — you're right, and it turned out to be the deepest one. Resolution works (numeric segments are tolerated), but anchoring doesn't: a field's qualifiedName has no array index, so phone[0].type, phone[1].type, … all share one locator and can't be uniquely targeted. Since a proper fix needs a stable per-item id (index-based anchoring detaches on reorder), and that's a core form-model concern, I've done a scoped guard for now — comment markers are hidden on fields nested inside array/list fields so no ambiguous comment can be created — and we'll wire up real per-element anchoring once the core team lands a stable item id. Everything else (top-level fields, single/non-repeatable nested objects, entry-level) is unaffected. Smaller items:
CommentsPresenterImpl / PresenterAbstraction.Interface — it already implements PresenterAbstraction.Interface; the one spot referencing the concrete type is makeAutoObservable<CommentsPresenterImpl, "api">(...), where the generic has to be the concrete class so mobx can exclude the private api field. Let me know if you meant somewhere else. ThreadCard renderBody — left the @mention highlighter as-is for now; if you had a specific simplification in mind I'm happy to take it. |
|
Issue regarding the WCP gating created: #5491 |
…eaders Refresh control in the comments and notifications panel headers (left of Close). Comments reloads threads; notifications reloads the list + counts. Both spin + disable while loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| messageId: string; | ||
| } | ||
|
|
||
| class CollaborationSchema_ implements GraphQLSchemaFactory.Interface { |
There was a problem hiding this comment.
Claude likes to invent new ways to go round its constraints.
It should be CollaborationSchemaImpl, not CollaborationSchema_.
Not a big issue, but we have convention for this. Who knows why Claude is not following it - and there are a lot of examples.
| after?: string; | ||
| } | ||
|
|
||
| class NotificationsSchema_ implements GraphQLSchemaFactory.Interface { |
There was a problem hiding this comment.
Same _ as in other GraphQLSchemaFactory implementation.
| context.container.register(NotificationPrivateModel); | ||
| }); | ||
|
|
||
| const notificationsContextPlugin = new ContextPlugin(async context => { |
There was a problem hiding this comment.
I think all of this can go into createRegisterExtensionPlugin. No need to have context and registerExtension plugins.
| maxHeight = 220, | ||
| onKeyDown | ||
| }: Props) => { | ||
| const ref = useRef<HTMLTextAreaElement>(null); |
There was a problem hiding this comment.
Still too much logic in react.
|
|
||
| // Highlights @mentions. Known display names (matched longest-first) are highlighted in full, | ||
| // e.g. "@John Doe"; anything else falls back to the single `@token`. | ||
| const renderBody = (body: string, names: string[]) => { |
There was a problem hiding this comment.
Still not simplified. Resolve comment if you think it cant be simplified.
c087a46 to
58dec94
Compare
CMS entry collaboration (comments) + notifications inbox
Two related features in one PR (two commits): threaded comments on Headless CMS entries/fields, and a per-user notifications inbox fed by comments + the Advanced Publishing Workflow.
1. Collaboration — comments (
feat(collaboration)commit)Threaded comments anchored to a CMS entry or a specific field: replies, resolve/reopen, soft-delete, edit, @mentions. Stored via a private CMS model (
wbyCollabThread, messages nested). Admin UI: a Comments toggle in the entry editor header + an animated side panel with per-field comment markers.@webiny/api-collaboration(core) +@webiny/app-collaboration(admin).commentThread/commentField) that open the panel, highlight the thread, and scroll to the field (consumed once, then stripped from the URL — shareable).2. Notifications — inbox (
feat(notifications)commit)A per-user notifications inbox surfaced in the admin.
@webiny/api-notifications(core) — private CMS modelwbyNotification(recipient-scoped). Repository wraps CMS access inwithoutAuthorization; use cases enforce access viarecipientId === current identity. Use cases: Create, List, Counts, MarkRead/MarkAllRead, Archive/Unarchive. GraphQL under thenotificationsnamespace.@webiny/api-notifications-integrations(event-driven bridge) — subscribes to collaboration domain events (thread created → mention; reply added → reply + mention) and workflow events (approved / rejected). Single bridge for both sources.@webiny/app-notifications(admin UI) — bell registered into the top bar via aUserMenudecorator (no fixed positioning) with an unread badge, plus a slide-in inbox panel (Inbox/Archive tabs, unread-only filter, time grouping, typed items, mark-all-read). Items deep-link to the entry + comment thread.CmsLocatorResolverresolves it viagetEntryTitle), not the anchored field.Wiring — registers
createCollaboration(),createNotifications()+createNotificationsIntegrations()in the API GraphQL templates (base + OpenSearch + sqlite overlays); mounts the admin apps inapp-serverless-cms.Reviewer notes
TODO(before release): collaboration + notifications are gated to tenant-only for development — the WCP/APW capability gate is ungated (searchTODO(before release)). Re-wrap before shipping.reviewRequestednotification (needs workflow step team→member resolution — only approved/rejected wired so far); tasks/assignee inbox; no admin tests yet; notification rows store the title at creation time (existing rows keep old titles).🤖 Generated with Claude Code