Fix accept changes request for new review panel (#20956)

* Fix accept changes request for new review panel

* Implement pessimistic UI updates for new review panel (#20986)

* Implement pessimistic UI updates for new review panel

* deleteThread, reopenThread handlers

* use finally

* handleSubmit in useCallback

GitOrigin-RevId: 358181a6b5601ad1b3579e001564dfa4da67a81d
This commit is contained in:
Domagoj Kriskovic 2024-10-10 15:01:30 +02:00 committed by Copybot
parent 42a199043a
commit 4a3eed35d6
13 changed files with 313 additions and 137 deletions

View file

@ -29,6 +29,8 @@
"accept_all": "", "accept_all": "",
"accept_and_continue": "", "accept_and_continue": "",
"accept_change": "", "accept_change": "",
"accept_change_error_description": "",
"accept_change_error_title": "",
"accept_invitation": "", "accept_invitation": "",
"accept_or_reject_each_changes_individually": "", "accept_or_reject_each_changes_individually": "",
"accept_terms_and_conditions": "", "accept_terms_and_conditions": "",
@ -53,6 +55,8 @@
"add_another_token": "", "add_another_token": "",
"add_comma_separated_emails_help": "", "add_comma_separated_emails_help": "",
"add_comment": "", "add_comment": "",
"add_comment_error_message": "",
"add_comment_error_title": "",
"add_company_details": "", "add_company_details": "",
"add_email_address": "", "add_email_address": "",
"add_email_to_claim_features": "", "add_email_to_claim_features": "",
@ -303,6 +307,8 @@
"delete_authentication_token_info": "", "delete_authentication_token_info": "",
"delete_certificate": "", "delete_certificate": "",
"delete_comment": "", "delete_comment": "",
"delete_comment_error_message": "",
"delete_comment_error_title": "",
"delete_comment_message": "", "delete_comment_message": "",
"delete_comment_thread": "", "delete_comment_thread": "",
"delete_comment_thread_message": "", "delete_comment_thread_message": "",
@ -384,6 +390,8 @@
"easily_import_and_sync_your_references": "", "easily_import_and_sync_your_references": "",
"easily_manage_your_project_files_everywhere": "", "easily_manage_your_project_files_everywhere": "",
"edit": "", "edit": "",
"edit_comment_error_message": "",
"edit_comment_error_title": "",
"edit_dictionary": "", "edit_dictionary": "",
"edit_dictionary_empty": "", "edit_dictionary_empty": "",
"edit_dictionary_remove": "", "edit_dictionary_remove": "",
@ -1181,6 +1189,8 @@
"rename": "", "rename": "",
"rename_project": "", "rename_project": "",
"reopen": "", "reopen": "",
"reopen_comment_error_message": "",
"reopen_comment_error_title": "",
"replace_figure": "", "replace_figure": "",
"replace_from_another_project": "", "replace_from_another_project": "",
"replace_from_computer": "", "replace_from_computer": "",
@ -1201,6 +1211,8 @@
"resize": "", "resize": "",
"resolve": "", "resolve": "",
"resolve_comment": "", "resolve_comment": "",
"resolve_comment_error_message": "",
"resolve_comment_error_title": "",
"resolved_comments": "", "resolved_comments": "",
"restore": "", "restore": "",
"restore_file": "", "restore_file": "",

View file

@ -13,6 +13,8 @@ import { Button } from 'react-bootstrap'
import { ReviewPanelEntry } from './review-panel-entry' import { ReviewPanelEntry } from './review-panel-entry'
import { ThreadId } from '../../../../../types/review-panel/review-panel' import { ThreadId } from '../../../../../types/review-panel/review-panel'
import { Decoration } from '@codemirror/view' import { Decoration } from '@codemirror/view'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { debugConsole } from '@/utils/debugging'
export const ReviewPanelAddComment: FC<{ export const ReviewPanelAddComment: FC<{
docId: string docId: string
@ -25,8 +27,8 @@ export const ReviewPanelAddComment: FC<{
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const state = useCodeMirrorStateContext() const state = useCodeMirrorStateContext()
const { addComment } = useThreadsActionsContext() const { addComment } = useThreadsActionsContext()
const [error, setError] = useState<Error>()
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
const { showGenericMessageModal } = useModalsContext()
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
view.dispatch({ view.dispatch({
@ -35,22 +37,27 @@ export const ReviewPanelAddComment: FC<{
}, [view, value]) }, [view, value])
const submitForm = useCallback( const submitForm = useCallback(
message => { async message => {
setSubmitting(true) setSubmitting(true)
const content = view.state.sliceDoc(from, to) const content = view.state.sliceDoc(from, to)
addComment(from, content, message) try {
.catch(setError) await addComment(from, content, message)
.finally(() => setSubmitting(false)) handleClose()
view.dispatch({
view.dispatch({ selection: EditorSelection.cursor(view.state.selection.main.anchor),
selection: EditorSelection.cursor(view.state.selection.main.anchor), })
}) } catch (err) {
debugConsole.error(err)
handleClose() showGenericMessageModal(
t('add_comment_error_title'),
t('add_comment_error_message')
)
}
setSubmitting(false)
}, },
[addComment, view, handleClose, from, to] [addComment, view, handleClose, from, to, showGenericMessageModal, t]
) )
const { handleChange, handleKeyPress, content } = const { handleChange, handleKeyPress, content } =
@ -123,6 +130,7 @@ export const ReviewPanelAddComment: FC<{
t: value.spec.id as ThreadId, t: value.spec.id as ThreadId,
}} }}
selectLineOnFocus={false} selectLineOnFocus={false}
disabled={submitting}
> >
<form <form
className="review-panel-entry-content" className="review-panel-entry-content"
@ -158,7 +166,6 @@ export const ReviewPanelAddComment: FC<{
{t('comment')} {t('comment')}
</Button> </Button>
</div> </div>
{error && <div>{error.message}</div>}
</form> </form>
</ReviewPanelEntry> </ReviewPanelEntry>
) )

View file

@ -1,4 +1,4 @@
import { memo } from 'react' import { memo, useCallback, useState } from 'react'
import { useRangesActionsContext } from '../context/ranges-context' import { useRangesActionsContext } from '../context/ranges-context'
import { import {
Change, Change,
@ -15,6 +15,7 @@ import { formatTimeBasedOnYear } from '@/features/utils/format-date'
import { useChangesUsersContext } from '../context/changes-users-context' import { useChangesUsersContext } from '../context/changes-users-context'
import { ReviewPanelChangeUser } from './review-panel-change-user' import { ReviewPanelChangeUser } from './review-panel-change-user'
import { ReviewPanelEntry } from './review-panel-entry' import { ReviewPanelEntry } from './review-panel-entry'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
export const ReviewPanelChange = memo<{ export const ReviewPanelChange = memo<{
change: Change<EditOperation> change: Change<EditOperation>
@ -42,6 +43,27 @@ export const ReviewPanelChange = memo<{
const { acceptChanges, rejectChanges } = useRangesActionsContext() const { acceptChanges, rejectChanges } = useRangesActionsContext()
const permissions = usePermissionsContext() const permissions = usePermissionsContext()
const changesUsers = useChangesUsersContext() const changesUsers = useChangesUsersContext()
const { showGenericMessageModal } = useModalsContext()
const [accepting, setAccepting] = useState(false)
const acceptHandler = useCallback(async () => {
setAccepting(true)
try {
if (aggregate) {
await acceptChanges(change.id, aggregate.id)
} else {
await acceptChanges(change.id)
}
} catch (err) {
showGenericMessageModal(
t('accept_change_error_title'),
t('accept_change_error_description')
)
} finally {
setAccepting(false)
}
}, [acceptChanges, aggregate, change.id, showGenericMessageModal, t])
if (!changesUsers) { if (!changesUsers) {
// if users are not loaded yet, do not show "Unknown" user // if users are not loaded yet, do not show "Unknown" user
@ -61,6 +83,7 @@ export const ReviewPanelChange = memo<{
position={change.op.p} position={change.op.p}
docId={docId} docId={docId}
hoverRanges={hoverRanges} hoverRanges={hoverRanges}
disabled={accepting}
> >
<div <div
className="review-panel-entry-indicator" className="review-panel-entry-indicator"
@ -92,14 +115,7 @@ export const ReviewPanelChange = memo<{
description={t('accept_change')} description={t('accept_change')}
tooltipProps={{ className: 'review-panel-tooltip' }} tooltipProps={{ className: 'review-panel-tooltip' }}
> >
<Button <Button onClick={acceptHandler} bsStyle={null}>
onClick={() =>
aggregate
? acceptChanges(change.id, aggregate.id)
: acceptChanges(change.id)
}
bsStyle={null}
>
<MaterialIcon <MaterialIcon
type="check" type="check"
className="review-panel-entry-actions-icon" className="review-panel-entry-actions-icon"

View file

@ -1,100 +1,98 @@
import { Dispatch, memo, SetStateAction, useCallback, useState } from 'react' import { memo, useCallback } from 'react'
import { Change, CommentOperation } from '../../../../../types/change' import { Change, CommentOperation } from '../../../../../types/change'
import { ReviewPanelMessage } from './review-panel-message' import { ReviewPanelMessage } from './review-panel-message'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import { useThreadsContext } from '../context/threads-context'
useThreadsActionsContext,
useThreadsContext,
} from '../context/threads-context'
import AutoExpandingTextArea from '@/shared/components/auto-expanding-text-area' import AutoExpandingTextArea from '@/shared/components/auto-expanding-text-area'
import ReviewPanelResolvedMessage from './review-panel-resolved-message' import ReviewPanelResolvedMessage from './review-panel-resolved-message'
import { ReviewPanelResolvedCommentThread } from '../../../../../types/review-panel/comment-thread' import { ReviewPanelResolvedCommentThread } from '../../../../../types/review-panel/comment-thread'
import useSubmittableTextInput from '../hooks/use-submittable-text-input' import useSubmittableTextInput from '../hooks/use-submittable-text-input'
import { CommentId } from '../../../../../types/review-panel/review-panel'
export const ReviewPanelCommentContent = memo<{ export const ReviewPanelCommentContent = memo<{
comment: Change<CommentOperation> comment: Change<CommentOperation>
isResolved: boolean isResolved: boolean
onEdit?: (commentId: CommentId, content: string) => Promise<void>
onReply?: (content: string) => Promise<void>
onDelete?: (commentId: CommentId) => Promise<void>
onResolve?: () => Promise<void>
onLeave?: () => void onLeave?: () => void
onEnter?: () => void onEnter?: () => void
}>(({ comment, isResolved, onLeave, onEnter }) => { }>(
const { t } = useTranslation() ({
const [submitting, setSubmitting] = useState(false) comment,
const [error, setError] = useState<Error>() isResolved,
const threads = useThreadsContext() onResolve,
const { resolveThread, addMessage } = useThreadsActionsContext() onDelete,
onEdit,
onReply,
onLeave,
onEnter,
}) => {
const { t } = useTranslation()
const threads = useThreadsContext()
const handleSubmitReply = useCallback( const handleSubmit = useCallback(
(content: string, setContent: Dispatch<SetStateAction<string>>) => { (content, setContent) => onReply?.(content).then(() => setContent('')),
setSubmitting(true) [onReply]
addMessage(comment.op.t, content) )
.then(() => {
setContent('')
})
.catch(error => {
setError(error)
})
.finally(() => {
setSubmitting(false)
})
},
[addMessage, comment.op.t]
)
const { handleChange, handleKeyPress, content } = const { handleChange, handleKeyPress, content } =
useSubmittableTextInput(handleSubmitReply) useSubmittableTextInput(handleSubmit)
const thread = threads?.[comment.op.t] const thread = threads?.[comment.op.t]
if (!thread) { if (!thread) {
return null return null
} }
return ( return (
<div <div
className="review-panel-entry-content" className="review-panel-entry-content"
onMouseEnter={onEnter} onMouseEnter={onEnter}
onMouseLeave={onLeave} onMouseLeave={onLeave}
> >
{thread.messages.map((message, i) => { {thread.messages.map((message, i) => {
const isReply = i !== 0 const isReply = i !== 0
return ( return (
<div key={message.id} className="review-panel-comment-wrapper"> <div key={message.id} className="review-panel-comment-wrapper">
{isReply && <div className="review-panel-comment-reply-divider" />} {isReply && (
<ReviewPanelMessage <div className="review-panel-comment-reply-divider" />
message={message} )}
threadId={comment.op.t} <ReviewPanelMessage
isReply={isReply} message={message}
hasReplies={!isReply && thread.messages.length > 1} isReply={isReply}
onResolve={() => resolveThread(comment.op.t)} hasReplies={!isReply && thread.messages.length > 1}
isThreadResolved={isResolved} onResolve={onResolve}
onEdit={onEdit}
onDelete={onDelete}
isThreadResolved={isResolved}
/>
</div>
)
})}
{isResolved && (
<div className="review-panel-comment-wrapper">
<div className="review-panel-comment-reply-divider" />
<ReviewPanelResolvedMessage
thread={thread as ReviewPanelResolvedCommentThread}
/> />
</div> </div>
) )}
})}
{isResolved && ( {!isResolved && (
<div className="review-panel-comment-wrapper"> <AutoExpandingTextArea
<div className="review-panel-comment-reply-divider" /> name="content"
<ReviewPanelResolvedMessage className="review-panel-comment-input"
thread={thread as ReviewPanelResolvedCommentThread} onChange={handleChange}
onKeyDown={handleKeyPress}
placeholder={t('reply')}
value={content}
/> />
</div> )}
)} </div>
)
{!isResolved && ( }
<AutoExpandingTextArea )
name="content"
className="review-panel-comment-input"
onChange={handleChange}
onKeyDown={handleKeyPress}
placeholder={t('reply')}
value={content}
disabled={submitting}
/>
)}
{error && <div>{error.message}</div>}
</div>
)
})
ReviewPanelCommentContent.displayName = 'ReviewPanelCommentContent' ReviewPanelCommentContent.displayName = 'ReviewPanelCommentContent'

View file

@ -1,10 +1,17 @@
import { memo } from 'react' import { memo, useCallback, useState } from 'react'
import { Change, CommentOperation } from '../../../../../types/change' import { Change, CommentOperation } from '../../../../../types/change'
import { useThreadsContext } from '../context/threads-context' import {
useThreadsActionsContext,
useThreadsContext,
} from '../context/threads-context'
import classnames from 'classnames' import classnames from 'classnames'
import { ReviewPanelEntry } from './review-panel-entry' import { ReviewPanelEntry } from './review-panel-entry'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import { ReviewPanelCommentContent } from './review-panel-comment-content' import { ReviewPanelCommentContent } from './review-panel-comment-content'
import { CommentId } from '../../../../../types/review-panel/review-panel'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { useTranslation } from 'react-i18next'
import { debugConsole } from '@/utils/debugging'
export const ReviewPanelComment = memo<{ export const ReviewPanelComment = memo<{
comment: Change<CommentOperation> comment: Change<CommentOperation>
@ -16,6 +23,82 @@ export const ReviewPanelComment = memo<{
hovered?: boolean hovered?: boolean
}>(({ comment, top, hovered, onEnter, onLeave, docId, hoverRanges }) => { }>(({ comment, top, hovered, onEnter, onLeave, docId, hoverRanges }) => {
const threads = useThreadsContext() const threads = useThreadsContext()
const { resolveThread, editMessage, deleteMessage, addMessage } =
useThreadsActionsContext()
const { showGenericMessageModal } = useModalsContext()
const { t } = useTranslation()
const [processing, setProcessing] = useState(false)
const handleResolveComment = useCallback(async () => {
setProcessing(true)
try {
await resolveThread(comment.op.t)
} catch (err) {
debugConsole.error(err)
showGenericMessageModal(
t('resolve_comment_error_title'),
t('resolve_comment_error_message')
)
} finally {
setProcessing(false)
}
}, [comment.op.t, resolveThread, showGenericMessageModal, t])
const handleEditMessage = useCallback(
async (commentId: CommentId, content: string) => {
setProcessing(true)
try {
await editMessage(comment.op.t, commentId, content)
} catch (err) {
debugConsole.error(err)
showGenericMessageModal(
t('edit_comment_error_title'),
t('edit_comment_error_message')
)
} finally {
setProcessing(false)
}
},
[comment.op.t, editMessage, showGenericMessageModal, t]
)
const handleDeleteMessage = useCallback(
async (commentId: CommentId) => {
setProcessing(true)
try {
await deleteMessage(comment.op.t, commentId)
} catch (err) {
debugConsole.error(err)
showGenericMessageModal(
t('delete_comment_error_title'),
t('delete_comment_error_message')
)
} finally {
setProcessing(false)
}
},
[comment.op.t, deleteMessage, showGenericMessageModal, t]
)
const handleSubmitReply = useCallback(
async (content: string) => {
setProcessing(true)
try {
await addMessage(comment.op.t, content)
} catch (err) {
debugConsole.error(err)
showGenericMessageModal(
t('add_comment_error_title'),
t('add_comment_error_message')
)
throw err
} finally {
setProcessing(false)
}
},
[addMessage, comment.op.t, showGenericMessageModal, t]
)
const thread = threads?.[comment.op.t] const thread = threads?.[comment.op.t]
if (!thread || thread.resolved || thread.messages.length === 0) { if (!thread || thread.resolved || thread.messages.length === 0) {
@ -33,6 +116,7 @@ export const ReviewPanelComment = memo<{
op={comment.op} op={comment.op}
position={comment.op.p} position={comment.op.p}
hoverRanges={hoverRanges} hoverRanges={hoverRanges}
disabled={processing}
> >
<div <div
className="review-panel-entry-indicator" className="review-panel-entry-indicator"
@ -46,6 +130,10 @@ export const ReviewPanelComment = memo<{
isResolved={false} isResolved={false}
onLeave={onLeave} onLeave={onLeave}
onEnter={onEnter} onEnter={onEnter}
onResolve={handleResolveComment}
onEdit={handleEditMessage}
onDelete={handleDeleteMessage}
onReply={handleSubmitReply}
/> />
</ReviewPanelEntry> </ReviewPanelEntry>
) )

View file

@ -21,6 +21,7 @@ export const ReviewPanelEntry: FC<{
className?: string className?: string
selectLineOnFocus?: boolean selectLineOnFocus?: boolean
hoverRanges?: boolean hoverRanges?: boolean
disabled?: boolean
}> = ({ }> = ({
children, children,
position, position,
@ -30,6 +31,7 @@ export const ReviewPanelEntry: FC<{
selectLineOnFocus = true, selectLineOnFocus = true,
docId, docId,
hoverRanges = true, hoverRanges = true,
disabled,
}) => { }) => {
const state = useCodeMirrorStateContext() const state = useCodeMirrorStateContext()
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
@ -95,6 +97,7 @@ export const ReviewPanelEntry: FC<{
{ {
'review-panel-entry-focused': focused, 'review-panel-entry-focused': focused,
'review-panel-entry-highlighted': highlighted, 'review-panel-entry-highlighted': highlighted,
'review-panel-entry-disabled': disabled,
}, },
className className
)} )}

View file

@ -1,10 +1,9 @@
import { FC, useCallback, useState } from 'react' import { FC, useCallback, useState } from 'react'
import { import {
CommentId,
ReviewPanelCommentThreadMessage, ReviewPanelCommentThreadMessage,
ThreadId,
} from '../../../../../types/review-panel/review-panel' } from '../../../../../types/review-panel/review-panel'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useThreadsActionsContext } from '../context/threads-context'
import { formatTimeBasedOnYear } from '@/features/utils/format-date' import { formatTimeBasedOnYear } from '@/features/utils/format-date'
import Tooltip from '@/shared/components/tooltip' import Tooltip from '@/shared/components/tooltip'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
@ -17,49 +16,39 @@ import ReviewPanelDeleteCommentModal from './review-panel-delete-comment-modal'
export const ReviewPanelMessage: FC<{ export const ReviewPanelMessage: FC<{
message: ReviewPanelCommentThreadMessage message: ReviewPanelCommentThreadMessage
threadId: ThreadId
hasReplies: boolean hasReplies: boolean
isReply: boolean isReply: boolean
onResolve: () => void onResolve?: () => Promise<void>
onEdit?: (commentId: CommentId, content: string) => Promise<void>
onDelete?: (CommentId: CommentId) => Promise<void>
isThreadResolved: boolean isThreadResolved: boolean
}> = ({ }> = ({
message, message,
threadId,
isReply, isReply,
hasReplies, hasReplies,
onResolve, onResolve,
onEdit,
onDelete,
isThreadResolved, isThreadResolved,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [deleting, setDeleting] = useState(false) const [deleting, setDeleting] = useState(false)
const [error, setError] = useState<Error>()
const [content, setContent] = useState(message.content) const [content, setContent] = useState(message.content)
const { editMessage, deleteMessage } = useThreadsActionsContext()
const handleEditOption = useCallback(() => setEditing(true), []) const handleEditOption = useCallback(() => setEditing(true), [])
const showDeleteModal = useCallback(() => setDeleting(true), []) const showDeleteModal = useCallback(() => setDeleting(true), [])
const hideDeleteModal = useCallback(() => setDeleting(false), []) const hideDeleteModal = useCallback(() => setDeleting(false), [])
const handleSubmit = useCallback(async () => { const handleSubmit = useCallback(() => {
await editMessage(threadId, message.id, content) onEdit?.(message.id, content)
.catch(error => { setEditing(false)
setError(error) }, [content, message.id, onEdit])
})
.finally(() => {
setEditing(false)
})
}, [content, editMessage, message.id, threadId])
const handleDelete = useCallback(async () => { const handleDelete = useCallback(() => {
await deleteMessage(threadId, message.id) onDelete?.(message.id)
.catch(error => { setDeleting(false)
setError(error) }, [message.id, onDelete])
})
.finally(() => {
setDeleting(false)
})
}, [deleteMessage, message.id, threadId])
if (editing) { if (editing) {
return ( return (
@ -83,7 +72,6 @@ export const ReviewPanelMessage: FC<{
value={content} value={content}
autoFocus // eslint-disable-line jsx-a11y/no-autofocus autoFocus // eslint-disable-line jsx-a11y/no-autofocus
/> />
{error && <div>{error.message}</div>}
</div> </div>
) )
} }

View file

@ -1,4 +1,4 @@
import React, { FC } from 'react' import React, { FC, useCallback, useState } from 'react'
import { useTranslation, Trans } from 'react-i18next' import { useTranslation, Trans } from 'react-i18next'
import { ThreadId } from '../../../../../types/review-panel/review-panel' import { ThreadId } from '../../../../../types/review-panel/review-panel'
import { useThreadsActionsContext } from '../context/threads-context' import { useThreadsActionsContext } from '../context/threads-context'
@ -8,17 +8,57 @@ import { ExpandableContent } from './review-panel-expandable-content'
import { ReviewPanelCommentContent } from './review-panel-comment-content' import { ReviewPanelCommentContent } from './review-panel-comment-content'
import { Change, CommentOperation } from '../../../../../types/change' import { Change, CommentOperation } from '../../../../../types/change'
import Tooltip from '@/shared/components/tooltip' import Tooltip from '@/shared/components/tooltip'
import classNames from 'classnames'
import { debugConsole } from '@/utils/debugging'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
export const ReviewPanelResolvedThread: FC<{ export const ReviewPanelResolvedThread: FC<{
id: string id: ThreadId
comment: Change<CommentOperation> comment: Change<CommentOperation>
docName: string docName: string
}> = ({ id, comment, docName }) => { }> = ({ id, comment, docName }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { reopenThread, deleteThread } = useThreadsActionsContext() const { reopenThread, deleteThread } = useThreadsActionsContext()
const [processing, setProcessing] = useState(false)
const { showGenericMessageModal } = useModalsContext()
const handleReopenThread = useCallback(async () => {
setProcessing(true)
try {
await reopenThread(id)
} catch (err) {
debugConsole.error(err)
showGenericMessageModal(
t('reopen_comment_error_title'),
t('reopen_comment_error_message')
)
} finally {
setProcessing(false)
}
}, [id, reopenThread, showGenericMessageModal, t])
const handleDeleteThread = useCallback(async () => {
setProcessing(true)
try {
await deleteThread(id)
} catch (err) {
debugConsole.error(err)
showGenericMessageModal(
t('delete_comment_error_title'),
t('delete_comment_error_message')
)
} finally {
setProcessing(false)
}
}, [id, deleteThread, showGenericMessageModal, t])
return ( return (
<div className="review-panel-resolved-comment" key={id}> <div
className={classNames('review-panel-resolved-comment', {
'review-panel-resolved-disabled': processing,
})}
key={id}
>
<div className="review-panel-resolved-comment-header"> <div className="review-panel-resolved-comment-header">
<div> <div>
<Trans <Trans
@ -38,7 +78,7 @@ export const ReviewPanelResolvedThread: FC<{
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
description={t('reopen')} description={t('reopen')}
> >
<Button onClick={() => reopenThread(id as ThreadId)}> <Button onClick={handleReopenThread}>
<MaterialIcon type="refresh" accessibilityLabel={t('reopen')} /> <MaterialIcon type="refresh" accessibilityLabel={t('reopen')} />
</Button> </Button>
</Tooltip> </Tooltip>
@ -48,7 +88,7 @@ export const ReviewPanelResolvedThread: FC<{
overlayProps={{ placement: 'bottom' }} overlayProps={{ placement: 'bottom' }}
description={t('delete')} description={t('delete')}
> >
<Button onClick={() => deleteThread(id as ThreadId)}> <Button onClick={handleDeleteThread}>
<MaterialIcon type="delete" accessibilityLabel={t('delete')} /> <MaterialIcon type="delete" accessibilityLabel={t('delete')} />
</Button> </Button>
</Tooltip> </Tooltip>

View file

@ -6,6 +6,7 @@ import useProjectRanges from '../hooks/use-project-ranges'
import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import Icon from '@/shared/components/icon' import Icon from '@/shared/components/icon'
import { Change, CommentOperation } from '../../../../../types/change' import { Change, CommentOperation } from '../../../../../types/change'
import { ThreadId } from '../../../../../types/review-panel/review-panel'
export const ReviewPanelResolvedThreadsMenu: FC = () => { export const ReviewPanelResolvedThreadsMenu: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -92,7 +93,7 @@ export const ReviewPanelResolvedThreadsMenu: FC = () => {
return ( return (
<ReviewPanelResolvedThread <ReviewPanelResolvedThread
key={thread.id} key={thread.id}
id={thread.id} id={thread.id as ThreadId}
comment={comment} comment={comment}
docName={docNameForThread.get(thread.id) ?? t('unknown')} docName={docNameForThread.get(thread.id) ?? t('unknown')}
/> />

View file

@ -16,6 +16,8 @@ import {
import RangesTracker from '@overleaf/ranges-tracker' import RangesTracker from '@overleaf/ranges-tracker'
import { rejectChanges } from '@/features/source-editor/extensions/changes/reject-changes' import { rejectChanges } from '@/features/source-editor/extensions/changes/reject-changes'
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context' import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
import { postJSON } from '@/infrastructure/fetch-json'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
export type Ranges = { export type Ranges = {
docId: string docId: string
@ -57,7 +59,7 @@ const RangesActionsContext = createContext<RangesActions | undefined>(undefined)
export const RangesProvider: FC = ({ children }) => { export const RangesProvider: FC = ({ children }) => {
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const { projectId } = useIdeReactContext()
const [currentDoc] = useScopeValue<DocumentContainer | null>( const [currentDoc] = useScopeValue<DocumentContainer | null>(
'editor.sharejs_doc' 'editor.sharejs_doc'
) )
@ -115,8 +117,10 @@ export const RangesProvider: FC = ({ children }) => {
const actions = useMemo( const actions = useMemo(
() => ({ () => ({
acceptChanges(...ids: string[]) { async acceptChanges(...ids: string[]) {
if (currentDoc?.ranges) { if (currentDoc?.ranges) {
const url = `/project/${projectId}/doc/${currentDoc.doc_id}/changes/accept`
await postJSON(url, { body: { change_ids: ids } })
currentDoc.ranges.removeChangeIds(ids) currentDoc.ranges.removeChangeIds(ids)
setRanges(buildRanges(currentDoc)) setRanges(buildRanges(currentDoc))
} }
@ -127,7 +131,7 @@ export const RangesProvider: FC = ({ children }) => {
} }
}, },
}), }),
[currentDoc, view] [currentDoc, projectId, view]
) )
return ( return (

View file

@ -248,7 +248,6 @@ export const ThreadsProvider: FC = ({ children }) => {
await postJSON(`/project/${projectId}/thread/${threadId}/messages`, { await postJSON(`/project/${projectId}/thread/${threadId}/messages`, {
body: { content }, body: { content },
}) })
// TODO: error_submitting_comment
}, },
async editMessage( async editMessage(
threadId: ThreadId, threadId: ThreadId,

View file

@ -31,6 +31,11 @@
z-index: 1; z-index: 1;
} }
.review-panel-entry.review-panel-entry-disabled {
opacity: 0.5;
pointer-events: none;
}
.review-panel-entry-indicator { .review-panel-entry-indicator {
display: none; display: none;
} }
@ -235,6 +240,11 @@
} }
} }
.review-panel-resolved-disabled {
opacity: 0.5;
pointer-events: none;
}
.review-panel-resolved-comments-loading { .review-panel-resolved-comments-loading {
text-align: center; text-align: center;
} }

View file

@ -33,6 +33,8 @@
"accept_all": "Accept all", "accept_all": "Accept all",
"accept_and_continue": "Accept and continue", "accept_and_continue": "Accept and continue",
"accept_change": "Accept change", "accept_change": "Accept change",
"accept_change_error_description": "There was an error accepting a track change. Please try again in a few moments.",
"accept_change_error_title": "Accept Change Error",
"accept_invitation": "Accept invitation", "accept_invitation": "Accept invitation",
"accept_or_reject_each_changes_individually": "Accept or reject each change individually", "accept_or_reject_each_changes_individually": "Accept or reject each change individually",
"accept_terms_and_conditions": "Accept terms and conditions", "accept_terms_and_conditions": "Accept terms and conditions",
@ -64,6 +66,8 @@
"add_another_token": "Add another token", "add_another_token": "Add another token",
"add_comma_separated_emails_help": "Separate multiple email addresses using the comma (,) character.", "add_comma_separated_emails_help": "Separate multiple email addresses using the comma (,) character.",
"add_comment": "Add comment", "add_comment": "Add comment",
"add_comment_error_message": "There was an error adding your comment. Please try again in a few moments.",
"add_comment_error_title": "Add Comment Error",
"add_company_details": "Add Company Details", "add_company_details": "Add Company Details",
"add_email": "Add Email", "add_email": "Add Email",
"add_email_address": "Add email address", "add_email_address": "Add email address",
@ -427,6 +431,8 @@
"delete_authentication_token_info": "Youre about to delete a Git authentication token. If you do, it can no longer be used to authenticate your identity when performing Git operations.", "delete_authentication_token_info": "Youre about to delete a Git authentication token. If you do, it can no longer be used to authenticate your identity when performing Git operations.",
"delete_certificate": "Delete certificate", "delete_certificate": "Delete certificate",
"delete_comment": "Delete comment", "delete_comment": "Delete comment",
"delete_comment_error_message": "There was an error deleting your comment. Please try again in a few moments.",
"delete_comment_error_title": "Delete Comment Error",
"delete_comment_message": "You cannot undo this action.", "delete_comment_message": "You cannot undo this action.",
"delete_comment_thread": "Delete comment thread", "delete_comment_thread": "Delete comment thread",
"delete_comment_thread_message": "This will delete the whole comment thread. You cannot undo this action.", "delete_comment_thread_message": "This will delete the whole comment thread. You cannot undo this action.",
@ -526,6 +532,8 @@
"easily_manage_your_project_files_everywhere": "Easily manage your project files, everywhere", "easily_manage_your_project_files_everywhere": "Easily manage your project files, everywhere",
"easy_collaboration_for_students": "Easy collaboration for students. Supports longer or more complex projects.", "easy_collaboration_for_students": "Easy collaboration for students. Supports longer or more complex projects.",
"edit": "Edit", "edit": "Edit",
"edit_comment_error_message": "There was an error editing your comment. Please try again in a few moments.",
"edit_comment_error_title": "Edit Comment Error",
"edit_dictionary": "Edit Dictionary", "edit_dictionary": "Edit Dictionary",
"edit_dictionary_empty": "Your custom dictionary is empty.", "edit_dictionary_empty": "Your custom dictionary is empty.",
"edit_dictionary_remove": "Remove from dictionary", "edit_dictionary_remove": "Remove from dictionary",
@ -1680,6 +1688,8 @@
"rename_project": "Rename Project", "rename_project": "Rename Project",
"renaming": "Renaming", "renaming": "Renaming",
"reopen": "Re-open", "reopen": "Re-open",
"reopen_comment_error_message": "There was an error reopening your comment. Please try again in a few moments.",
"reopen_comment_error_title": "Reopen Comment Error",
"replace_figure": "Replace figure", "replace_figure": "Replace figure",
"replace_from_another_project": "Replace from another project", "replace_from_another_project": "Replace from another project",
"replace_from_computer": "Replace from computer", "replace_from_computer": "Replace from computer",