diff --git a/dev/test-studio/schema/debug/comments.ts b/dev/test-studio/schema/debug/comments.ts index bfac01f9312..3846857aa40 100644 --- a/dev/test-studio/schema/debug/comments.ts +++ b/dev/test-studio/schema/debug/comments.ts @@ -36,13 +36,6 @@ export const commentsDebug = defineType({ }, ], }, - { - name: 'image', - type: 'image', - title: 'Image title', - hidden: ({document}) => Boolean(document?.hideFields), - description: DESCRIPTION, - }, { type: 'array', name: 'arrayOfObjects', @@ -111,5 +104,12 @@ export const commentsDebug = defineType({ }, ], }, + { + name: 'image', + type: 'image', + title: 'Image title', + hidden: ({document}) => Boolean(document?.hideFields), + description: DESCRIPTION, + }, ], }) diff --git a/packages/sanity/src/core/comments/__workshop__/CommentDeleteDialogStory.tsx b/packages/sanity/src/core/comments/__workshop__/CommentDeleteDialogStory.tsx new file mode 100644 index 00000000000..b1eed81b61c --- /dev/null +++ b/packages/sanity/src/core/comments/__workshop__/CommentDeleteDialogStory.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import {useAction, useBoolean} from '@sanity/ui-workshop' +import {CommentDeleteDialog} from '../components' + +export default function CommentDeleteDialogStory() { + const isParent = useBoolean('Is parent', false, 'Props') || false + const error = useBoolean('Error', false, 'Props') || false + const loading = useBoolean('Loading', false, 'Props') || false + + return ( + + ) +} diff --git a/packages/sanity/src/core/comments/__workshop__/CommentsListStory.tsx b/packages/sanity/src/core/comments/__workshop__/CommentsListStory.tsx index 2a6d946abe9..9bd42213eee 100644 --- a/packages/sanity/src/core/comments/__workshop__/CommentsListStory.tsx +++ b/packages/sanity/src/core/comments/__workshop__/CommentsListStory.tsx @@ -156,6 +156,9 @@ export default function CommentsListStory() { onEdit={handleEdit} onReply={handleReplySubmit} status={status} + onCreateRetry={() => { + // ... + }} onNewThreadCreate={() => { // ... }} diff --git a/packages/sanity/src/core/comments/__workshop__/CommentsProviderStory.tsx b/packages/sanity/src/core/comments/__workshop__/CommentsProviderStory.tsx index 09726b09112..69e1c1df5e6 100644 --- a/packages/sanity/src/core/comments/__workshop__/CommentsProviderStory.tsx +++ b/packages/sanity/src/core/comments/__workshop__/CommentsProviderStory.tsx @@ -46,6 +46,9 @@ function Inner() { onNewThreadCreate={create.execute} onReply={create.execute} status="open" + onCreateRetry={() => { + // ... + }} /> ) } diff --git a/packages/sanity/src/core/comments/__workshop__/index.ts b/packages/sanity/src/core/comments/__workshop__/index.ts index 9c0ab1e89c7..b63922ba9a3 100644 --- a/packages/sanity/src/core/comments/__workshop__/index.ts +++ b/packages/sanity/src/core/comments/__workshop__/index.ts @@ -30,5 +30,10 @@ export default defineScope({ title: 'MentionsMenu', component: lazy(() => import('./MentionsMenuStory')), }, + { + name: 'comment-delete-dialog', + title: 'CommentDeleteDialog', + component: lazy(() => import('./CommentDeleteDialogStory')), + }, ], }) diff --git a/packages/sanity/src/core/comments/components/CommentDeleteDialog.tsx b/packages/sanity/src/core/comments/components/CommentDeleteDialog.tsx index a943b3aa7d7..371d788a992 100644 --- a/packages/sanity/src/core/comments/components/CommentDeleteDialog.tsx +++ b/packages/sanity/src/core/comments/components/CommentDeleteDialog.tsx @@ -1,5 +1,6 @@ import {Dialog, Grid, Button, Stack, Text} from '@sanity/ui' import React, {useCallback} from 'react' +import {TextWithTone} from '../../components' const DIALOG_COPY: Record< 'thread' | 'comment', @@ -60,10 +61,14 @@ export function CommentDeleteDialog(props: CommentDeleteDialogProps) { } > - {error && Something went wrong} - - + {body} + + {error && ( + + An error occurred while deleting the comment. Please try again. + + )} ) diff --git a/packages/sanity/src/core/comments/components/avatars/SpacerAvatar.tsx b/packages/sanity/src/core/comments/components/avatars/SpacerAvatar.tsx index fed96082ac4..232080e8dd9 100644 --- a/packages/sanity/src/core/comments/components/avatars/SpacerAvatar.tsx +++ b/packages/sanity/src/core/comments/components/avatars/SpacerAvatar.tsx @@ -1,7 +1,9 @@ import React from 'react' +export const AVATAR_HEIGHT = 25 + const INLINE_STYLE: React.CSSProperties = { - minWidth: 25, + minWidth: AVATAR_HEIGHT, } /** diff --git a/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx b/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx index 5cd91691e32..040f0b781c6 100644 --- a/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx +++ b/packages/sanity/src/core/comments/components/list/CommentThreadLayout.tsx @@ -43,6 +43,9 @@ export function CommentThreadLayout(props: CommentThreadLayoutProps) { } = props const createNewThreadInputRef = useRef(null) const [displayNewThreadInput, setDisplayNewThreadInput] = useState(false) + const [newThreadButtonElement, setNewThreadButtonElement] = useState( + null, + ) const onCreateNewThreadClick = useCallback(() => { setDisplayNewThreadInput(true) @@ -75,8 +78,11 @@ export function CommentThreadLayout(props: CommentThreadLayoutProps) { onNewThreadCreate?.(nextComment) setDisplayNewThreadInput(false) + + // When the new thread is created, we focus the button again + newThreadButtonElement?.focus() }, - [onNewThreadCreate, path], + [newThreadButtonElement, onNewThreadCreate, path], ) return ( @@ -114,6 +120,7 @@ export function CommentThreadLayout(props: CommentThreadLayoutProps) { mode="bleed" onClick={onCreateNewThreadClick} padding={2} + ref={setNewThreadButtonElement} /> )} diff --git a/packages/sanity/src/core/comments/components/list/CommentsList.tsx b/packages/sanity/src/core/comments/components/list/CommentsList.tsx index 033f43bf644..d18a4dece8a 100644 --- a/packages/sanity/src/core/comments/components/list/CommentsList.tsx +++ b/packages/sanity/src/core/comments/components/list/CommentsList.tsx @@ -47,6 +47,7 @@ export interface CommentsListProps { error: Error | null loading: boolean mentionOptions: MentionOptionsHookValue + onCreateRetry: (id: string) => void onDelete: (id: string) => void onEdit: (id: string, payload: CommentEditPayload) => void onNewThreadCreate: (payload: CommentCreatePayload) => void @@ -78,6 +79,7 @@ export const CommentsList = forwardRef(fu error, loading, mentionOptions, + onCreateRetry, onDelete, onEdit, onNewThreadCreate, @@ -202,12 +204,18 @@ export const CommentsList = forwardRef(fu // We use slice() to avoid mutating the original array. const replies = item.replies.slice().reverse() + const canReply = + status === 'open' && + item.parentComment._state?.type !== 'createError' && + item.parentComment._state?.type !== 'createRetrying' + return ( void onDelete: (id: string) => void onEdit: (id: string, payload: CommentEditPayload) => void onPathFocus?: (path: Path) => void @@ -60,6 +61,7 @@ export function CommentsListItem(props: CommentsListItemProps) { canReply, currentUser, mentionOptions, + onCreateRetry, onDelete, onEdit, onPathFocus, @@ -137,67 +139,77 @@ export function CommentsListItem(props: CommentsListItemProps) { return ( - - - + + ))} + + {canReply && ( + + )} diff --git a/packages/sanity/src/core/comments/components/list/CommentsListItemLayout.tsx b/packages/sanity/src/core/comments/components/list/CommentsListItemLayout.tsx index 8a76e4b95fc..b3224043ded 100644 --- a/packages/sanity/src/core/comments/components/list/CommentsListItemLayout.tsx +++ b/packages/sanity/src/core/comments/components/list/CommentsListItemLayout.tsx @@ -22,6 +22,7 @@ import { Box, TooltipDelayGroupProvider, TooltipDelayGroupProviderProps, + MenuButtonProps, } from '@sanity/ui' import React, {useCallback, useRef, useState} from 'react' import {CurrentUser, Path} from '@sanity/types' @@ -31,7 +32,7 @@ import * as PathUtils from '@sanity/util/paths' import {TimeAgoOpts, useTimeAgo} from '../../../hooks' import {useUser} from '../../../store' import {CommentMessageSerializer} from '../pte' -import {CommentInput} from '../pte/comment-input' +import {CommentInput, CommentInputHandle} from '../pte/comment-input' import { CommentDocument, CommentEditPayload, @@ -43,15 +44,29 @@ import {FLEX_GAP} from '../constants' import {useDidUpdate} from '../../../form' import {useCommentHasChanged} from '../../helpers' import {TextTooltip} from '../TextTooltip' -import {CommentsAvatar, SpacerAvatar} from '../avatars' +import {AVATAR_HEIGHT, CommentsAvatar, SpacerAvatar} from '../avatars' const TOOLTIP_GROUP_DELAY: TooltipDelayGroupProviderProps['delay'] = {open: 500} const SKELETON_INLINE_STYLE: React.CSSProperties = {width: '50%'} +const POPOVER_PROPS: MenuButtonProps['popover'] = {placement: 'bottom-end'} const TimeText = styled(Text)` min-width: max-content; ` +const InnerStack = styled(Stack)` + transition: opacity 200ms ease; + + &[data-muted='true'] { + transition: unset; + opacity: 0.5; + } +` + +const ErrorFlex = styled(Flex)` + min-height: ${AVATAR_HEIGHT}px; +` + const FloatingLayer = styled(Layer)(({theme}) => { const {space} = theme.sanity @@ -65,6 +80,13 @@ const FloatingLayer = styled(Layer)(({theme}) => { ` }) +const RetryCardButton = styled(Card)` + // Add not on hover + &:not(:hover) { + background-color: transparent; + } +` + const FloatingCard = styled(Card)(({theme}) => { const {space} = theme.sanity @@ -111,8 +133,11 @@ interface CommentsListItemLayoutProps { canEdit?: boolean comment: CommentDocument currentUser: CurrentUser + hasError?: boolean isParent?: boolean + isRetrying?: boolean mentionOptions: MentionOptionsHookValue + onCreateRetry?: (id: string) => void onDelete: (id: string) => void onEdit: (id: string, message: CommentEditPayload) => void onPathFocus?: (path: Path) => void @@ -127,8 +152,11 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { canEdit, comment, currentUser, + hasError, isParent, + isRetrying, mentionOptions, + onCreateRetry, onDelete, onEdit, onPathFocus, @@ -146,6 +174,8 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { const hasChanges = useCommentHasChanged(value) + const commentInputRef = useRef(null) + const createdDate = _createdAt ? new Date(_createdAt) : new Date() const createdTimeAgo = useTimeAgo(createdDate, TIME_AGO_OPTS) const formattedCreatedAt = format(createdDate, 'PPPPp') @@ -155,6 +185,12 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { const handleMenuOpen = useCallback(() => setMenuOpen(true), []) const handleMenuClose = useCallback(() => setMenuOpen(false), []) + const displayError = hasError || isRetrying + + const handleCreateRetry = useCallback(() => { + onCreateRetry?.(_id) + }, [_id, onCreateRetry]) + const cancelEdit = useCallback(() => { setIsEditing(false) setValue(startMessage.current) @@ -200,6 +236,7 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { useClickOutside(() => { if (!hasChanges) { cancelEdit() + commentInputRef.current?.blur() } }, [rootElement]) @@ -218,138 +255,173 @@ export function CommentsListItemLayout(props: CommentsListItemLayoutProps) { return ( - - {avatar} - - - - {name} - - - - {createdTimeAgo} - - - {formattedLastEditAt && ( - - (edited) - + + + {avatar} + + + + {name} + + {!displayError && ( + + + {createdTimeAgo} + + + {formattedLastEditAt && ( + + (edited) + + )} + )} - - {!isEditing && ( - -