review-panel-add-comment.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { FormEventHandler, useCallback, useState, useRef, memo } from 'react'
  2. import {
  3. useCodeMirrorStateContext,
  4. useCodeMirrorViewContext,
  5. } from '@/features/source-editor/components/codemirror-context'
  6. import { EditorSelection } from '@codemirror/state'
  7. import { useTranslation } from 'react-i18next'
  8. import { useThreadsActionsContext } from '../context/threads-context'
  9. import { removeNewCommentRangeEffect } from '@/features/source-editor/extensions/review-tooltip'
  10. import useSubmittableTextInput from '../hooks/use-submittable-text-input'
  11. import AutoExpandingTextArea from '@/shared/components/auto-expanding-text-area'
  12. import { ReviewPanelEntry } from './review-panel-entry'
  13. import { ThreadId } from '../../../../../types/review-panel/review-panel'
  14. import { useModalsContext } from '@/features/ide-react/context/modals-context'
  15. import { debugConsole } from '@/utils/debugging'
  16. import OLButton from '@/features/ui/components/ol/ol-button'
  17. export const ReviewPanelAddComment = memo<{
  18. docId: string
  19. from: number
  20. to: number
  21. threadId: string
  22. top: number | undefined
  23. }>(function ReviewPanelAddComment({ from, to, threadId, top, docId }) {
  24. const { t } = useTranslation()
  25. const view = useCodeMirrorViewContext()
  26. const state = useCodeMirrorStateContext()
  27. const { addComment } = useThreadsActionsContext()
  28. const [submitting, setSubmitting] = useState(false)
  29. const { showGenericMessageModal } = useModalsContext()
  30. const handleClose = useCallback(() => {
  31. view.dispatch({
  32. effects: removeNewCommentRangeEffect.of(threadId),
  33. })
  34. }, [view, threadId])
  35. const submitForm = useCallback(
  36. async (message: string) => {
  37. setSubmitting(true)
  38. const content = view.state.sliceDoc(from, to)
  39. try {
  40. await addComment(from, content, message)
  41. handleClose()
  42. view.dispatch({
  43. selection: EditorSelection.cursor(view.state.selection.main.anchor),
  44. })
  45. } catch (err) {
  46. debugConsole.error(err)
  47. showGenericMessageModal(
  48. t('add_comment_error_title'),
  49. t('add_comment_error_message')
  50. )
  51. }
  52. setSubmitting(false)
  53. },
  54. [addComment, view, handleClose, from, to, showGenericMessageModal, t]
  55. )
  56. const { handleChange, handleKeyPress, content } =
  57. useSubmittableTextInput(submitForm)
  58. const handleBlur = useCallback(() => {
  59. if (content === '') {
  60. window.setTimeout(() => {
  61. handleClose()
  62. })
  63. }
  64. }, [content, handleClose])
  65. const handleSubmit = useCallback<FormEventHandler>(
  66. event => {
  67. event.preventDefault()
  68. submitForm(content)
  69. },
  70. [submitForm, content]
  71. )
  72. // We only ever want to focus the element once
  73. const hasBeenFocused = useRef(false)
  74. // Auto-focus the textarea once the element has been correctly positioned.
  75. // We cannot use the autofocus attribute as we need to wait until the parent element
  76. // has been positioned (with the "top" attribute) to avoid scrolling to the initial
  77. // position of the element
  78. const observerCallback = useCallback((mutationList: MutationRecord[]) => {
  79. if (hasBeenFocused.current) {
  80. return
  81. }
  82. for (const mutation of mutationList) {
  83. const target = mutation.target as HTMLElement
  84. if (target.style.top) {
  85. const textArea = target.getElementsByTagName('textarea')[0]
  86. if (textArea) {
  87. textArea.focus()
  88. hasBeenFocused.current = true
  89. }
  90. }
  91. }
  92. }, [])
  93. const handleElement = useCallback(
  94. (element: HTMLElement | null) => {
  95. if (element) {
  96. element.dispatchEvent(new Event('review-panel:position'))
  97. const observer = new MutationObserver(observerCallback)
  98. const entryWrapper = element.closest('.review-panel-entry')
  99. if (entryWrapper) {
  100. observer.observe(entryWrapper, {
  101. attributes: true,
  102. attributeFilter: ['style'],
  103. })
  104. return () => observer.disconnect()
  105. }
  106. }
  107. },
  108. [observerCallback]
  109. )
  110. return (
  111. <ReviewPanelEntry
  112. docId={docId}
  113. top={top}
  114. position={from}
  115. op={{
  116. p: from,
  117. c: state.sliceDoc(from, to),
  118. t: threadId as ThreadId,
  119. }}
  120. selectLineOnFocus={false}
  121. disabled={submitting}
  122. >
  123. <form
  124. className="review-panel-entry-content"
  125. onBlur={handleBlur}
  126. onSubmit={handleSubmit}
  127. ref={handleElement}
  128. >
  129. <AutoExpandingTextArea
  130. name="message"
  131. className="review-panel-add-comment-textarea"
  132. onChange={handleChange}
  133. onKeyPress={handleKeyPress}
  134. placeholder={t('add_your_comment_here')}
  135. value={content}
  136. disabled={submitting}
  137. />
  138. <div className="review-panel-add-comment-buttons">
  139. <OLButton
  140. variant="ghost"
  141. size="sm"
  142. className="review-panel-add-comment-cancel-button"
  143. disabled={submitting}
  144. onClick={handleClose}
  145. >
  146. {t('cancel')}
  147. </OLButton>
  148. <OLButton
  149. type="submit"
  150. variant="primary"
  151. size="sm"
  152. disabled={content === '' || submitting}
  153. >
  154. {t('comment')}
  155. </OLButton>
  156. </div>
  157. </form>
  158. </ReviewPanelEntry>
  159. )
  160. })