Просмотр исходного кода

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
Domagoj Kriskovic 2 лет назад
Родитель
Сommit
4a3eed35d6

+ 12 - 0
services/web/frontend/extracted-translations.json

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

+ 20 - 13
services/web/frontend/js/features/review-panel-new/components/review-panel-add-comment.tsx

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

+ 25 - 9
services/web/frontend/js/features/review-panel-new/components/review-panel-change.tsx

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

+ 75 - 77
services/web/frontend/js/features/review-panel-new/components/review-panel-comment-content.tsx

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

+ 90 - 2
services/web/frontend/js/features/review-panel-new/components/review-panel-comment.tsx

@@ -1,10 +1,17 @@
-import { memo } from 'react'
+import { memo, useCallback, useState } from 'react'
 import { Change, CommentOperation } from '../../../../../types/change'
-import { useThreadsContext } from '../context/threads-context'
+import {
+  useThreadsActionsContext,
+  useThreadsContext,
+} from '../context/threads-context'
 import classnames from 'classnames'
 import { ReviewPanelEntry } from './review-panel-entry'
 import MaterialIcon from '@/shared/components/material-icon'
 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<{
   comment: Change<CommentOperation>
@@ -16,6 +23,82 @@ export const ReviewPanelComment = memo<{
   hovered?: boolean
 }>(({ comment, top, hovered, onEnter, onLeave, docId, hoverRanges }) => {
   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]
   if (!thread || thread.resolved || thread.messages.length === 0) {
@@ -33,6 +116,7 @@ export const ReviewPanelComment = memo<{
       op={comment.op}
       position={comment.op.p}
       hoverRanges={hoverRanges}
+      disabled={processing}
     >
       <div
         className="review-panel-entry-indicator"
@@ -46,6 +130,10 @@ export const ReviewPanelComment = memo<{
         isResolved={false}
         onLeave={onLeave}
         onEnter={onEnter}
+        onResolve={handleResolveComment}
+        onEdit={handleEditMessage}
+        onDelete={handleDeleteMessage}
+        onReply={handleSubmitReply}
       />
     </ReviewPanelEntry>
   )

+ 3 - 0
services/web/frontend/js/features/review-panel-new/components/review-panel-entry.tsx

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

+ 14 - 26
services/web/frontend/js/features/review-panel-new/components/review-panel-message.tsx

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

+ 45 - 5
services/web/frontend/js/features/review-panel-new/components/review-panel-resolved-thread.tsx

@@ -1,4 +1,4 @@
-import React, { FC } from 'react'
+import React, { FC, useCallback, useState } from 'react'
 import { useTranslation, Trans } from 'react-i18next'
 import { ThreadId } from '../../../../../types/review-panel/review-panel'
 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 { Change, CommentOperation } from '../../../../../types/change'
 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<{
-  id: string
+  id: ThreadId
   comment: Change<CommentOperation>
   docName: string
 }> = ({ id, comment, docName }) => {
   const { t } = useTranslation()
   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 (
-    <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>
           <Trans
@@ -38,7 +78,7 @@ export const ReviewPanelResolvedThread: FC<{
             overlayProps={{ placement: 'bottom' }}
             description={t('reopen')}
           >
-            <Button onClick={() => reopenThread(id as ThreadId)}>
+            <Button onClick={handleReopenThread}>
               <MaterialIcon type="refresh" accessibilityLabel={t('reopen')} />
             </Button>
           </Tooltip>
@@ -48,7 +88,7 @@ export const ReviewPanelResolvedThread: FC<{
             overlayProps={{ placement: 'bottom' }}
             description={t('delete')}
           >
-            <Button onClick={() => deleteThread(id as ThreadId)}>
+            <Button onClick={handleDeleteThread}>
               <MaterialIcon type="delete" accessibilityLabel={t('delete')} />
             </Button>
           </Tooltip>

+ 2 - 1
services/web/frontend/js/features/review-panel-new/components/review-panel-resolved-threads-menu.tsx

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

+ 7 - 3
services/web/frontend/js/features/review-panel-new/context/ranges-context.tsx

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

+ 0 - 1
services/web/frontend/js/features/review-panel-new/context/threads-context.tsx

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

+ 10 - 0
services/web/frontend/stylesheets/app/editor/review-panel-new.less

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

+ 10 - 0
services/web/locales/en.json

@@ -33,6 +33,8 @@
   "accept_all": "Accept all",
   "accept_and_continue": "Accept and continue",
   "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_or_reject_each_changes_individually": "Accept or reject each change individually",
   "accept_terms_and_conditions": "Accept terms and conditions",
@@ -64,6 +66,8 @@
   "add_another_token": "Add another token",
   "add_comma_separated_emails_help": "Separate multiple email addresses using the comma (,) character.",
   "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_email": "Add Email",
   "add_email_address": "Add email address",
@@ -427,6 +431,8 @@
   "delete_authentication_token_info": "You’re 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_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_thread": "Delete comment thread",
   "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",
   "easy_collaboration_for_students": "Easy collaboration for students. Supports longer or more complex projects.",
   "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_empty": "Your custom dictionary is empty.",
   "edit_dictionary_remove": "Remove from dictionary",
@@ -1680,6 +1688,8 @@
   "rename_project": "Rename Project",
   "renaming": "Renaming",
   "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_from_another_project": "Replace from another project",
   "replace_from_computer": "Replace from computer",