Customize comment actions
Add, remove, and reorder the actions on a comment with commentActions and CommentActionBar.
The actions on a comment are an array. commentActions builds the package's default list, CommentActionBar renders it, and both are exported, so adding, removing, and reordering entries are array operations rather than props. This guide covers each of those, and where an action's flow lives.
Prerequisites:
- A comment layout that renders its own
CommentActionBar. The defaultCommentbuilds this list internally. See Build a custom comment.
Build the default list
Every snippet on this page imports from one list:
import {SDKCommentsProvider} from '@sanity/collaboration/sdk'
import {
AddReactionButton,
CommentActionBar,
commentActions,
CommentsProvider,
DeleteCommentDialog,
useCommentsContext,
} from '@sanity/collaboration/ui'
import {WarningOutlineIcon} from '@sanity/icons/WarningOutline'
import {Flex} from '@sanity/ui'
import {useState} from 'react'const {resolveCapabilities} = useCommentsContext()
const capabilities = resolveCapabilities(comment)
const actions = commentActions({
comment,
capabilities,
onStatusChange,
onEdit,
onDelete,
onCopyLink,
onRetry,
})comment is the comment your card renders, from its props. onStatusChange, onEdit, onDelete, onCopyLink, and onRetry are your card's own handlers. Pass the ones the card supports.
comment is typed as the fields the list branches on, so a whole CommentData fits and so does an object carrying only those. capabilities comes from useCommentsContext().resolveCapabilities(comment), which applies the provider's policy for you. Unlike commentActions, resolveCapabilities needs the whole CommentData.
It returns resolve or reopen, retry, edit, copy link, and delete, in that order. An entry appears only when its callback was passed, and is disabled when the matching capability resolved to false: a missing callback renders nothing, and a forbidden one renders grayed out. Two entries have a second condition: resolve or reopen appears only on a thread parent, a comment with no parentCommentId, and retry appears only on a comment whose state.type is 'createError'.
The order is part of the contract, because it decides which entries CommentActionBar promotes to buttons. How many it promotes is maxVisible, covered in CommentActionBar.
Add an action
An entry is a CommentAction: a name, an action, and optionally an icon, a disabled flag, and a critical tone. The full shape is in Helpers.
const [flagged, setFlagged] = useState(false)
const actions = [
...commentActions({comment, capabilities, onEdit, onDelete}),
{
name: 'Mark as spam',
icon: WarningOutlineIcon,
action: () => setFlagged(true),
},
]Then render <CommentActionBar actions={actions} />, and show flagged however your card marks a flagged comment.
An entry with no icon always goes to the overflow menu, since an icon button with no icon is nothing to click.
Remove or reorder an action
// No copy link on this surface.
const actions = commentActions({
comment,
capabilities,
onEdit,
onDelete,
}).filter((action) => action.name !== 'Copy link')Leaving onCopyLink out is the better version of that, because an action with no callback is never built in the first place. Filtering is for the case where the same callbacks are wired but one surface should not offer the action.
To reorder, build the list in parts and concatenate them. Each part keeps the default order within it, and the result goes to CommentActionBar as usual:
// Edit first, so CommentActionBar promotes it to a button.
const actions = [
...commentActions({comment, capabilities, onEdit}),
...commentActions({comment, capabilities, onStatusChange, onDelete}),
]Filtering on action.name breaks as soon as the names are translated. Build the list without the callback instead, or filter before the labels are applied. See Translate the UI text.
Own the flow an action starts
An action reports that the user asked for something. CommentActionBar does not own the flow an entry starts, which is what lets you replace the flow without replacing the action.
Edit flips your own state and renders CommentInput:
const [editing, setEditing] = useState(false)
commentActions({comment, capabilities, onEdit: () => setEditing(true)})Delete opens DeleteCommentDialog, and the dialog's confirm is what reaches the SDK:
const [deletePending, setDeletePending] = useState(false)
commentActions({comment, capabilities, onDelete: () => setDeletePending(true)})
<DeleteCommentDialog
open={deletePending}
onClose={() => setDeletePending(false)}
onConfirm={() => remove(comment.id)}
/>The default Comment does both of these internally, which is why its onEdit takes a message rather than firing on the menu click.
Let capabilities decide what is disabled
Per capability, the resolution order is the component's capabilities prop, then the provider's policy, then the built-in defaults. In a layout of your own, the first of those is the optional second argument to resolveCapabilities(comment, overrides). canResolve covers reopening too, and canCreate covers retrying a failed create.
A policy goes on the provider. In an App SDK app that is SDKCommentsProvider. This one disables deletion on resolved comments:
<SDKCommentsProvider
capabilities={(comment) =>
comment.status === 'resolved' ? {canDelete: false} : {}
}
>
{children}
</SDKCommentsProvider>CommentsProvider takes the same capabilities prop. A role check goes there too:
<CommentsProvider
currentUser={me}
getUser={getUser}
// `isAdmin` is your app's own role check.
capabilities={() => ({canDelete: isAdmin})}
>
{children}
</CommentsProvider>The defaults are author ownership for edit and delete, thread status for reply, and true for resolve, reactions, and canCreate, which covers retry. The resolution order in full is in Providers.
Reactions are not actions
AddReactionButton opens its own popover rather than running a handler, so it composes beside CommentActionBar, or inside it as children, rather than as an entry in the array:
<ActionRow align="center" gap={1}>
<AddReactionButton
reactions={comment.reactions}
onReactionToggle={toggleReaction}
/>
<CommentActionBar actions={actions} />
</ActionRow>ActionRow is your card's own styled Flex, the one its hover reveal targets. See Declare the hover reveal.
Give it reactions and it marks what the current user has already picked.
Next steps
- The whole card assembled this way: Build a custom comment.
- What
commentActionsbuilds, entry by entry: Helpers. - Why an action exists only when its callback does: Comment components.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?