mirror of
https://github.com/harness/drone.git
synced 2025-05-05 23:42:57 +00:00
Add code to restore new and edited comments
This commit is contained in:
parent
e6dc4c4fc9
commit
58073b0146
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import type { EditorView } from '@codemirror/view'
|
import type { EditorView } from '@codemirror/view'
|
||||||
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
import { Render, Match, Truthy, Falsy, Else } from 'react-jsx-match'
|
||||||
import { Container, Layout, Avatar, TextInput, Text, FlexExpander, Button, useIsMounted } from '@harnessio/uicore'
|
import { Container, Layout, Avatar, TextInput, Text, FlexExpander, Button, useIsMounted } from '@harnessio/uicore'
|
||||||
@ -34,6 +34,7 @@ import { ButtonRoleProps, CodeCommentState } from 'utils/Utils'
|
|||||||
import { useResizeObserver } from 'hooks/useResizeObserver'
|
import { useResizeObserver } from 'hooks/useResizeObserver'
|
||||||
import { useCustomEventListener } from 'hooks/useEventListener'
|
import { useCustomEventListener } from 'hooks/useEventListener'
|
||||||
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
|
import type { SuggestionBlock } from 'components/SuggestionBlock/SuggestionBlock'
|
||||||
|
import type { CommentRestorationTrackingState, DiffViewerExchangeState } from 'components/DiffViewer/DiffViewer'
|
||||||
import commentActiveIconUrl from './comment.svg?url'
|
import commentActiveIconUrl from './comment.svg?url'
|
||||||
import commentResolvedIconUrl from './comment-resolved.svg?url'
|
import commentResolvedIconUrl from './comment-resolved.svg?url'
|
||||||
import css from './CommentBox.module.scss'
|
import css from './CommentBox.module.scss'
|
||||||
@ -83,6 +84,7 @@ interface CommentBoxProps<T> {
|
|||||||
resetOnSave?: boolean
|
resetOnSave?: boolean
|
||||||
hideCancel?: boolean
|
hideCancel?: boolean
|
||||||
currentUserName: string
|
currentUserName: string
|
||||||
|
commentThreadId?: number
|
||||||
commentItems: CommentItem<T>[]
|
commentItems: CommentItem<T>[]
|
||||||
handleAction: (
|
handleAction: (
|
||||||
action: CommentAction,
|
action: CommentAction,
|
||||||
@ -99,6 +101,8 @@ interface CommentBoxProps<T> {
|
|||||||
routingId: string
|
routingId: string
|
||||||
copyLinkToComment: (commentId: number, commentItem: CommentItem<T>) => void
|
copyLinkToComment: (commentId: number, commentItem: CommentItem<T>) => void
|
||||||
suggestionBlock?: SuggestionBlock
|
suggestionBlock?: SuggestionBlock
|
||||||
|
memorizedState?: CommentRestorationTrackingState
|
||||||
|
commentsVisibilityAtLineNumber?: DiffViewerExchangeState['commentsVisibilityAtLineNumber']
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentBoxInternal = <T = unknown,>({
|
const CommentBoxInternal = <T = unknown,>({
|
||||||
@ -109,6 +113,7 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
initialContent = '',
|
initialContent = '',
|
||||||
width,
|
width,
|
||||||
fluid,
|
fluid,
|
||||||
|
commentThreadId,
|
||||||
commentItems = [],
|
commentItems = [],
|
||||||
currentUserName,
|
currentUserName,
|
||||||
handleAction,
|
handleAction,
|
||||||
@ -123,7 +128,9 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
standalone,
|
standalone,
|
||||||
routingId,
|
routingId,
|
||||||
copyLinkToComment,
|
copyLinkToComment,
|
||||||
suggestionBlock
|
suggestionBlock,
|
||||||
|
memorizedState,
|
||||||
|
commentsVisibilityAtLineNumber
|
||||||
}: CommentBoxProps<T>) => {
|
}: CommentBoxProps<T>) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const [comments, setComments] = useState<CommentItem<T>[]>(commentItems)
|
const [comments, setComments] = useState<CommentItem<T>[]>(commentItems)
|
||||||
@ -134,6 +141,13 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const isMounted = useIsMounted()
|
const isMounted = useIsMounted()
|
||||||
|
|
||||||
|
const clearMemorizedState = useCallback(() => {
|
||||||
|
if (memorizedState) {
|
||||||
|
delete memorizedState.showReplyPlaceHolder
|
||||||
|
delete memorizedState.uncommittedText
|
||||||
|
}
|
||||||
|
}, [memorizedState])
|
||||||
|
|
||||||
useResizeObserver(
|
useResizeObserver(
|
||||||
containerRef,
|
containerRef,
|
||||||
useCallback(
|
useCallback(
|
||||||
@ -156,10 +170,13 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
const _onCancel = useCallback(() => {
|
const _onCancel = useCallback(() => {
|
||||||
setMarkdown('')
|
setMarkdown('')
|
||||||
setShowReplyPlaceHolder(true)
|
setShowReplyPlaceHolder(true)
|
||||||
|
|
||||||
|
clearMemorizedState()
|
||||||
|
|
||||||
if (onCancel && !comments.length) {
|
if (onCancel && !comments.length) {
|
||||||
onCancel()
|
onCancel()
|
||||||
}
|
}
|
||||||
}, [setShowReplyPlaceHolder, onCancel, comments.length])
|
}, [setShowReplyPlaceHolder, onCancel, comments.length, clearMemorizedState])
|
||||||
const hidePlaceHolder = useCallback(() => setShowReplyPlaceHolder(false), [setShowReplyPlaceHolder])
|
const hidePlaceHolder = useCallback(() => setShowReplyPlaceHolder(false), [setShowReplyPlaceHolder])
|
||||||
const onQuote = useCallback((content: string) => {
|
const onQuote = useCallback((content: string) => {
|
||||||
const replyContent = content
|
const replyContent = content
|
||||||
@ -179,13 +196,73 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
})
|
})
|
||||||
}, [dirties, setDirty])
|
}, [dirties, setDirty])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
// This function restores CommentBox internal states from memorizedState
|
||||||
|
// after it got destroyed during HTML/textContent serialization/deserialization
|
||||||
|
// This approach is not optimized, we probably have to think about a shared
|
||||||
|
// store per diff or something else to make the flow nicer
|
||||||
|
function serializeNewCommentInfo() {
|
||||||
|
if (!commentThreadId || !memorizedState) return
|
||||||
|
|
||||||
|
if (commentThreadId < 0) {
|
||||||
|
if (!comments?.[0]?.id) {
|
||||||
|
if (!markdown && memorizedState.uncommittedText) {
|
||||||
|
setMarkdown(memorizedState.uncommittedText)
|
||||||
|
viewRef.current?.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: viewRef.current.state.doc.length,
|
||||||
|
insert: memorizedState.uncommittedText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
viewRef.current?.contentDOM?.blur()
|
||||||
|
} else {
|
||||||
|
memorizedState.uncommittedText = markdown
|
||||||
|
memorizedState.showReplyPlaceHolder = showReplyPlaceHolder
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearMemorizedState()
|
||||||
|
}
|
||||||
|
} else if (commentThreadId > 0) {
|
||||||
|
if (!showReplyPlaceHolder) {
|
||||||
|
if (markdown) {
|
||||||
|
memorizedState.uncommittedText = markdown
|
||||||
|
memorizedState.showReplyPlaceHolder = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!markdown && memorizedState.showReplyPlaceHolder === false) {
|
||||||
|
setShowReplyPlaceHolder(false)
|
||||||
|
|
||||||
|
const { uncommittedText = '' } = memorizedState
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setMarkdown(uncommittedText)
|
||||||
|
viewRef.current?.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: viewRef.current.state.doc.length,
|
||||||
|
insert: uncommittedText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
viewRef.current?.contentDOM?.blur()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete memorizedState.showReplyPlaceHolder
|
||||||
|
delete memorizedState.uncommittedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[markdown, commentThreadId, comments, memorizedState, clearMemorizedState, showReplyPlaceHolder]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className={cx(css.main, { [css.fluid]: fluid }, outerClassName)}
|
className={cx(css.main, { [css.fluid]: fluid }, outerClassName)}
|
||||||
padding={!fluid ? 'medium' : undefined}
|
padding={!fluid ? 'medium' : undefined}
|
||||||
width={width}
|
width={width}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
data-comment-thread-id={comments?.[0]?.id || ''}>
|
data-comment-thread-id={comments?.[0]?.id || commentThreadId || ''}>
|
||||||
{outlets[CommentBoxOutletPosition.TOP]}
|
{outlets[CommentBoxOutletPosition.TOP]}
|
||||||
<Container className={cx(boxClassName, css.box)}>
|
<Container className={cx(boxClassName, css.box)}>
|
||||||
<Layout.Vertical>
|
<Layout.Vertical>
|
||||||
@ -210,6 +287,8 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
outlets={outlets}
|
outlets={outlets}
|
||||||
copyLinkToComment={copyLinkToComment}
|
copyLinkToComment={copyLinkToComment}
|
||||||
suggestionBlock={suggestionBlock}
|
suggestionBlock={suggestionBlock}
|
||||||
|
memorizedState={memorizedState}
|
||||||
|
commentsVisibilityAtLineNumber={commentsVisibilityAtLineNumber}
|
||||||
/>
|
/>
|
||||||
<Match expr={showReplyPlaceHolder && enableReplyPlaceHolderRef.current}>
|
<Match expr={showReplyPlaceHolder && enableReplyPlaceHolderRef.current}>
|
||||||
<Truthy>
|
<Truthy>
|
||||||
@ -250,6 +329,8 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
value={markdown}
|
value={markdown}
|
||||||
onChange={setMarkdown}
|
onChange={setMarkdown}
|
||||||
onSave={async (value: string) => {
|
onSave={async (value: string) => {
|
||||||
|
clearMemorizedState()
|
||||||
|
|
||||||
if (handleAction) {
|
if (handleAction) {
|
||||||
const [result, updatedItem] = await handleAction(
|
const [result, updatedItem] = await handleAction(
|
||||||
comments.length ? CommentAction.REPLY : CommentAction.NEW,
|
comments.length ? CommentAction.REPLY : CommentAction.NEW,
|
||||||
@ -306,7 +387,13 @@ const CommentBoxInternal = <T = unknown,>({
|
|||||||
interface CommentsThreadProps<T>
|
interface CommentsThreadProps<T>
|
||||||
extends Pick<
|
extends Pick<
|
||||||
CommentBoxProps<T>,
|
CommentBoxProps<T>,
|
||||||
'commentItems' | 'handleAction' | 'outlets' | 'copyLinkToComment' | 'suggestionBlock'
|
| 'commentItems'
|
||||||
|
| 'handleAction'
|
||||||
|
| 'outlets'
|
||||||
|
| 'copyLinkToComment'
|
||||||
|
| 'suggestionBlock'
|
||||||
|
| 'memorizedState'
|
||||||
|
| 'commentsVisibilityAtLineNumber'
|
||||||
> {
|
> {
|
||||||
onQuote: (content: string) => void
|
onQuote: (content: string) => void
|
||||||
setDirty: (index: number, dirty: boolean) => void
|
setDirty: (index: number, dirty: boolean) => void
|
||||||
@ -321,21 +408,26 @@ const CommentsThread = <T = unknown,>({
|
|||||||
outlets = {},
|
outlets = {},
|
||||||
repoMetadata,
|
repoMetadata,
|
||||||
copyLinkToComment,
|
copyLinkToComment,
|
||||||
suggestionBlock
|
suggestionBlock,
|
||||||
|
memorizedState,
|
||||||
|
commentsVisibilityAtLineNumber
|
||||||
}: CommentsThreadProps<T>) => {
|
}: CommentsThreadProps<T>) => {
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
const { standalone, routingId } = useAppContext()
|
const { standalone, routingId } = useAppContext()
|
||||||
const [editIndexes, setEditIndexes] = useState<Record<number, boolean>>({})
|
const [editIndexes, setEditIndexes] = useState<Record<number, boolean>>({})
|
||||||
const resetStateAtIndex = useCallback(
|
const resetStateAtIndex = useCallback(
|
||||||
(index: number) => {
|
(index: number, commentItem: CommentItem<T>) => {
|
||||||
delete editIndexes[index]
|
delete editIndexes[index]
|
||||||
setEditIndexes({ ...editIndexes })
|
setEditIndexes({ ...editIndexes })
|
||||||
|
|
||||||
|
if (memorizedState?.uncommittedEditComments && commentItem?.id) {
|
||||||
|
memorizedState.uncommittedEditComments.delete(commentItem.id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[editIndexes]
|
[editIndexes, memorizedState]
|
||||||
)
|
)
|
||||||
const isCommentThreadResolved = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems])
|
const isCommentThreadResolved = useMemo(() => !!get(commentItems[0], 'payload.resolved'), [commentItems])
|
||||||
const domRef = useRef<HTMLElement>()
|
const domRef = useRef<HTMLElement>()
|
||||||
const show = useRef(isCommentThreadResolved ? false : true)
|
|
||||||
const internalFlags = useRef({ initialized: false })
|
const internalFlags = useRef({ initialized: false })
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@ -353,10 +445,12 @@ const CommentsThread = <T = unknown,>({
|
|||||||
const lineNumColDOM = annotatedRow.firstElementChild as HTMLElement
|
const lineNumColDOM = annotatedRow.firstElementChild as HTMLElement
|
||||||
const sourceLineNumber = annotatedRow.dataset.sourceLineNumber
|
const sourceLineNumber = annotatedRow.dataset.sourceLineNumber
|
||||||
const button: HTMLButtonElement = lineNumColDOM?.querySelector('button') || document.createElement('button')
|
const button: HTMLButtonElement = lineNumColDOM?.querySelector('button') || document.createElement('button')
|
||||||
|
const showFromMemory = commentsVisibilityAtLineNumber?.get(Number(sourceLineNumber))
|
||||||
|
let show = showFromMemory !== undefined ? showFromMemory : isCommentThreadResolved ? false : true
|
||||||
|
|
||||||
if (!button.onclick) {
|
if (!button.onclick) {
|
||||||
const toggleHidden = (dom: Element) => {
|
const toggleHidden = (dom: Element) => {
|
||||||
if (show.current) dom.setAttribute('hidden', '')
|
if (show) dom.setAttribute('hidden', '')
|
||||||
else dom.removeAttribute('hidden')
|
else dom.removeAttribute('hidden')
|
||||||
}
|
}
|
||||||
const toggleComments = (e: KeyboardEvent | MouseEvent) => {
|
const toggleComments = (e: KeyboardEvent | MouseEvent) => {
|
||||||
@ -377,9 +471,13 @@ const CommentsThread = <T = unknown,>({
|
|||||||
toggleHidden(commentRow)
|
toggleHidden(commentRow)
|
||||||
commentRow = commentRow.nextElementSibling as HTMLElement
|
commentRow = commentRow.nextElementSibling as HTMLElement
|
||||||
}
|
}
|
||||||
show.current = !show.current
|
show = !show
|
||||||
|
|
||||||
if (!show.current) button.dataset.threadsCount = String(activeThreads + resolvedThreads)
|
if (memorizedState) {
|
||||||
|
commentsVisibilityAtLineNumber?.set(Number(sourceLineNumber), show)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!show) button.dataset.threadsCount = String(activeThreads + resolvedThreads)
|
||||||
else delete button.dataset.threadsCount
|
else delete button.dataset.threadsCount
|
||||||
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -404,7 +502,9 @@ const CommentsThread = <T = unknown,>({
|
|||||||
while (commentRow?.dataset?.annotatedLine) {
|
while (commentRow?.dataset?.annotatedLine) {
|
||||||
if (commentRow.dataset.commentThreadStatus == CodeCommentState.RESOLVED) {
|
if (commentRow.dataset.commentThreadStatus == CodeCommentState.RESOLVED) {
|
||||||
resolvedThreads++
|
resolvedThreads++
|
||||||
if (!internalFlags.current.initialized) show.current = false
|
if (!internalFlags.current.initialized && !showFromMemory) {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
} else activeThreads++
|
} else activeThreads++
|
||||||
|
|
||||||
commentRow = commentRow.nextElementSibling as HTMLElement
|
commentRow = commentRow.nextElementSibling as HTMLElement
|
||||||
@ -415,19 +515,44 @@ const CommentsThread = <T = unknown,>({
|
|||||||
if (!internalFlags.current.initialized) {
|
if (!internalFlags.current.initialized) {
|
||||||
internalFlags.current.initialized = true
|
internalFlags.current.initialized = true
|
||||||
|
|
||||||
if (!show.current && resolvedThreads) button.dataset.threadsCount = String(resolvedThreads)
|
if (!show && resolvedThreads) button.dataset.threadsCount = String(resolvedThreads)
|
||||||
else delete button.dataset.threadsCount
|
else delete button.dataset.threadsCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isCommentThreadResolved, getString]
|
[isCommentThreadResolved, getString, commentsVisibilityAtLineNumber, memorizedState]
|
||||||
)
|
)
|
||||||
|
const viewRefs = useRef(
|
||||||
|
Object.fromEntries(
|
||||||
|
commentItems.map(commentItem => [commentItem.id, createRef() as React.MutableRefObject<EditorView | undefined>])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const contentRestoredRefs = useRef<Record<number, boolean>>({})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Render when={commentItems.length}>
|
<Render when={commentItems.length}>
|
||||||
<Container className={css.viewer} padding="xlarge" ref={domRef}>
|
<Container className={css.viewer} padding="xlarge" ref={domRef}>
|
||||||
{commentItems.map((commentItem, index) => {
|
{commentItems.map((commentItem, index) => {
|
||||||
const isLastItem = index === commentItems.length - 1
|
const isLastItem = index === commentItems.length - 1
|
||||||
|
const contentFromMemorizedState = memorizedState?.uncommittedEditComments?.get(commentItem.id)
|
||||||
|
const viewRef = viewRefs.current[commentItem.id]
|
||||||
|
|
||||||
|
if (viewRef && contentFromMemorizedState !== undefined && !contentRestoredRefs.current[commentItem.id]) {
|
||||||
|
editIndexes[index] = true
|
||||||
|
contentRestoredRefs.current[commentItem.id] = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (contentFromMemorizedState !== commentItem.content) {
|
||||||
|
viewRef.current?.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: viewRef.current.state.doc.length,
|
||||||
|
insert: contentFromMemorizedState
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadSection
|
<ThreadSection
|
||||||
@ -487,7 +612,14 @@ const CommentsThread = <T = unknown,>({
|
|||||||
className: cx(css.optionMenuIcon, css.edit),
|
className: cx(css.optionMenuIcon, css.edit),
|
||||||
iconName: 'Edit',
|
iconName: 'Edit',
|
||||||
text: getString('edit'),
|
text: getString('edit'),
|
||||||
onClick: () => setEditIndexes({ ...editIndexes, ...{ [index]: true } })
|
onClick: () => {
|
||||||
|
setEditIndexes({ ...editIndexes, ...{ [index]: true } })
|
||||||
|
if (memorizedState) {
|
||||||
|
memorizedState.uncommittedEditComments =
|
||||||
|
memorizedState.uncommittedEditComments || new Map()
|
||||||
|
memorizedState.uncommittedEditComments.set(commentItem.id, commentItem.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hasIcon: true,
|
hasIcon: true,
|
||||||
@ -512,7 +644,7 @@ const CommentsThread = <T = unknown,>({
|
|||||||
text: getString('delete'),
|
text: getString('delete'),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
if (await handleAction(CommentAction.DELETE, '', commentItem)) {
|
if (await handleAction(CommentAction.DELETE, '', commentItem)) {
|
||||||
resetStateAtIndex(index)
|
resetStateAtIndex(index, commentItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -538,13 +670,20 @@ const CommentsThread = <T = unknown,>({
|
|||||||
standalone={standalone}
|
standalone={standalone}
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
value={commentItem?.content}
|
value={commentItem?.content}
|
||||||
|
viewRef={viewRefs.current[commentItem.id]}
|
||||||
onSave={async value => {
|
onSave={async value => {
|
||||||
if (await handleAction(CommentAction.UPDATE, value, commentItem)) {
|
if (await handleAction(CommentAction.UPDATE, value, commentItem)) {
|
||||||
commentItem.content = value
|
commentItem.content = value
|
||||||
resetStateAtIndex(index)
|
resetStateAtIndex(index, commentItem)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCancel={() => resetStateAtIndex(index)}
|
onChange={value => {
|
||||||
|
if (memorizedState) {
|
||||||
|
memorizedState.uncommittedEditComments = memorizedState.uncommittedEditComments || new Map()
|
||||||
|
memorizedState.uncommittedEditComments.set(commentItem.id, value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => resetStateAtIndex(index, commentItem)}
|
||||||
setDirty={_dirty => {
|
setDirty={_dirty => {
|
||||||
setDirty(index, _dirty)
|
setDirty(index, _dirty)
|
||||||
}}
|
}}
|
||||||
@ -555,7 +694,7 @@ const CommentsThread = <T = unknown,>({
|
|||||||
save: getString('save'),
|
save: getString('save'),
|
||||||
cancel: getString('cancel')
|
cancel: getString('cancel')
|
||||||
}}
|
}}
|
||||||
autoFocusAndPosition
|
autoFocusAndPosition={contentFromMemorizedState ? false : true}
|
||||||
suggestionBlock={suggestionBlock}
|
suggestionBlock={suggestionBlock}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -41,7 +41,7 @@ import { useStrings } from 'framework/strings'
|
|||||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||||
import type { DiffFileEntry } from 'utils/types'
|
import type { DiffFileEntry } from 'utils/types'
|
||||||
import { useAppContext } from 'AppContext'
|
import { useAppContext } from 'AppContext'
|
||||||
import type { GitFileDiff, TypesPullReq } from 'services/code'
|
import type { GitFileDiff, TypesPullReq, TypesPullReqActivity } from 'services/code'
|
||||||
import { CopyButton } from 'components/CopyButton/CopyButton'
|
import { CopyButton } from 'components/CopyButton/CopyButton'
|
||||||
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
|
import { NavigationCheck } from 'components/NavigationCheck/NavigationCheck'
|
||||||
import type { UseGetPullRequestInfoResult } from 'pages/PullRequest/useGetPullRequestInfo'
|
import type { UseGetPullRequestInfoResult } from 'pages/PullRequest/useGetPullRequestInfo'
|
||||||
@ -58,7 +58,8 @@ import {
|
|||||||
DIFF_VIEWER_HEADER_HEIGHT,
|
DIFF_VIEWER_HEADER_HEIGHT,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
getFileViewedState,
|
getFileViewedState,
|
||||||
FileViewedState
|
FileViewedState,
|
||||||
|
DiffCommentItem
|
||||||
} from './DiffViewerUtils'
|
} from './DiffViewerUtils'
|
||||||
import { usePullReqComments } from './usePullReqComments'
|
import { usePullReqComments } from './usePullReqComments'
|
||||||
import Collapse from '../../icons/collapse.svg'
|
import Collapse from '../../icons/collapse.svg'
|
||||||
@ -234,7 +235,8 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
|
|||||||
containerRef,
|
containerRef,
|
||||||
contentRef,
|
contentRef,
|
||||||
refetchActivities,
|
refetchActivities,
|
||||||
setDirty
|
setDirty,
|
||||||
|
memorizedState
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@ -337,14 +339,16 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
|
|||||||
// Save current innerHTML
|
// Save current innerHTML
|
||||||
contentHTML.current = innerHTML
|
contentHTML.current = innerHTML
|
||||||
|
|
||||||
|
const pre = document.createElement('pre')
|
||||||
|
pre.style.height = clientHeight + 'px'
|
||||||
|
pre.textContent = textContent
|
||||||
|
pre.classList.add(css.offscreenText)
|
||||||
|
|
||||||
|
dom.textContent = ''
|
||||||
|
dom.appendChild(pre)
|
||||||
|
|
||||||
// TODO: Might be good to clean textContent a bit to not include
|
// TODO: Might be good to clean textContent a bit to not include
|
||||||
// diff header info, line numbers, hunk headers, etc...
|
// diff header info, line numbers, hunk headers, etc...
|
||||||
|
|
||||||
// Set innerHTML to a pre tag with the same height to avoid reflow
|
|
||||||
// The pre textContent allows Cmd/Ctrl-F to work
|
|
||||||
dom.innerHTML = `<pre style="height: ${clientHeight + 'px'}" class="${
|
|
||||||
css.offscreenText
|
|
||||||
}">${textContent}</pre>`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -384,7 +388,10 @@ const DiffViewerInternal: React.FC<DiffViewerProps> = ({
|
|||||||
|
|
||||||
if (memorizedState.get(diff.filePath)?.collapsed) {
|
if (memorizedState.get(diff.filePath)?.collapsed) {
|
||||||
setCollapsed(false)
|
setCollapsed(false)
|
||||||
memorizedState.set(diff.filePath, { ...memorizedState.get(diff.filePath), collapsed: false })
|
memorizedState.set(diff.filePath, {
|
||||||
|
...memorizedState.get(diff.filePath),
|
||||||
|
collapsed: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError(getErrorMessage(exception), 0)
|
showError(getErrorMessage(exception), 0)
|
||||||
@ -602,6 +609,14 @@ export interface DiffViewerExchangeState {
|
|||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
useFullDiff?: boolean
|
useFullDiff?: boolean
|
||||||
fullDiff?: DiffFileEntry
|
fullDiff?: DiffFileEntry
|
||||||
|
comments?: Map<number, CommentRestorationTrackingState>
|
||||||
|
commentsVisibilityAtLineNumber?: Map<number, boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentRestorationTrackingState extends DiffCommentItem<TypesPullReqActivity> {
|
||||||
|
uncommittedText?: string
|
||||||
|
showReplyPlaceHolder?: boolean
|
||||||
|
uncommittedEditComments?: Map<number, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { scheduleTask, cancelTask } = createRequestAnimationFrameTaskPool()
|
const { scheduleTask, cancelTask } = createRequestAnimationFrameTaskPool()
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
||||||
import { useMutate } from 'restful-react'
|
import { useMutate } from 'restful-react'
|
||||||
import Selecto from 'selecto'
|
import Selecto from 'selecto'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
@ -35,6 +35,7 @@ import { CodeCommentStatusSelect } from 'components/CodeCommentStatusSelect/Code
|
|||||||
import { dispatchCustomEvent } from 'hooks/useEventListener'
|
import { dispatchCustomEvent } from 'hooks/useEventListener'
|
||||||
import { UseGetPullRequestInfoResult, usePullReqActivities } from 'pages/PullRequest/useGetPullRequestInfo'
|
import { UseGetPullRequestInfoResult, usePullReqActivities } from 'pages/PullRequest/useGetPullRequestInfo'
|
||||||
import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration'
|
import { CommentThreadTopDecoration } from 'components/CommentThreadTopDecoration/CommentThreadTopDecoration'
|
||||||
|
import type { DiffViewerExchangeState } from './DiffViewer'
|
||||||
import {
|
import {
|
||||||
activitiesToDiffCommentItems,
|
activitiesToDiffCommentItems,
|
||||||
activityToCommentItem,
|
activityToCommentItem,
|
||||||
@ -68,6 +69,7 @@ interface UsePullReqCommentsProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
|||||||
contentRef: React.RefObject<HTMLDivElement | null>
|
contentRef: React.RefObject<HTMLDivElement | null>
|
||||||
refetchActivities?: UseGetPullRequestInfoResult['refetchActivities']
|
refetchActivities?: UseGetPullRequestInfoResult['refetchActivities']
|
||||||
setDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
setDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
memorizedState: Map<string, DiffViewerExchangeState>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePullReqComments({
|
export function usePullReqComments({
|
||||||
@ -85,7 +87,8 @@ export function usePullReqComments({
|
|||||||
containerRef,
|
containerRef,
|
||||||
contentRef,
|
contentRef,
|
||||||
refetchActivities,
|
refetchActivities,
|
||||||
setDirty
|
setDirty,
|
||||||
|
memorizedState
|
||||||
}: UsePullReqCommentsProps) {
|
}: UsePullReqCommentsProps) {
|
||||||
const activities = usePullReqActivities()
|
const activities = usePullReqActivities()
|
||||||
const { getString } = useStrings()
|
const { getString } = useStrings()
|
||||||
@ -98,7 +101,18 @@ export function usePullReqComments({
|
|||||||
)
|
)
|
||||||
const { save, update, remove } = useCommentAPI(commentPath)
|
const { save, update, remove } = useCommentAPI(commentPath)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [comments] = useState(new Map<number, DiffCommentItem<TypesPullReqActivity>>())
|
|
||||||
|
const comments = useMemo(() => {
|
||||||
|
let _comments = memorizedState.get(diff.filePath)?.comments
|
||||||
|
|
||||||
|
if (!_comments) {
|
||||||
|
_comments = new Map<number, DiffCommentItem<TypesPullReqActivity>>()
|
||||||
|
memorizedState.set(diff.filePath, { ...memorizedState.get(diff.filePath), comments: _comments })
|
||||||
|
}
|
||||||
|
|
||||||
|
return _comments
|
||||||
|
}, [diff.filePath, memorizedState])
|
||||||
|
|
||||||
const copyLinkToComment = useCallback(
|
const copyLinkToComment = useCallback(
|
||||||
(id, commentItem) => {
|
(id, commentItem) => {
|
||||||
const path = `${routes.toCODEPullRequest({
|
const path = `${routes.toCODEPullRequest({
|
||||||
@ -252,8 +266,18 @@ export function usePullReqComments({
|
|||||||
commentRowElement.innerHTML = `<td colspan="2"></td>`
|
commentRowElement.innerHTML = `<td colspan="2"></td>`
|
||||||
lineInfo.rowElement.after(commentRowElement)
|
lineInfo.rowElement.after(commentRowElement)
|
||||||
|
|
||||||
|
if (!memorizedState.get(diff.filePath)?.commentsVisibilityAtLineNumber) {
|
||||||
|
memorizedState.set(diff.filePath, {
|
||||||
|
...memorizedState.get(diff.filePath),
|
||||||
|
commentsVisibilityAtLineNumber: new Map()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get show commments from memorizedState
|
||||||
|
const showComments = memorizedState.get(diff.filePath)?.commentsVisibilityAtLineNumber?.get(comment.lineNumberEnd)
|
||||||
|
|
||||||
// Set both place-holder and comment box hidden when comment thread is resolved
|
// Set both place-holder and comment box hidden when comment thread is resolved
|
||||||
if (isCommentThreadResolved) {
|
if ((showComments === undefined && isCommentThreadResolved) || showComments === false) {
|
||||||
oppositeRowPlaceHolder.setAttribute('hidden', '')
|
oppositeRowPlaceHolder.setAttribute('hidden', '')
|
||||||
commentRowElement.setAttribute('hidden', '')
|
commentRowElement.setAttribute('hidden', '')
|
||||||
}
|
}
|
||||||
@ -300,6 +324,12 @@ export function usePullReqComments({
|
|||||||
lang: filenameToLanguage(diff.filePath.split('/').pop())
|
lang: filenameToLanguage(diff.filePath.split('/').pop())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vars to verified if a CommentBox is restored from innerHTML/textContent optimization
|
||||||
|
const _memorizedState = memorizedState.get(diff.filePath)?.comments?.get(commentThreadId)
|
||||||
|
const isCommentBoxRestored =
|
||||||
|
(commentThreadId && commentThreadId < 0 && _memorizedState?.uncommittedText !== undefined) ||
|
||||||
|
_memorizedState?.showReplyPlaceHolder === false
|
||||||
|
|
||||||
// Note: CommentBox is rendered as an independent React component.
|
// Note: CommentBox is rendered as an independent React component.
|
||||||
// Everything passed to it must be either values, or refs.
|
// Everything passed to it must be either values, or refs.
|
||||||
// If you pass callbacks or states, they won't be updated and
|
// If you pass callbacks or states, they won't be updated and
|
||||||
@ -311,6 +341,7 @@ export function usePullReqComments({
|
|||||||
routingId={routingId}
|
routingId={routingId}
|
||||||
standalone={standalone}
|
standalone={standalone}
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
|
commentThreadId={commentThreadId}
|
||||||
commentItems={comment._commentItems as CommentItem<TypesPullReqActivity>[]}
|
commentItems={comment._commentItems as CommentItem<TypesPullReqActivity>[]}
|
||||||
initialContent={''}
|
initialContent={''}
|
||||||
width={getCommentBoxWidth(isSideBySide)}
|
width={getCommentBoxWidth(isSideBySide)}
|
||||||
@ -323,7 +354,7 @@ export function usePullReqComments({
|
|||||||
last.style.height = `${boxHeight}px`
|
last.style.height = `${boxHeight}px`
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
autoFocusAndPosition={true}
|
autoFocusAndPosition={isCommentBoxRestored ? false : true}
|
||||||
enableReplyPlaceHolder={(comment._commentItems as CommentItem<TypesPullReqActivity>[])?.length > 0}
|
enableReplyPlaceHolder={(comment._commentItems as CommentItem<TypesPullReqActivity>[])?.length > 0}
|
||||||
onCancel={comment.destroy}
|
onCancel={comment.destroy}
|
||||||
setDirty={setDirty || noop}
|
setDirty={setDirty || noop}
|
||||||
@ -470,6 +501,8 @@ export function usePullReqComments({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
memorizedState={_memorizedState}
|
||||||
|
commentsVisibilityAtLineNumber={memorizedState.get(diff.filePath)?.commentsVisibilityAtLineNumber}
|
||||||
/>
|
/>
|
||||||
</AppWrapper>,
|
</AppWrapper>,
|
||||||
element
|
element
|
||||||
@ -498,7 +531,8 @@ export function usePullReqComments({
|
|||||||
refetchActivities,
|
refetchActivities,
|
||||||
copyLinkToComment,
|
copyLinkToComment,
|
||||||
markSelectedLines,
|
markSelectedLines,
|
||||||
updateDataCommentIds
|
updateDataCommentIds,
|
||||||
|
memorizedState
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user