Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions packages/payload-plugin-comments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,13 +485,13 @@ RESEND_FROM_EMAIL=comments@yourdomain.com
2. It creates a `comments` collection (hidden from the admin sidebar by default).
3. It patches **every collection** to inject `FieldCommentLabel` into each field's admin label and registers an `afterDelete` hook that cascade-deletes comments when a document is removed.
4. It patches **every global** to inject `FieldCommentLabel` into each field's admin label.
5. It registers two admin providers (`CommentsProviderWrapper`, `GlobalCommentsLoader`) and one admin action (`CommentsHeaderButton`).
5. It registers an admin provider (`CommentsProviderWrapper`) and one admin action (`CommentsHeaderButton`).
6. It registers REST endpoints under `/api/comments-plugin/*` (list, create, delete, resolve, mark-read, unread-mentions-count, mentionable-users, document-titles, field-labels). Each one requires an authenticated user and uses the Payload instance from the request, so it works on a freshly started serverless instance.

**Data loading** (`GlobalCommentsLoader`):
**Data loading** (`src/api`):

- This server component runs on every admin page load.
- It fetches all comments, document titles, mentionable users, field labels, collection labels, and global labels in parallel.
- Results are passed to `GlobalCommentsHydrator` (a client component) which hydrates the `CommentsContext`.
- The admin UI fetches comments, document titles, mentionable users, field labels, and unread mention counts with TanStack Query from the plugin's endpoints.
- Mutations (add, delete, resolve, mark as read) call the same endpoints and update the query cache optimistically.

**State management** (`CommentsProvider`):

Expand Down Expand Up @@ -526,10 +526,10 @@ RESEND_FROM_EMAIL=comments@yourdomain.com

## Exports Reference

| Import path | Exports |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `@focus-reactive/payload-plugin-comments` | `commentsPlugin`, `CommentsPluginConfig` (type), `CommentsTranslations` (type), `setPayloadConfig` |
| `@focus-reactive/payload-plugin-comments/styles.css` | Plugin stylesheet (Tailwind-compiled CSS) |
| Import path | Exports |
| ---------------------------------------------------- | ------------------------------------------------------------------------------- |
| `@focus-reactive/payload-plugin-comments` | `commentsPlugin`, `CommentsPluginConfig` (type), `CommentsTranslations` (type) |
| `@focus-reactive/payload-plugin-comments/styles.css` | Plugin stylesheet (Tailwind-compiled CSS) |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { useMutation } from "@tanstack/react-query";
import { getCommentsKey, getFieldLabelsKey, getDocumentTitlesKey, QUERY_KEYS } from "../queryKeys";
import { createComment } from "../../services/createComment";
import type { CreateCommentArgs } from "../../services/createComment";
import { useCommentsRequest } from "../useCommentsRequest";
import { COMMENTS_ENDPOINT_PATHS } from "../../constants";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import type { Comment, User, QueryContext } from "../../types";

Expand All @@ -20,12 +22,13 @@ interface AddCommentVariables {

export function useAddCommentMutation() {
const queryClient = useCommentsQueryClient();
const request = useCommentsRequest();

return useMutation(
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
mutationFn: ({ ctx: _ctx, currentUser: _user, ...data }: AddCommentVariables) =>
createComment(data),
request<CreateCommentArgs, Comment>(COMMENTS_ENDPOINT_PATHS.create, data),
onMutate: async (variables) => {
const {
ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { useMutation } from "@tanstack/react-query";
import { getCommentsKey } from "../queryKeys";
import { deleteComment } from "../../services/deleteComment";
import type { DeleteCommentArgs } from "../../services/deleteComment";
import { useCommentsRequest } from "../useCommentsRequest";
import { COMMENTS_ENDPOINT_PATHS } from "../../constants";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import type { Comment, QueryContext } from "../../types";

Expand All @@ -13,10 +15,12 @@ interface DeleteCommentVariables {

export function useDeleteCommentMutation() {
const queryClient = useCommentsQueryClient();
const request = useCommentsRequest();

return useMutation(
{
mutationFn: ({ commentId }: DeleteCommentVariables) => deleteComment(commentId),
mutationFn: ({ commentId }: DeleteCommentVariables) =>
request<DeleteCommentArgs, Comment>(COMMENTS_ENDPOINT_PATHS.delete, { id: commentId }),
onMutate: async (variables) => {
const { ctx, commentId } = variables;
const key = getCommentsKey(ctx);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";

import { useMutation } from "@tanstack/react-query";
import { markCommentRead } from "../../services/markCommentRead";
import type { MarkCommentReadArgs } from "../../services/markCommentRead";
import { useCommentsRequest } from "../useCommentsRequest";
import { COMMENTS_ENDPOINT_PATHS } from "../../constants";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import { useUnreadMentions } from "../../providers/UnreadMentionsProvider";

Expand All @@ -17,11 +19,15 @@ const UNREAD_COUNT_KEY_PREFIX = ["unread-mentions-count"] as const;

export function useMarkCommentReadMutation() {
const queryClient = useCommentsQueryClient();
const request = useCommentsRequest();
const { rememberRead } = useUnreadMentions();

return useMutation(
{
mutationFn: ({ commentId }: Variables) => markCommentRead({ commentId }),
mutationFn: ({ commentId }: Variables) =>
request<MarkCommentReadArgs, { alreadyRead: boolean }>(COMMENTS_ENDPOINT_PATHS.markRead, {
commentId,
}),
onMutate: async ({ commentId }) => {
rememberRead(commentId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { useMutation } from "@tanstack/react-query";
import { getCommentsKey } from "../queryKeys";
import { resolveComment } from "../../services/resolveComment";
import type { ResolveCommentArgs } from "../../services/resolveComment";
import { useCommentsRequest } from "../useCommentsRequest";
import { COMMENTS_ENDPOINT_PATHS } from "../../constants";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import type { Comment, QueryContext } from "../../types";

Expand All @@ -15,11 +17,15 @@ interface ResolveCommentVariables {

export function useResolveCommentMutation() {
const queryClient = useCommentsQueryClient();
const request = useCommentsRequest();

return useMutation(
{
mutationFn: ({ commentId, resolved }: ResolveCommentVariables) =>
resolveComment(commentId, resolved),
request<ResolveCommentArgs, Comment>(COMMENTS_ENDPOINT_PATHS.resolve, {
id: commentId,
resolved,
}),
onMutate: async (variables) => {
const { ctx, commentId, resolved, currentUser } = variables;
const key = getCommentsKey(ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
import { useQuery } from "@tanstack/react-query";
import { useConfig } from "@payloadcms/ui";
import { getCommentsKey } from "../queryKeys";
import { findAllComments } from "../../services/findAllComments";
import type { FindAllCommentsArgs } from "../../services/findAllComments";
import { useCommentsRequest } from "../useCommentsRequest";
import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import type { QueryContext } from "../../types";
import type { Comment, QueryContext } from "../../types";
import type { CommentsPluginConfigStorage } from "../../types";
import { REFETCH_INTERVAL } from "../../constants";
import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants";

export function useCommentsQuery(ctx: QueryContext) {
const queryClient = useCommentsQueryClient();
const { isOpen } = useCommentsDrawer();
const { config } = useConfig();
const request = useCommentsRequest();
const pluginConfig = config.admin?.custom?.commentsPlugin as
| CommentsPluginConfigStorage
| undefined;
Expand All @@ -22,7 +24,7 @@ export function useCommentsQuery(ctx: QueryContext) {
{
queryKey: getCommentsKey(ctx),
queryFn: async () => {
const params =
const params: FindAllCommentsArgs =
ctx.mode === "doc"
? {
enabledCollections: pluginConfig?.collections,
Expand All @@ -41,7 +43,10 @@ export function useCommentsQuery(ctx: QueryContext) {
enabledGlobals: pluginConfig?.globals,
};

const res = await findAllComments(params);
const res = await request<FindAllCommentsArgs, Comment[]>(
COMMENTS_ENDPOINT_PATHS.list,
params
);

if (!res.success) throw new Error(res.error);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import { useQuery } from "@tanstack/react-query";
import { useConfig } from "@payloadcms/ui";
import { useLocale } from "@payloadcms/ui";
import { getDocumentTitlesKey } from "../queryKeys";
import { getDocumentTitles } from "../../services/getDocumentTitles";
import type { GetDocumentTitlesArgs } from "../../services/getDocumentTitles";
import { useCommentsRequest } from "../useCommentsRequest";
import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import { useCommentsQuery } from "./useCommentsQuery";
import type { QueryContext } from "../../types";
import type { DocumentTitles, QueryContext } from "../../types";
import type { CommentsPluginConfigStorage } from "../../types";
import { REFETCH_INTERVAL } from "../../constants";
import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants";

export function useDocumentTitlesQuery(ctx: QueryContext) {
const queryClient = useCommentsQueryClient();
const { isOpen } = useCommentsDrawer();
const { data: comments } = useCommentsQuery(ctx);
const { code: locale } = useLocale();
const { config } = useConfig();
const request = useCommentsRequest();

const pluginConfig = config.admin?.custom?.commentsPlugin as
| CommentsPluginConfigStorage
Expand All @@ -27,10 +29,13 @@ export function useDocumentTitlesQuery(ctx: QueryContext) {
{
queryKey: getDocumentTitlesKey(ctx),
queryFn: async () => {
const res = await getDocumentTitles(
comments ?? [],
pluginConfig?.documentTitleFields ?? {},
{ locale }
const res = await request<GetDocumentTitlesArgs, DocumentTitles>(
COMMENTS_ENDPOINT_PATHS.documentTitles,
{
comments: comments ?? [],
documentTitleFields: pluginConfig?.documentTitleFields ?? {},
locale,
}
);

if (!res.success) throw new Error(res.error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,33 @@

import { useQuery } from "@tanstack/react-query";
import { getFieldLabelsKey } from "../queryKeys";
import { fetchFieldLabels } from "../../services/fieldLabels/fetchFieldLabels";
import type { FetchFieldLabelsArgs } from "../../services/fieldLabels/fetchFieldLabels";
import { useCommentsRequest } from "../useCommentsRequest";
import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import { useCommentsQuery } from "./useCommentsQuery";
import type { QueryContext } from "../../types";
import { REFETCH_INTERVAL } from "../../constants";
import type { GlobalFieldLabelRegistry, QueryContext } from "../../types";
import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants";

export function useFieldLabelsQuery(ctx: QueryContext) {
const queryClient = useCommentsQueryClient();
const { isOpen } = useCommentsDrawer();
const { data: comments } = useCommentsQuery(ctx);
const request = useCommentsRequest();

return useQuery(
{
queryKey: getFieldLabelsKey(ctx),
queryFn: () => fetchFieldLabels(comments ?? []),
queryFn: async () => {
const res = await request<FetchFieldLabelsArgs, GlobalFieldLabelRegistry>(
COMMENTS_ENDPOINT_PATHS.fieldLabels,
{ comments: comments ?? [] }
);

if (!res.success) throw new Error(res.error);

return res.data;
},
enabled: isOpen && !!comments,
staleTime: 0,
refetchInterval: isOpen ? REFETCH_INTERVAL : false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

import { useQuery } from "@tanstack/react-query";
import { QUERY_KEYS } from "../queryKeys";
import { fetchMentionableUsers } from "../../services/fetchMentionableUsers";
import { useCommentsRequest } from "../useCommentsRequest";
import { useCommentsDrawer } from "../../providers/CommentsDrawerProvider";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import { REFETCH_INTERVAL } from "../../constants";
import { COMMENTS_ENDPOINT_PATHS, REFETCH_INTERVAL } from "../../constants";
import type { User } from "../../types";

export function useMentionableUsersQuery() {
const queryClient = useCommentsQueryClient();
const { isOpen } = useCommentsDrawer();
const request = useCommentsRequest();

return useQuery(
{
queryKey: QUERY_KEYS.mentionableUsers(),
queryFn: async () => {
const res = await fetchMentionableUsers();
const res = await request<object, User[]>(COMMENTS_ENDPOINT_PATHS.mentionableUsers, {});

if (!res.success) throw new Error(res.error);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { useQuery } from "@tanstack/react-query";
import { QUERY_KEYS } from "../queryKeys";
import { countUnreadMentions } from "../../services/countUnreadMentions";
import type { CountUnreadMentionsArgs } from "../../services/countUnreadMentions";
import { useCommentsRequest } from "../useCommentsRequest";
import { useCommentsQueryClient } from "../../providers/CommentsQueryClientProvider";
import { UNREAD_COUNT_REFETCH_INTERVAL } from "../../constants";
import { COMMENTS_ENDPOINT_PATHS, UNREAD_COUNT_REFETCH_INTERVAL } from "../../constants";
import type { Mode } from "../../types";

interface Props {
Expand All @@ -23,6 +24,7 @@ export function useUnreadMentionsCountQuery({
locale,
}: Props) {
const queryClient = useCommentsQueryClient();
const request = useCommentsRequest();

return useQuery(
{
Expand All @@ -34,13 +36,16 @@ export function useUnreadMentionsCountQuery({
locale
),
queryFn: async () => {
const res = await countUnreadMentions({
mode,
collectionSlug,
documentId,
globalSlug,
locale,
});
const res = await request<CountUnreadMentionsArgs, { count: number }>(
COMMENTS_ENDPOINT_PATHS.unreadMentionsCount,
{
mode,
collectionSlug,
documentId,
globalSlug,
locale,
}
);

if (!res.success) throw new Error(res.error);

Expand Down
40 changes: 40 additions & 0 deletions packages/payload-plugin-comments/src/api/useCommentsRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { useCallback } from "react";
import { useConfig } from "@payloadcms/ui";
import { formatAdminURL } from "payload/shared";
import type { Response } from "../types";
import { getDefaultErrorMessage } from "../utils/error/getDefaultErrorMessage";

function isServiceResponse<TData>(value: unknown): value is Response<TData> {
return typeof value === "object" && value !== null && "success" in value;
}

export function useCommentsRequest() {
const {
config: {
routes: { api: apiRoute },
},
} = useConfig();

return useCallback(
async <TArgs, TData>(path: `/${string}`, args: TArgs): Promise<Response<TData>> => {
try {
const res = await fetch(formatAdminURL({ apiRoute, path }), {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(args),
});
const body: unknown = await res.json().catch(() => null);

if (isServiceResponse<TData>(body)) return body;

return { success: false, error: res.statusText || `Request failed with ${res.status}` };
} catch (err) {
return { success: false, error: getDefaultErrorMessage(err) };
}
},
[apiRoute]
);
}
Loading
Loading