Skip to content

feat: CMS entry collaboration (comments) + notifications inbox - #5473

Open
SvenAlHamad wants to merge 12 commits into
release/6.5.0from
feat/notifications
Open

feat: CMS entry collaboration (comments) + notifications inbox#5473
SvenAlHamad wants to merge 12 commits into
release/6.5.0from
feat/notifications

Conversation

@SvenAlHamad

Copy link
Copy Markdown
Contributor

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).
  • Field-scoped filtering (click a field's marker → panel filters to that field), "Copy link to thread", and URL-query-param deep-links (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 model wbyNotification (recipient-scoped). Repository wraps CMS access in withoutAuthorization; use cases enforce access via 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; 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 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). Items deep-link to the entry + comment thread.
  • Notification titles reference the entry title (CmsLocatorResolver resolves it via getEntryTitle), not the anchored field.

Wiring — registers createCollaboration(), createNotifications() + createNotificationsIntegrations() in the API GraphQL templates (base + OpenSearch + sqlite overlays); mounts the admin apps in app-serverless-cms.

Reviewer notes

  • ⚠️ TODO(before release): collaboration + notifications are gated to tenant-only for development — the WCP/APW capability gate is ungated (search TODO(before release)). Re-wrap before shipping.
  • Deploy: API packages need a redeploy; the admin UI is client-side.
  • Deferred: reviewRequested notification (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

SvenAlHamad and others added 2 commits July 22, 2026 11:41
…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>
@brunozoric brunozoric added this to the 6.5.0 milestone Jul 23, 2026

@brunozoric brunozoric left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally API looks ok. I would say that there is a bit too much logic/methods in React side.

Comment thread packages/api-collaboration/src/features/thread/ListThreads/ListThreadsUseCase.ts Outdated
Comment thread packages/api-collaboration/src/graphql/collaboration.ts Outdated
Comment thread packages/api-collaboration/src/utils/cmsContentId.ts Outdated
Comment thread packages/api-collaboration/src/utils/identity.ts Outdated
Comment thread packages/api-collaboration/src/index.ts
maxHeight = 220,
onKeyDown
}: Props) => {
const ref = useRef<HTMLTextAreaElement>(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too much logic in React...?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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[]) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it can be simplified?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not simplified. Resolve comment if you think it cant be simplified.

SvenAlHamad and others added 5 commits July 23, 2026 12:37
… 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>
@SvenAlHamad

Copy link
Copy Markdown
Contributor Author

@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:

  • Composer submit + its draft state now live on the presenter (submitComposer(), setComposerBody, addComposerMention, resetComposer); the component is now a thin view.
  • Reply and message-edit submits go through presenter.submitReply() / presenter.submitMessageEdit().
  • I kept the transient input/caret state (and the @mention detection inside the textarea) local, since that's genuinely view/DOM concern — happy to revisit if you'd draw the line differently.

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:

  • cmsContentId — simplified to split(":"); aligned the client-side parse in NotificationItem too.
  • Inline types removed — identity.ts → WebinyIdentityLike, CommentFieldMarker → CommentFieldMarkerProps.
  • CommentsPanel — extracted a ThreadList component that returns null when empty; the sections use it.
  • The TODO(before release) (WCP re-gate) — agreed a code TODO isn't enough; filing a tracked issue and linking it here.

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.

@SvenAlHamad SvenAlHamad modified the milestones: 6.5.0, 6.6.0 Jul 27, 2026
@SvenAlHamad

Copy link
Copy Markdown
Contributor Author

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same _ as in other GraphQLSchemaFactory implementation.

context.container.register(NotificationPrivateModel);
});

const notificationsContextPlugin = new ContextPlugin(async context => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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[]) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not simplified. Resolve comment if you think it cant be simplified.

@Pavel910
Pavel910 deleted the branch release/6.5.0 August 3, 2026 16:18
@Pavel910 Pavel910 closed this Aug 3, 2026
@Pavel910 Pavel910 reopened this Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants