Skip to content

Commit 75273af

Browse files
authoredMar 8, 2024
feat(comments): support task comments (#5934)
* feat(comments): support task comments * fix(comments): omit `payload` when creating new thread * chore(eslint): add "sortOrder" to `ignores.attributes` * fix(comments): remove eslint ignore comment
1 parent 8bf1c92 commit 75273af

17 files changed

+302
-165
lines changed
 

‎.eslintrc.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const config = {
7373
{
7474
ignores: {
7575
componentPatterns: ['motion$'],
76-
attributes: ['animate', 'closed', 'exit', 'fill', 'full', 'initial', 'size'],
76+
attributes: ['animate', 'closed', 'exit', 'fill', 'full', 'initial', 'size', 'sortOrder'],
7777
},
7878
},
7979
],

‎packages/sanity/src/structure/comments/plugin/document-layout/CommentsDocumentLayout.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ function CommentsDocumentLayoutInner(props: DocumentLayoutProps) {
4343
documentType={documentType}
4444
isCommentsOpen={inspector?.name === COMMENTS_INSPECTOR_NAME}
4545
onCommentsOpen={handleOpenCommentsInspector}
46+
sortOrder="desc"
47+
type="field"
4648
>
4749
<CommentsSelectedPathProvider>
4850
<CommentsAuthoringPathProvider>{props.renderDefault(props)}</CommentsAuthoringPathProvider>

‎packages/sanity/src/structure/comments/plugin/field/CommentsField.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ function CommentFieldInner(
222222

223223
// Construct the comment payload
224224
const nextComment: CommentCreatePayload = {
225+
type: 'field',
225226
fieldPath: PathUtils.toString(props.path),
226227
message: value,
227228
parentCommentId: undefined,

‎packages/sanity/src/structure/comments/plugin/input/components/CommentsPortableTextInput.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export const CommentsPortableTextInputInner = React.memo(function CommentsPortab
134134
const threadId = uuid()
135135

136136
operation.create({
137+
type: 'field',
137138
contentSnapshot: fragment,
138139
fieldPath: stringFieldPath,
139140
message: nextCommentValue,
@@ -173,7 +174,7 @@ export const CommentsPortableTextInputInner = React.memo(function CommentsPortab
173174
if (!comment) return
174175

175176
setSelectedPath({
176-
fieldPath: comment.target.path.field,
177+
fieldPath: comment.target.path?.field || '',
177178
threadId: comment.threadId,
178179
origin: 'form',
179180
})
@@ -276,7 +277,7 @@ export const CommentsPortableTextInputInner = React.memo(function CommentsPortab
276277

277278
const nextValue: CommentsTextSelectionItem[] = updatedDecoration
278279
? [
279-
...(comment.target.path.selection?.value
280+
...(comment.target.path?.selection?.value
280281
.filter((r) => r._key !== nextRange[0]?._key)
281282
.concat(nextRange)
282283
.flat() || EMPTY_ARRAY),
@@ -287,7 +288,8 @@ export const CommentsPortableTextInputInner = React.memo(function CommentsPortab
287288
target: {
288289
...comment.target,
289290
path: {
290-
...comment.target.path,
291+
...(comment.target?.path || {}),
292+
field: comment.target.path?.field || '',
291293
selection: {
292294
type: 'text',
293295
value: nextValue,

‎packages/sanity/src/structure/comments/plugin/inspector/CommentsInspector.tsx

+27-12
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {EMPTY_PARAMS} from '../../../constants'
99
import {useDocumentPane} from '../../../panes/document/useDocumentPane'
1010
import {commentsLocaleNamespace} from '../../i18n'
1111
import {
12-
type CommentCreatePayload,
12+
type CommentBaseCreatePayload,
1313
CommentDeleteDialog,
1414
type CommentReactionOption,
1515
CommentsList,
@@ -163,7 +163,8 @@ function CommentsInspectorInner(
163163
if (!comment) return
164164

165165
operation.create({
166-
fieldPath: comment.target.path.field,
166+
type: 'field',
167+
fieldPath: comment.target.path?.field || '',
167168
id: comment._id,
168169
message: comment.message,
169170
parentCommentId: comment.parentCommentId,
@@ -204,28 +205,42 @@ function CommentsInspectorInner(
204205
)
205206

206207
const handleNewThreadCreate = useCallback(
207-
(payload: CommentCreatePayload) => {
208-
operation.create(payload)
208+
(nextComment: CommentBaseCreatePayload) => {
209+
const fieldPath = nextComment?.payload?.fieldPath || ''
210+
211+
operation.create({
212+
type: 'field',
213+
fieldPath,
214+
message: nextComment.message,
215+
parentCommentId: nextComment.parentCommentId,
216+
reactions: nextComment.reactions,
217+
status: nextComment.status,
218+
threadId: nextComment.threadId,
219+
})
209220

210221
setSelectedPath({
211-
fieldPath: payload.fieldPath,
222+
fieldPath,
212223
origin: 'inspector',
213-
threadId: payload.threadId,
224+
threadId: nextComment.threadId,
214225
})
215226
},
216227
[operation, setSelectedPath],
217228
)
218229

219230
const handleReply = useCallback(
220-
(payload: CommentCreatePayload) => {
221-
operation.create(payload)
231+
(nextComment: CommentBaseCreatePayload) => {
232+
operation.create({
233+
...nextComment,
234+
type: 'field',
235+
fieldPath: nextComment?.payload?.fieldPath || '',
236+
})
222237
},
223238
[operation],
224239
)
225240

226241
const handleEdit = useCallback(
227-
(id: string, payload: CommentUpdatePayload) => {
228-
operation.update(id, payload)
242+
(id: string, nextComment: CommentUpdatePayload) => {
243+
operation.update(id, nextComment)
229244
},
230245
[operation],
231246
)
@@ -276,7 +291,7 @@ function CommentsInspectorInner(
276291
if (!comment) return
277292

278293
setSelectedPath({
279-
fieldPath: comment.target.path.field || null,
294+
fieldPath: comment.target.path?.field || null,
280295
origin: 'inspector',
281296
threadId: comment.threadId || null,
282297
})
@@ -331,7 +346,7 @@ function CommentsInspectorInner(
331346
setStatus(commentToScrollTo.status || 'open')
332347

333348
setSelectedPath({
334-
fieldPath: commentToScrollTo.target.path.field || null,
349+
fieldPath: commentToScrollTo.target.path?.field || null,
335350
origin: 'url',
336351
threadId: commentToScrollTo.threadId || null,
337352
})

‎packages/sanity/src/structure/comments/src/__workshop__/CommentInlineHighlightDebugStory.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export default function CommentInlineHighlightDebugStory() {
135135
// Build comment thread items here so that we can test the validation of each comment
136136
// since that is done in the `buildCommentThreadItems` function.
137137
return buildCommentThreadItems({
138+
type: 'field',
138139
comments: commentDocuments,
139140
currentUser,
140141
schemaType: schema.get('article'),
@@ -186,7 +187,7 @@ export default function CommentInlineHighlightDebugStory() {
186187
selection: {
187188
type: 'text',
188189
value: [
189-
...(comment.target.path.selection?.value
190+
...(comment.target.path?.selection?.value
190191
.filter((r) => r._key !== range._key)
191192
.concat(currentBlockKey ? {...range, _key: currentBlockKey} : [])
192193
.flat() || []),

‎packages/sanity/src/structure/comments/src/__workshop__/CommentsListStory.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {useCurrentUser, useUserListWithPermissions} from 'sanity'
77

88
import {CommentsList} from '../components'
99
import {
10-
type CommentCreatePayload,
10+
type CommentBaseCreatePayload,
1111
type CommentDocument,
1212
type CommentReactionOption,
1313
type CommentStatus,
@@ -196,7 +196,7 @@ export default function CommentsListStory() {
196196
const mentionOptions = useUserListWithPermissions(MENTION_HOOK_OPTIONS)
197197

198198
const handleReplySubmit = useCallback(
199-
(payload: CommentCreatePayload) => {
199+
(payload: CommentBaseCreatePayload) => {
200200
const reply: CommentDocument = {
201201
...BASE,
202202
...payload,
@@ -235,7 +235,7 @@ export default function CommentsListStory() {
235235
)
236236

237237
const handleNewThreadCreate = useCallback(
238-
(payload: CommentCreatePayload) => {
238+
(payload: CommentBaseCreatePayload) => {
239239
const comment: CommentDocument = {
240240
...BASE,
241241
...payload,
@@ -356,6 +356,7 @@ export default function CommentsListStory() {
356356
currentUser,
357357
documentValue: {},
358358
schemaType: schema.get('article'),
359+
type: 'field',
359360
})
360361

361362
return items

‎packages/sanity/src/structure/comments/src/__workshop__/CommentsProviderStory.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function CommentsProviderStory() {
2525
return (
2626
<AddonDatasetProvider>
2727
<CommentsEnabledProvider documentType={_type} documentId={_id}>
28-
<CommentsProvider documentType={_type} documentId={_id}>
28+
<CommentsProvider documentType={_type} documentId={_id} type="field" sortOrder="desc">
2929
<ConditionalWrapper
3030
condition={_mode === 'upsell'}
3131
// eslint-disable-next-line react/jsx-no-bind
@@ -74,9 +74,15 @@ function Inner({mode}: {mode: CommentsUIMode}) {
7474
onCreateRetry={noop}
7575
onDelete={operation.remove}
7676
onEdit={operation.update}
77-
onNewThreadCreate={operation.create}
77+
// eslint-disable-next-line react/jsx-no-bind
78+
onNewThreadCreate={(c) =>
79+
operation.create({type: 'field', fieldPath: c.payload?.fieldPath || '', ...c})
80+
}
7881
onReactionSelect={operation.react}
79-
onReply={operation.create}
82+
// eslint-disable-next-line react/jsx-no-bind
83+
onReply={(c) =>
84+
operation.create({type: 'field', fieldPath: c.payload?.fieldPath || '', ...c})
85+
}
8086
selectedPath={null}
8187
status="open"
8288
/>

‎packages/sanity/src/structure/comments/src/components/list/CommentThreadLayout.tsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import styled, {css} from 'styled-components'
1414
import {commentsLocaleNamespace} from '../../../i18n'
1515
import {type CommentsSelectedPath} from '../../context'
1616
import {
17-
type CommentCreatePayload,
17+
type CommentBaseCreatePayload,
1818
type CommentListBreadcrumbs,
1919
type CommentMessage,
2020
type CommentsUIMode,
@@ -47,7 +47,7 @@ interface CommentThreadLayoutProps {
4747
isSelected: boolean
4848
mentionOptions: UserListWithPermissionsHookValue
4949
mode: CommentsUIMode
50-
onNewThreadCreate: (payload: CommentCreatePayload) => void
50+
onNewThreadCreate: (payload: CommentBaseCreatePayload) => void
5151
onPathSelect?: (nextPath: CommentsSelectedPath) => void
5252
readOnly?: boolean
5353
}
@@ -71,15 +71,18 @@ export function CommentThreadLayout(props: CommentThreadLayoutProps) {
7171

7272
const handleNewThreadCreate = useCallback(
7373
(payload: CommentMessage) => {
74-
const nextComment: CommentCreatePayload = {
75-
fieldPath,
74+
const nextComment: CommentBaseCreatePayload = {
7675
message: payload,
7776
parentCommentId: undefined,
7877
status: 'open',
7978
// Since this is a new comment, we generate a new thread ID
8079
threadId: uuid(),
8180
// New comments have no reactions
8281
reactions: [],
82+
83+
payload: {
84+
fieldPath,
85+
},
8386
}
8487

8588
onNewThreadCreate?.(nextComment)

‎packages/sanity/src/structure/comments/src/components/list/CommentsList.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {type UserListWithPermissionsHookValue} from 'sanity'
66
import {type CommentsSelectedPath} from '../../context'
77
import {applyCommentsGroupAttr} from '../../hooks'
88
import {
9-
type CommentCreatePayload,
9+
type CommentBaseCreatePayload,
1010
type CommentReactionOption,
1111
type CommentStatus,
1212
type CommentsUIMode,
@@ -51,10 +51,10 @@ export interface CommentsListProps {
5151
onCreateRetry: (id: string) => void
5252
onDelete: (id: string) => void
5353
onEdit: (id: string, payload: CommentUpdatePayload) => void
54-
onNewThreadCreate: (payload: CommentCreatePayload) => void
54+
onNewThreadCreate: (payload: CommentBaseCreatePayload) => void
5555
onPathSelect?: (nextPath: CommentsSelectedPath) => void
5656
onReactionSelect?: (id: string, reaction: CommentReactionOption) => void
57-
onReply: (payload: CommentCreatePayload) => void
57+
onReply: (payload: CommentBaseCreatePayload) => void
5858
onStatusChange?: (id: string, status: CommentStatus) => void
5959
readOnly?: boolean
6060
selectedPath: CommentsSelectedPath | null
@@ -185,7 +185,7 @@ const CommentsListInner = forwardRef(function CommentsListInner(
185185
// selected path.
186186
const threadIsSelected =
187187
selectedPath?.threadId === item.parentComment.threadId &&
188-
selectedPath?.fieldPath === item.parentComment.target.path.field
188+
selectedPath?.fieldPath === item.parentComment.target.path?.field
189189

190190
return (
191191
<CommentsListItem

‎packages/sanity/src/structure/comments/src/components/list/CommentsListItem.tsx

+10-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {type CommentsSelectedPath} from '../../context'
1111
import {commentIntentIfDiffers, hasCommentMessageValue} from '../../helpers'
1212
import {applyCommentIdAttr} from '../../hooks'
1313
import {
14-
type CommentCreatePayload,
14+
type CommentBaseCreatePayload,
1515
type CommentDocument,
1616
type CommentMessage,
1717
type CommentReactionOption,
@@ -88,7 +88,7 @@ interface CommentsListItemProps {
8888
onKeyDown?: (event: React.KeyboardEvent<Element>) => void
8989
onPathSelect?: (nextPath: CommentsSelectedPath) => void
9090
onReactionSelect?: (id: string, reaction: CommentReactionOption) => void
91-
onReply: (payload: CommentCreatePayload) => void
91+
onReply: (payload: CommentBaseCreatePayload) => void
9292
onStatusChange?: (id: string, status: CommentStatus) => void
9393
parentComment: CommentDocument
9494
readOnly?: boolean
@@ -132,15 +132,18 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
132132
const handleMouseLeave = useCallback(() => setMouseOver(false), [])
133133

134134
const handleReplySubmit = useCallback(() => {
135-
const nextComment: CommentCreatePayload = {
136-
fieldPath: parentComment.target.path.field,
135+
const nextComment: CommentBaseCreatePayload = {
137136
message: value,
138137
parentCommentId: parentComment._id,
139138
status: parentComment?.status || 'open',
140139
// Since this is a reply to an existing comment, we use the same thread ID as the parent
141140
threadId: parentComment.threadId,
142141
// A new comment will not have any reactions
143142
reactions: EMPTY_ARRAY,
143+
144+
payload: {
145+
fieldPath: parentComment.target.path?.field || '',
146+
},
144147
}
145148

146149
onReply?.(nextComment)
@@ -149,7 +152,7 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
149152
onReply,
150153
parentComment._id,
151154
parentComment?.status,
152-
parentComment.target.path.field,
155+
parentComment.target.path?.field,
153156
parentComment.threadId,
154157
value,
155158
])
@@ -198,12 +201,12 @@ export const CommentsListItem = React.memo(function CommentsListItem(props: Comm
198201
if (!isTopLayer) return
199202

200203
onPathSelect?.({
201-
fieldPath: parentComment.target.path.field,
204+
fieldPath: parentComment.target.path?.field || '',
202205
origin: 'inspector',
203206
threadId: parentComment.threadId,
204207
})
205208
},
206-
[isTopLayer, onPathSelect, parentComment.target.path.field, parentComment.threadId],
209+
[isTopLayer, onPathSelect, parentComment.target.path?.field, parentComment.threadId],
207210
)
208211

209212
const handleExpand = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {

‎packages/sanity/src/structure/comments/src/context/comments/CommentsProvider.tsx

+9-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {useCommentsStore} from '../../store'
1919
import {
2020
type CommentPostPayload,
2121
type CommentStatus,
22+
type CommentsType,
2223
type CommentThreadItem,
2324
type CommentUpdatePayload,
2425
} from '../../types'
@@ -46,6 +47,8 @@ export interface CommentsProviderProps {
4647
children: ReactNode
4748
documentId: string
4849
documentType: string
50+
type: CommentsType
51+
sortOrder: 'asc' | 'desc'
4952

5053
isCommentsOpen?: boolean
5154
onCommentsOpen?: () => void
@@ -58,7 +61,8 @@ type TransactionId = string
5861
* @beta
5962
*/
6063
export const CommentsProvider = memo(function CommentsProvider(props: CommentsProviderProps) {
61-
const {children, documentId, documentType, isCommentsOpen, onCommentsOpen} = props
64+
const {children, documentId, documentType, isCommentsOpen, onCommentsOpen, sortOrder, type} =
65+
props
6266
const commentsEnabled = useCommentsEnabled()
6367
const [status, setStatus] = useState<CommentStatus>('open')
6468
const {client, createAddonDataset, isCreatingDataset} = useAddonDataset()
@@ -128,25 +132,21 @@ export const CommentsProvider = memo(function CommentsProvider(props: CommentsPr
128132

129133
const threadItemsByStatus: ThreadItemsByStatus = useMemo(() => {
130134
if (!schemaType || !currentUser) return EMPTY_COMMENTS_DATA
131-
// Since we only make one query to get all comments using the order `_createdAt desc` – we
132-
// can't know for sure that the comments added through the real time listener will be in the
133-
// correct order. In order to avoid that comments are out of order, we make an additional
134-
// sort here. The comments can be out of order if e.g a comment creation fails and is retried
135-
// later.
136-
const sorted = orderBy(data, ['_createdAt'], ['desc'])
135+
const sorted = orderBy(data, ['_createdAt'], [sortOrder])
137136

138137
const items = buildCommentThreadItems({
139138
comments: sorted,
140-
schemaType,
141139
currentUser,
142140
documentValue,
141+
schemaType,
142+
type,
143143
})
144144

145145
return {
146146
open: items.filter((item) => item.parentComment.status === 'open'),
147147
resolved: items.filter((item) => item.parentComment.status === 'resolved'),
148148
}
149-
}, [currentUser, data, documentValue, schemaType])
149+
}, [currentUser, data, documentValue, schemaType, sortOrder, type])
150150

151151
const getThreadLength = useCallback(
152152
(threadId: string) => {

‎packages/sanity/src/structure/comments/src/hooks/use-comment-operations/createOperation.ts

+85-48
Original file line numberDiff line numberDiff line change
@@ -54,69 +54,106 @@ export async function createOperation(props: CreateOperationProps): Promise<void
5454
// but the request failed. In that case, we'll reuse the id when retrying to
5555
// create the comment.
5656
const commentId = comment?.id || uuid()
57-
5857
const authorId = currentUser.id
5958

6059
// Get the current thread length of the thread the comment is being added to.
6160
// We add 1 to the length to account for the comment being added.
6261
const currentThreadLength = (getThreadLength?.(comment.threadId) || 0) + 1
6362

64-
const {
65-
documentTitle = '',
66-
url = '',
67-
workspaceTitle = '',
68-
} = getNotificationValue({commentId}) || {}
69-
70-
const notification: CommentContext['notification'] = {
71-
currentThreadLength,
72-
documentTitle,
73-
url,
74-
workspaceTitle,
75-
}
76-
77-
const intent = getIntent?.({id: documentId, type: documentType, path: comment.fieldPath})
78-
79-
const nextComment: CommentPostPayload = {
80-
_id: commentId,
81-
_type: 'comment',
82-
authorId,
83-
lastEditedAt: undefined,
84-
message: comment.message,
85-
parentCommentId: comment.parentCommentId,
86-
status: comment.status,
87-
threadId: comment.threadId,
88-
89-
context: {
90-
payload: {
91-
workspace,
63+
let nextComment: CommentPostPayload | undefined
64+
65+
if (comment.type === 'task') {
66+
nextComment = {
67+
_id: commentId,
68+
_type: 'comment',
69+
authorId,
70+
message: comment.message,
71+
lastEditedAt: undefined,
72+
parentCommentId: comment.parentCommentId,
73+
status: comment.status,
74+
threadId: comment.threadId,
75+
reactions: comment.reactions,
76+
77+
context: {
78+
payload: {
79+
workspace,
80+
},
81+
notification: undefined, // TODO: add task notification data
82+
tool: activeTool?.name || '',
9283
},
93-
intent,
94-
notification,
95-
tool: activeTool?.name || '',
96-
},
9784

98-
reactions: [],
85+
target: {
86+
document: {
87+
_ref: documentId,
88+
_type: 'reference',
89+
weak: true,
90+
},
91+
documentType,
92+
},
93+
}
9994

100-
contentSnapshot: comment.contentSnapshot,
95+
return
96+
}
10197

102-
target: {
103-
documentRevisionId: documentRevisionId || '',
98+
if (comment.type === 'field') {
99+
const {
100+
documentTitle = '',
101+
url = '',
102+
workspaceTitle = '',
103+
} = getNotificationValue({commentId}) || {}
104+
105+
const notification: CommentContext['notification'] = {
106+
currentThreadLength,
107+
documentTitle,
108+
url,
109+
workspaceTitle,
110+
}
104111

105-
path: {
106-
field: comment.fieldPath,
107-
selection: comment.selection,
112+
const intent = getIntent?.({id: documentId, type: documentType, path: comment.fieldPath})
113+
114+
nextComment = {
115+
_id: commentId,
116+
_type: 'comment',
117+
authorId,
118+
message: comment.message,
119+
lastEditedAt: undefined,
120+
parentCommentId: comment.parentCommentId,
121+
status: comment.status,
122+
threadId: comment.threadId,
123+
reactions: comment.reactions,
124+
125+
context: {
126+
payload: {
127+
workspace,
128+
},
129+
intent,
130+
notification,
131+
tool: activeTool?.name || '',
108132
},
109-
document: {
110-
_dataset: dataset,
111-
_projectId: projectId,
112-
_ref: documentId,
113-
_type: 'crossDatasetReference',
114-
_weak: true,
133+
134+
contentSnapshot: comment.contentSnapshot,
135+
136+
target: {
137+
documentRevisionId: documentRevisionId || '',
138+
139+
path: {
140+
field: comment.fieldPath,
141+
selection: comment.selection,
142+
},
143+
document: {
144+
_dataset: dataset,
145+
_projectId: projectId,
146+
_ref: documentId,
147+
_type: 'crossDatasetReference',
148+
_weak: true,
149+
},
150+
documentType,
115151
},
116-
documentType,
117-
},
152+
}
118153
}
119154

155+
if (!nextComment) return
156+
120157
onCreate?.(nextComment)
121158

122159
// If we don't have a client, that means that the dataset doesn't have an addon dataset.

‎packages/sanity/src/structure/comments/src/store/useCommentsStore.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,7 @@ const LISTEN_OPTIONS: ListenOptions = {
3333
export const SORT_FIELD = '_createdAt'
3434
export const SORT_ORDER = 'desc'
3535

36-
const QUERY_FILTERS = [
37-
`_type == "comment"`,
38-
`target.document._ref == $documentId`,
39-
`defined(target.path)`,
40-
]
36+
const QUERY_FILTERS = [`_type == "comment"`, `target.document._ref == $documentId`]
4137

4238
const QUERY_PROJECTION = `{
4339
_createdAt,

‎packages/sanity/src/structure/comments/src/types.ts

+55-16
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ export interface CommentReactionItem {
168168
_optimisticState?: 'added' | 'removed'
169169
}
170170

171+
/**
172+
* @beta
173+
* @hidden
174+
*/
175+
export type CommentsType = 'field' | 'task'
176+
171177
/**
172178
* The state is used to track the state of the comment (e.g. if it failed to be created, etc.)
173179
* It is a local value and is not stored on the server.
@@ -204,17 +210,23 @@ export interface CommentDocument {
204210
contentSnapshot?: unknown
205211

206212
target: {
207-
path: CommentPath
213+
path?: CommentPath
208214

209-
documentRevisionId: string
215+
documentRevisionId?: string
210216
documentType: string
211-
document: {
212-
_dataset: string
213-
_projectId: string
214-
_ref: string
215-
_type: 'crossDatasetReference'
216-
_weak: boolean
217-
}
217+
document:
218+
| {
219+
_dataset: string
220+
_projectId: string
221+
_ref: string
222+
_type: 'crossDatasetReference'
223+
_weak: boolean
224+
}
225+
| {
226+
_ref: string
227+
_type: 'reference'
228+
weak: boolean
229+
}
218230
}
219231
}
220232

@@ -228,21 +240,48 @@ export type CommentPostPayload = Omit<CommentDocument, '_rev' | '_updatedAt' | '
228240
* @beta
229241
* @hidden
230242
*/
231-
export interface CommentCreatePayload {
232-
contentSnapshot?: CommentDocument['contentSnapshot']
233-
/**
234-
* The stringified path to the field where the comment was created.
235-
*/
236-
fieldPath: string
243+
export interface CommentBaseCreatePayload {
237244
id?: CommentDocument['_id']
238245
message: CommentDocument['message']
239246
parentCommentId: CommentDocument['parentCommentId']
240247
reactions: CommentDocument['reactions']
241-
selection?: CommentPathSelection
242248
status: CommentDocument['status']
243249
threadId: CommentDocument['threadId']
250+
251+
payload?: {
252+
fieldPath: string
253+
}
244254
}
245255

256+
/**
257+
* @beta
258+
* @hidden
259+
*/
260+
export interface CommentTaskCreatePayload extends CommentBaseCreatePayload {
261+
// ...
262+
type: 'task'
263+
}
264+
265+
/**
266+
* @beta
267+
* @hidden
268+
*/
269+
export interface CommentFieldCreatePayload extends CommentBaseCreatePayload {
270+
type: 'field'
271+
contentSnapshot?: CommentDocument['contentSnapshot']
272+
/**
273+
* The stringified path to the field where the comment was created.
274+
*/
275+
fieldPath: string
276+
selection?: CommentPathSelection
277+
}
278+
279+
/**
280+
* @beta
281+
* @hidden
282+
*/
283+
export type CommentCreatePayload = CommentTaskCreatePayload | CommentFieldCreatePayload
284+
246285
/**
247286
* @beta
248287
* @hidden

‎packages/sanity/src/structure/comments/src/utils/buildCommentThreadItems.ts

+79-48
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {type SanityDocument} from '@sanity/client'
22
import {type CurrentUser, type SchemaType} from '@sanity/types'
33

44
import {isTextSelectionComment} from '../helpers'
5-
import {type CommentDocument, type CommentThreadItem} from '../types'
5+
import {type CommentDocument, type CommentsType, type CommentThreadItem} from '../types'
66
import {buildCommentBreadcrumbs} from './buildCommentBreadcrumbs'
77

88
const EMPTY_ARRAY: [] = []
@@ -12,6 +12,7 @@ interface BuildCommentThreadItemsProps {
1212
currentUser: CurrentUser
1313
documentValue: Partial<SanityDocument> | null
1414
schemaType: SchemaType
15+
type: CommentsType
1516
}
1617

1718
/**
@@ -21,55 +22,85 @@ interface BuildCommentThreadItemsProps {
2122
* returned array.
2223
*/
2324
export function buildCommentThreadItems(props: BuildCommentThreadItemsProps): CommentThreadItem[] {
24-
const {comments, currentUser, documentValue, schemaType} = props
25+
const {comments, currentUser, documentValue, schemaType, type} = props
2526
const parentComments = comments?.filter((c) => !c.parentCommentId)
2627

27-
const items = parentComments.map((parentComment) => {
28-
const crumbs = buildCommentBreadcrumbs({
29-
currentUser,
30-
documentValue,
31-
fieldPath: parentComment.target.path.field,
32-
schemaType,
28+
// If the comments are "task" comments, just group them together as thread items
29+
// without any validation of the comments.
30+
if (type === 'task') {
31+
const taskCommentItems = parentComments.map((parentComment) => {
32+
const replies = comments?.filter((r) => r.parentCommentId === parentComment._id)
33+
const commentsCount = [parentComment, ...replies].length
34+
const hasReferencedValue = false
35+
36+
const item: CommentThreadItem = {
37+
commentsCount,
38+
parentComment,
39+
replies,
40+
threadId: parentComment.threadId,
41+
hasReferencedValue,
42+
breadcrumbs: EMPTY_ARRAY,
43+
fieldPath: '',
44+
}
45+
46+
return item
3347
})
3448

35-
// NOTE: Keep this code commented out for now as we might want to use it later.
36-
let hasTextSelection = false
37-
38-
// If the comment is a text selection comment, we need to make sure that
39-
// we can successfully build a range decoration selection from it.
40-
if (isTextSelectionComment(parentComment)) {
41-
hasTextSelection = Boolean(
42-
parentComment.target.path.selection &&
43-
parentComment.target.path.selection.value.some((v) => v.text),
44-
)
45-
}
46-
47-
// Check if the comment has an invalid breadcrumb. The breadcrumbs can be invalid if:
48-
// - The field is hidden by conditional fields
49-
// - The field is not found in the schema type
50-
// - The field is not found in the document value (array items only)
51-
const hasInvalidBreadcrumb = crumbs.some((bc) => bc.invalid)
52-
53-
// If the comment has an invalid breadcrumb or selection, we will omit it from the list.
54-
if (hasInvalidBreadcrumb) return undefined
55-
56-
const replies = comments?.filter((r) => r.parentCommentId === parentComment._id)
57-
const commentsCount = [parentComment, ...replies].length
58-
const hasReferencedValue = hasTextSelection
59-
60-
const item: CommentThreadItem = {
61-
breadcrumbs: crumbs,
62-
commentsCount,
63-
fieldPath: parentComment.target.path.field,
64-
parentComment,
65-
replies,
66-
threadId: parentComment.threadId,
67-
hasReferencedValue,
68-
}
69-
70-
return item
71-
})
72-
73-
// We use the `Boolean` function to filter out any `undefined` items from the array.
74-
return items.filter(Boolean) as CommentThreadItem[]
49+
return taskCommentItems
50+
}
51+
52+
// If the comments are "field" comments, we want to validate them against
53+
// the document value and schema type.
54+
if (type === 'field') {
55+
const fieldCommentItems = parentComments.map((parentComment) => {
56+
const crumbs = buildCommentBreadcrumbs({
57+
currentUser,
58+
documentValue,
59+
fieldPath: parentComment.target.path?.field || '',
60+
schemaType,
61+
})
62+
63+
// NOTE: Keep this code commented out for now as we might want to use it later.
64+
let hasTextSelection = false
65+
66+
// If the comment is a text selection comment, we need to make sure that
67+
// we can successfully build a range decoration selection from it.
68+
if (isTextSelectionComment(parentComment)) {
69+
hasTextSelection = Boolean(
70+
parentComment.target.path?.selection &&
71+
parentComment.target.path.selection.value.some((v) => v.text),
72+
)
73+
}
74+
75+
// Check if the comment has an invalid breadcrumb. The breadcrumbs can be invalid if:
76+
// - The field is hidden by conditional fields
77+
// - The field is not found in the schema type
78+
// - The field is not found in the document value (array items only)
79+
const hasInvalidBreadcrumb = crumbs.some((bc) => bc.invalid)
80+
81+
// If the comment has an invalid breadcrumb or selection, we will omit it from the list.
82+
if (hasInvalidBreadcrumb) return undefined
83+
84+
const replies = comments?.filter((r) => r.parentCommentId === parentComment._id)
85+
const commentsCount = [parentComment, ...replies].length
86+
const hasReferencedValue = hasTextSelection
87+
88+
const item: CommentThreadItem = {
89+
breadcrumbs: crumbs,
90+
commentsCount,
91+
fieldPath: parentComment.target.path?.field || '',
92+
parentComment,
93+
replies,
94+
threadId: parentComment.threadId,
95+
hasReferencedValue,
96+
}
97+
98+
return item
99+
})
100+
101+
// We use the `Boolean` function to filter out any `undefined` items from the array.
102+
return fieldCommentItems.filter(Boolean) as CommentThreadItem[]
103+
}
104+
105+
return EMPTY_ARRAY
75106
}

‎packages/sanity/src/structure/comments/src/utils/inline-comments/buildRangeDecorationSelectionsFromComments.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function buildRangeDecorationSelectionsFromComments(
7676
const decorators: BuildCommentsRangeDecorationsResultItem[] = []
7777

7878
textSelections.forEach((comment) => {
79-
comment.target.path.selection?.value.forEach((selectionMember) => {
79+
comment.target.path?.selection?.value.forEach((selectionMember) => {
8080
const matchedBlock = value.find((block) => block._key === selectionMember._key)
8181
if (!matchedBlock || !isPortableTextTextBlock(matchedBlock)) {
8282
return

0 commit comments

Comments
 (0)
Please sign in to comment.