diff --git a/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx b/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx index d392929b523..2ffea4e85e2 100644 --- a/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx +++ b/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx @@ -21,7 +21,6 @@ interface CommentThreadLayoutProps { canCreateNewThread: boolean children: React.ReactNode currentUser: CurrentUser - threadId: string mentionOptions: MentionOptionsHookValue onNewThreadCreate: (payload: CommentCreatePayload) => void path: Path @@ -33,7 +32,6 @@ export function CommentThreadLayout(props: CommentThreadLayoutProps) { canCreateNewThread, children, currentUser, - threadId, mentionOptions, onNewThreadCreate, path, @@ -77,7 +75,7 @@ export function CommentThreadLayout(props: CommentThreadLayoutProps) { ) return ( - + diff --git a/packages/sanity/src/core/comments/components/list/CommentsList.tsx b/packages/sanity/src/core/comments/components/list/CommentsList.tsx index 27954182ca1..813cf6aa9ab 100644 --- a/packages/sanity/src/core/comments/components/list/CommentsList.tsx +++ b/packages/sanity/src/core/comments/components/list/CommentsList.tsx @@ -1,4 +1,3 @@ -/* eslint-disable max-nested-callbacks */ import React, {forwardRef, useCallback, useImperativeHandle, useMemo, useState} from 'react' import {BoundaryElementProvider, Container, Flex, Spinner, Stack, Text} from '@sanity/ui' import {CurrentUser, Path} from '@sanity/types' @@ -15,6 +14,12 @@ import {CommentsListItem} from './CommentsListItem' import {CommentThreadLayout} from './CommentThreadLayout' import {EMPTY_STATE_MESSAGES} from './constants' +const SCROLL_INTO_VIEW_OPTIONS: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + inline: 'center', +} + interface GroupedComments { [field: string]: CommentDocument[] } @@ -33,6 +38,17 @@ function groupComments(comments: CommentDocument[]) { }, {} as GroupedComments) } +function getReplies(parentCommentId: string, group: CommentDocument[]) { + const replies = group.filter((c) => c.parentCommentId === parentCommentId) + + // The default sort order is by date, descending (newest first). + // However, inside a thread, we want the order to be ascending (oldest first). + // So we reverse the array here. + const orderedReplies = [...replies].reverse() + + return orderedReplies +} + /** * @beta * @hidden @@ -89,12 +105,7 @@ export const CommentsList = forwardRef(fu const scrollToComment = useCallback( (id: string) => { const commentElement = boundaryElement?.querySelector(`[data-comment-id="${id}"]`) - - commentElement?.scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center', - }) + commentElement?.scrollIntoView(SCROLL_INTO_VIEW_OPTIONS) }, [boundaryElement], ) @@ -114,9 +125,8 @@ export const CommentsList = forwardRef(fu // 1. Get all comments that are not replies and are in the current view (open or resolved) .filter((c) => !c.parentCommentId) // 2. Get all replies to each parent comment and add them to the array - .map((c) => { - return [c, ...comments.filter((c2) => c2.parentCommentId === c._id)] - }) + // eslint-disable-next-line max-nested-callbacks + .map((c) => [c, ...comments.filter((c2) => c2.parentCommentId === c._id)]) .flat() return Object.entries(groupComments(filteredComments)) @@ -186,45 +196,50 @@ export const CommentsList = forwardRef(fu {groupedComments.map(([fieldPath, group]) => { const parentComments = group.filter((c) => !c.parentCommentId) - const breadcrumbs = buildCommentBreadcrumbs?.(fieldPath) + // The threadId is used to identify the thread in the DOM, so we can scroll to it. + // todo: validate this approach const threadId = group[0].threadId + const breadcrumbs = buildCommentBreadcrumbs?.(fieldPath) + const hasInvalidBreadcrumb = breadcrumbs?.some((b) => b.invalid === true) + + // If the breadcrumb is invalid, the field might have been remove from the + // the schema, or an array item might have been removed. In that case, we don't + // want to render any button to open the field. + const _onPathFocus = hasInvalidBreadcrumb ? undefined : onPathFocus + return ( - - {parentComments.map((comment) => { - const replies = group.filter((c) => c.parentCommentId === comment._id) - - // The default sort order is by date, descending (newest first). - // However, inside a thread, we want the order to be ascending (oldest first). - // So we reverse the array here. - const orderedReplies = [...replies].reverse() - - return ( - - ) - })} - + + + {parentComments.map((comment) => { + const replies = getReplies(comment._id, group) + + return ( + + ) + })} + + ) })} diff --git a/packages/sanity/src/core/comments/hooks/useCommentsEnabled.ts b/packages/sanity/src/core/comments/hooks/useCommentsEnabled.ts index ce8b4f18928..645ab40b721 100644 --- a/packages/sanity/src/core/comments/hooks/useCommentsEnabled.ts +++ b/packages/sanity/src/core/comments/hooks/useCommentsEnabled.ts @@ -3,17 +3,22 @@ import {useFeatureEnabled} from '../../hooks' import {useSource} from '../../studio' import {getPublishedId} from '../../util' -interface Enabled { +interface Disabled { isEnabled: false - reason: 'disabled-by-config' | 'plan-upgrade-required' + reason: 'disabled-by-config' | 'plan-upgrade-required' | 'loading' } -interface Disabled { +interface Enabled { isEnabled: true reason: null } -type CommentsEnabled = Enabled | Disabled +interface Loading { + isEnabled: false + reason: 'loading' +} + +type CommentsEnabled = Enabled | Disabled | Loading interface CommentsEnabledHookOptions { documentId: string @@ -34,14 +39,21 @@ export function useCommentsEnabled(opts: CommentsEnabledHookOptions): CommentsEn const enabledFromConfig = enabled({documentType, documentId: getPublishedId(documentId)}) const commentsEnabled = useMemo(() => { - if (!featureEnabled && !isLoading) { + if (isLoading) { + return { + isEnabled: false, + reason: 'loading', + } satisfies CommentsEnabled + } + + if (!featureEnabled) { return { isEnabled: false, reason: 'plan-upgrade-required', } satisfies CommentsEnabled } - if (!enabledFromConfig || isLoading) { + if (!enabledFromConfig) { return { isEnabled: false, reason: 'disabled-by-config', diff --git a/packages/sanity/src/core/comments/utils/buildCommentBreadcrumbs.ts b/packages/sanity/src/core/comments/utils/buildCommentBreadcrumbs.ts index d4e130f7f5b..40c89274fdc 100644 --- a/packages/sanity/src/core/comments/utils/buildCommentBreadcrumbs.ts +++ b/packages/sanity/src/core/comments/utils/buildCommentBreadcrumbs.ts @@ -25,6 +25,7 @@ function getSchemaField( return field } } + return undefined } @@ -36,7 +37,7 @@ function findArrayItemIndex(array: unknown[], pathSegment: any): number | false return index === -1 ? false : index } -interface BuildCommentBreadcrumbs { +interface BuildCommentBreadcrumbsProps { documentValue: unknown fieldPath: string schemaType: SchemaType @@ -46,7 +47,7 @@ interface BuildCommentBreadcrumbs { * @beta * @hidden */ -export function buildCommentBreadcrumbs(props: BuildCommentBreadcrumbs): CommentBreadcrumbs { +export function buildCommentBreadcrumbs(props: BuildCommentBreadcrumbsProps): CommentBreadcrumbs { const {schemaType, fieldPath, documentValue} = props const paths = PathUtils.fromString(fieldPath) const fieldPaths: CommentBreadcrumbs = [] @@ -56,17 +57,7 @@ export function buildCommentBreadcrumbs(props: BuildCommentBreadcrumbs): Comment const isArraySegment = seg.hasOwnProperty('_key') const field = getSchemaField(schemaType, currentPath) - if (!field && !isArraySegment) { - fieldPaths.push({ - invalid: true, - isArrayItem: false, - title: startCase(seg.toString()), - }) - - return - } - - if (field && !isArraySegment) { + if (field) { const title = getSchemaTypeTitle(field?.type) fieldPaths.push({ @@ -74,6 +65,8 @@ export function buildCommentBreadcrumbs(props: BuildCommentBreadcrumbs): Comment isArrayItem: false, title, }) + + return } if (isArraySegment) { @@ -86,6 +79,19 @@ export function buildCommentBreadcrumbs(props: BuildCommentBreadcrumbs): Comment isArrayItem: true, title: `#${Number(arrayItemIndex) + 1}`, }) + + return + } + + // This is usually the schema definition of an array item (ie inside of: []). + // todo: fix so that we get the defined titled instead of the field name. + // We need to add support for it in `getSchemaField`. + if (!isArraySegment) { + fieldPaths.push({ + invalid: false, + isArrayItem: false, + title: startCase(seg.toString()), + }) } }) diff --git a/packages/sanity/src/desk/comments/field/CommentField.tsx b/packages/sanity/src/desk/comments/field/CommentField.tsx index 008d84d2a6a..77ed1565ceb 100644 --- a/packages/sanity/src/desk/comments/field/CommentField.tsx +++ b/packages/sanity/src/desk/comments/field/CommentField.tsx @@ -14,6 +14,8 @@ import { useFieldCommentsCount, } from 'sanity' +const SCROLL_INTO_VIEW_OPTIONS: ScrollIntoViewOptions = {behavior: 'smooth', block: 'start'} + export function CommentField(props: FieldProps) { const {documentId, documentType} = useDocumentPane() @@ -52,7 +54,7 @@ function CommentFieldInner(props: FieldProps) { useEffect(() => { if (status === 'open' && inspector?.name === COMMENTS_INSPECTOR_NAME && shouldScrollToThread) { const threadId = comments.data.open.find( - (comment) => comment.target.path.field === props.path.toString(), + (comment) => comment.target?.path?.field === props.path.toString(), )?.threadId // Find the node in the DOM @@ -61,7 +63,7 @@ function CommentFieldInner(props: FieldProps) { // Scroll to the node if (node) { requestAnimationFrame(() => { - node.scrollIntoView({behavior: 'smooth', block: 'start'}) + node.scrollIntoView(SCROLL_INTO_VIEW_OPTIONS) }) }