Implement PR code commenting using payload field (#235)

This commit is contained in:
Tan Nhu 2023-01-19 16:29:38 -08:00 committed by GitHub
parent 5524dfa9ba
commit 56a6dba484
6 changed files with 281 additions and 80 deletions

View File

@ -20,9 +20,15 @@ import { useEventListener } from 'hooks/useEventListener'
import { UserPreference, useUserPreference } from 'hooks/useUserPreference' import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator' import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import type { DiffFileEntry } from 'utils/types' import type { DiffFileEntry } from 'utils/types'
import { DIFF2HTML_CONFIG, ViewStyle } from 'components/DiffViewer/DiffViewerUtils' import {
DIFF2HTML_CONFIG,
PR_CODE_COMMENT_PAYLOAD_VERSION,
PullRequestCodeCommentPayload,
ViewStyle
} from 'components/DiffViewer/DiffViewerUtils'
import { NoResultCard } from 'components/NoResultCard/NoResultCard' import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import type { TypesPullReq } from 'services/code' import type { TypesPullReq, TypesPullReqActivity } from 'services/code'
import { useShowRequestError } from 'hooks/useShowRequestError'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { ChangesDropdown } from './ChangesDropdown' import { ChangesDropdown } from './ChangesDropdown'
import { DiffViewConfiguration } from './DiffViewConfiguration' import { DiffViewConfiguration } from './DiffViewConfiguration'
@ -31,15 +37,15 @@ import css from './Changes.module.scss'
const STICKY_TOP_POSITION = 64 const STICKY_TOP_POSITION = 64
const STICKY_HEADER_HEIGHT = 150 const STICKY_HEADER_HEIGHT = 150
const diffViewerId = (collection: Unknown[]) => collection.filter(Boolean).join('::::') const changedFileId = (collection: Unknown[]) => collection.filter(Boolean).join('::::')
interface ChangesProps extends Pick<GitInfoProps, 'repoMetadata'> { interface ChangesProps extends Pick<GitInfoProps, 'repoMetadata'> {
targetBranch?: string targetBranch?: string
sourceBranch?: string sourceBranch?: string
readOnly?: boolean readOnly?: boolean
pullRequestMetadata?: TypesPullReq
emptyTitle: string emptyTitle: string
emptyMessage: string emptyMessage: string
pullRequestMetadata?: TypesPullReq
className?: string className?: string
} }
@ -48,9 +54,9 @@ export const Changes: React.FC<ChangesProps> = ({
targetBranch, targetBranch,
sourceBranch, sourceBranch,
readOnly, readOnly,
pullRequestMetadata,
emptyTitle, emptyTitle,
emptyMessage, emptyMessage,
pullRequestMetadata,
className className
}) => { }) => {
const { getString } = useStrings() const { getString } = useStrings()
@ -67,6 +73,15 @@ export const Changes: React.FC<ChangesProps> = ({
path: `/api/v1/repos/${repoMetadata?.path}/+/compare/${targetBranch}...${sourceBranch}`, path: `/api/v1/repos/${repoMetadata?.path}/+/compare/${targetBranch}...${sourceBranch}`,
lazy: !targetBranch || !sourceBranch lazy: !targetBranch || !sourceBranch
}) })
const {
data: activities,
loading: loadingActivities,
error: errorActivities
// refetch: refetchActivities
} = useGet<TypesPullReqActivity[]>({
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/activities`,
lazy: !pullRequestMetadata?.number
})
const diffStats = useMemo( const diffStats = useMemo(
() => () =>
@ -87,25 +102,44 @@ export const Changes: React.FC<ChangesProps> = ({
if (rawDiff) { if (rawDiff) {
setDiffs( setDiffs(
Diff2Html.parse(_raw, DIFF2HTML_CONFIG).map(diff => { Diff2Html.parse(_raw, DIFF2HTML_CONFIG).map(diff => {
const viewerId = diffViewerId([diff.oldName, diff.newName]) const fileId = changedFileId([diff.oldName, diff.newName])
const containerId = `container-${viewerId}` const containerId = `container-${fileId}`
const contentId = `content-${viewerId}` const contentId = `content-${fileId}`
const fileTitle = diff.isDeleted
? diff.oldName
: diff.isRename
? `${diff.oldName} -> ${diff.newName}`
: diff.newName
const fileActivities: TypesPullReqActivity[] | undefined = activities?.filter(activity => {
const payload = activity.payload as PullRequestCodeCommentPayload
return payload?.file_id === fileId && payload?.version === PR_CODE_COMMENT_PAYLOAD_VERSION
})
return { ...diff, containerId, contentId } return {
...diff,
containerId,
contentId,
fileId,
fileTitle,
fileActivities: fileActivities || [],
activities: activities || []
}
}) })
) )
} }
}, [rawDiff]) }, [rawDiff, activities])
useEventListener( useEventListener(
'scroll', 'scroll',
useCallback(() => setSticky(window.scrollY >= STICKY_HEADER_HEIGHT), []) useCallback(() => setSticky(window.scrollY >= STICKY_HEADER_HEIGHT), [])
) )
useShowRequestError(errorActivities)
return ( return (
<Container className={cx(css.container, className)} {...(!!loading || !!error ? { flex: true } : {})}> <Container className={cx(css.container, className)} {...(!!loading || !!error ? { flex: true } : {})}>
<LoadingSpinner visible={loading} /> <LoadingSpinner visible={loading || loadingActivities} />
{error && <PageError message={getErrorMessage(error)} onClick={voidFn(refetch)} />} {error && <PageError message={getErrorMessage(error || errorActivities)} onClick={voidFn(refetch)} />}
{!loading && {!loading &&
!error && !error &&
(diffs?.length ? ( (diffs?.length ? (
@ -158,18 +192,24 @@ export const Changes: React.FC<ChangesProps> = ({
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>
<Layout.Vertical spacing="large" className={cx(css.main, { [css.enableDiffLineBreaks]: lineBreaks })}> {/* TODO: lineBreaks is broken in line-by-line view, enable it for side-by-side only now */}
<Layout.Vertical
spacing="large"
className={cx(css.main, {
[css.enableDiffLineBreaks]: lineBreaks && viewStyle === ViewStyle.SIDE_BY_SIDE
})}>
{diffs?.map((diff, index) => ( {diffs?.map((diff, index) => (
// Note: `key={viewStyle + index + lineBreaks}` resets DiffView when view configuration // Note: `key={viewStyle + index + lineBreaks}` resets DiffView when view configuration
// is changed. Making it easier to control states inside DiffView itself, as it does not // is changed. Making it easier to control states inside DiffView itself, as it does not
// have to deal with any view configuration // have to deal with any view configuration
<DiffViewer <DiffViewer
readOnly={readOnly} readOnly={readOnly}
index={index}
key={viewStyle + index + lineBreaks} key={viewStyle + index + lineBreaks}
diff={diff} diff={diff}
viewStyle={viewStyle} viewStyle={viewStyle}
stickyTopPosition={STICKY_TOP_POSITION} stickyTopPosition={STICKY_TOP_POSITION}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
/> />
))} ))}
</Layout.Vertical> </Layout.Vertical>

View File

@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useMutate } from 'restful-react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { useInView } from 'react-intersection-observer' import { useInView } from 'react-intersection-observer'
import { import {
@ -10,35 +11,43 @@ import {
Layout, Layout,
Text, Text,
ButtonSize, ButtonSize,
Intent useToaster
} from '@harness/uicore' } from '@harness/uicore'
import cx from 'classnames' import cx from 'classnames'
import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui' import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui'
import { noop } from 'lodash-es'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import { CodeIcon } from 'utils/GitUtils' import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
import { useEventListener } from 'hooks/useEventListener' import { useEventListener } from 'hooks/useEventListener'
import type { DiffFileEntry } from 'utils/types' import type { DiffFileEntry } from 'utils/types'
import { useConfirmAct } from 'hooks/useConfirmAction'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator' import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { useConfirmAction } from 'hooks/useConfirmAction'
import { useAppContext } from 'AppContext' import { useAppContext } from 'AppContext'
import type { TypesPullReq, TypesPullReqActivity } from 'services/code'
import { getErrorMessage } from 'utils/Utils'
import { import {
activitiesToDiffCommentItems,
activityToCommentItem,
CommentType,
DIFF2HTML_CONFIG, DIFF2HTML_CONFIG,
DiffCommentItem, DiffCommentItem,
DIFF_VIEWER_HEADER_HEIGHT, DIFF_VIEWER_HEADER_HEIGHT,
getCommentLineInfo, getCommentLineInfo,
getDiffHTMLSnapshot,
getRawTextInRange,
PR_CODE_COMMENT_PAYLOAD_VERSION,
PullRequestCodeCommentPayload,
renderCommentOppositePlaceHolder, renderCommentOppositePlaceHolder,
ViewStyle ViewStyle
} from './DiffViewerUtils' } from './DiffViewerUtils'
import { CommentBox } from '../CommentBox/CommentBox' import { CommentAction, CommentBox, CommentItem } from '../CommentBox/CommentBox'
import css from './DiffViewer.module.scss' import css from './DiffViewer.module.scss'
interface DiffViewerProps { interface DiffViewerProps extends Pick<GitInfoProps, 'repoMetadata'> {
index: number
diff: DiffFileEntry diff: DiffFileEntry
viewStyle: ViewStyle viewStyle: ViewStyle
stickyTopPosition?: number stickyTopPosition?: number
readOnly?: boolean readOnly?: boolean
pullRequestMetadata?: TypesPullReq
} }
// //
@ -46,7 +55,14 @@ interface DiffViewerProps {
// Avoid React re-rendering at all cost as it might cause unresponsive UI // Avoid React re-rendering at all cost as it might cause unresponsive UI
// when diff content is big, or when a PR has a lot of changed files. // when diff content is big, or when a PR has a lot of changed files.
// //
export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle, stickyTopPosition = 0, readOnly }) => { export const DiffViewer: React.FC<DiffViewerProps> = ({
diff,
viewStyle,
stickyTopPosition = 0,
readOnly,
repoMetadata,
pullRequestMetadata
}) => {
const { getString } = useStrings() const { getString } = useStrings()
const [viewed, setViewed] = useState(false) const [viewed, setViewed] = useState(false)
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false)
@ -58,42 +74,17 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
const { ref: inViewRef, inView } = useInView({ rootMargin: '100px 0px' }) const { ref: inViewRef, inView } = useInView({ rootMargin: '100px 0px' })
const containerRef = useRef<HTMLDivElement | null>(null) const containerRef = useRef<HTMLDivElement | null>(null)
const { currentUser } = useAppContext() const { currentUser } = useAppContext()
const executeDeleteComentConfirmation = useConfirmAction({ const { showError } = useToaster()
title: getString('delete'), const confirmAct = useConfirmAct()
intent: Intent.DANGER, const path = useMemo(
message: <Text>{getString('deleteCommentConfirm')}</Text>, () => `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/comments`,
action: async ({ commentEntry, onSuccess = noop }) => { [repoMetadata.path, pullRequestMetadata?.number]
// TODO: Delete comment
onSuccess('Delete ', commentEntry)
}
})
const [comments, setComments] = useState<DiffCommentItem[]>(
!index
? [
{
left: false,
right: true,
height: 0,
lineNumber: 11,
commentItems: [
`Logs will looks similar to\n\n<img width="1494" alt="image" src="https://user-images.githubusercontent.com/98799615/207994246-19ce9eb2-604f-4226-9a3c-6f4125d3b7cc.png">\n\n\ngitrpc logs using the \`ctx\` will have the following annotations:\n- \`grpc.service=rpc.ReferenceService\`\n- \`grpc.method=CreateBranch\`\n- \`grpc.peer=127.0.0.1:49364\`\n- \`grpc.request_id=cedrl6p1eqltblt13mgg\``,
`it seems we don't actually do anything with the explicit error type other than calling .Error(), which technically we could do on the original err object too? unless I'm missing something, could we then use errors.Is instead? (would avoid the extra var definitions at the top)`
//`If error is not converted then it will be detailed error: in BranchDelete: Branch doesn't exists. What we want is human readable error: Branch 'name' doesn't exists.`,
// `* GitRPC isolated errors, bcoz this will be probably separate repo in future and we dont want every where to include grpc status codes in our main app\n* Errors are explicit for repsonses based on error passing by types`,
// `> global ctx in wire will kill all routines, right? is this affect middlewares and interceptors? because requests should finish they work, right?\n\nI've changed the code now to pass the config directly instead of the systemstore and context, to avoid confusion (what we discussed yesterday - I remove systemstore itself another time).`
].map(content => ({
author: 'Tan Nhu',
created: '2022-12-21',
updated: '2022-12-21',
deleted: 0,
content
}))
}
]
: []
) )
const commentsRef = useRef<DiffCommentItem[]>(comments) const { mutate: saveComment } = useMutate({ verb: 'POST', path })
const { mutate: updateComment } = useMutate({ verb: 'PATCH', path: ({ id }) => `${path}/${id}` })
const { mutate: deleteComment } = useMutate({ verb: 'DELETE', path: ({ id }) => `${path}/${id}` })
const [comments, setComments] = useState<DiffCommentItem<TypesPullReqActivity>[]>(activitiesToDiffCommentItems(diff))
const commentsRef = useRef<DiffCommentItem<TypesPullReqActivity>[]>(comments)
const setContainerRef = useCallback( const setContainerRef = useCallback(
node => { node => {
containerRef.current = node containerRef.current = node
@ -282,11 +273,10 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
<CommentBox <CommentBox
commentItems={comment.commentItems} commentItems={comment.commentItems}
getString={getString} getString={getString}
width={isSideBySide ? 'calc(100vw / 2 - 163px)' : undefined} width={isSideBySide ? 'calc(100vw / 2 - 163px)' : undefined} // TODO: Re-calcualte for standalone version
onHeightChange={boxHeight => { onHeightChange={boxHeight => {
if (comment.height !== boxHeight) { if (comment.height !== boxHeight) {
comment.height = boxHeight comment.height = boxHeight
// element.style.height = `${boxHeight}px`
setTimeout(() => setComments([...commentsRef.current]), 0) setTimeout(() => setComments([...commentsRef.current]), 0)
} }
}} }}
@ -301,7 +291,90 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
setTimeout(() => setComments(commentsRef.current.filter(item => item !== comment)), 0) setTimeout(() => setComments(commentsRef.current.filter(item => item !== comment)), 0)
}} }}
currentUserName={currentUser.display_name} currentUserName={currentUser.display_name}
handleAction={async () => [true, undefined]} // TODO: Integrate with API handleAction={async (action, value, commentItem) => {
let result = true
let updatedItem: CommentItem<TypesPullReqActivity> | undefined = undefined
const id = (commentItem as CommentItem<TypesPullReqActivity>)?.payload?.id
switch (action) {
case CommentAction.NEW: {
// This can be used to allow multiple-line selection when commenting
const lineNumberRange = [comment.lineNumber]
const payload: PullRequestCodeCommentPayload = {
type: CommentType.PR_CODE_COMMENT,
version: PR_CODE_COMMENT_PAYLOAD_VERSION,
file_id: diff.fileId,
file_title: diff.fileTitle,
language: diff.language || '',
is_on_left: comment.left,
at_line_number: comment.lineNumber,
line_number_range: lineNumberRange,
range_text_content: getRawTextInRange(diff, lineNumberRange),
diff_html_snapshot: getDiffHTMLSnapshot(rowElement)
}
await saveComment({ text: value, payload })
.then((newComment: TypesPullReqActivity) => {
updatedItem = activityToCommentItem(newComment)
})
.catch(exception => {
result = false
showError(getErrorMessage(exception), 0)
})
break
}
case CommentAction.REPLY: {
const rootComment = diff.fileActivities?.find(
activity => (activity.payload as PullRequestCodeCommentPayload).file_id === diff.fileId
)
if (rootComment) {
await saveComment({ text: value, parent_id: Number(rootComment.id as number) })
.then(newComment => {
updatedItem = activityToCommentItem(newComment)
})
.catch(exception => {
result = false
showError(getErrorMessage(exception), 0)
})
}
break
}
case CommentAction.DELETE: {
result = false
await confirmAct({
message: getString('deleteCommentConfirm'),
action: async () => {
await deleteComment({}, { pathParams: { id } })
.then(() => {
result = true
})
.catch(exception => {
result = false
showError(getErrorMessage(exception), 0, getString('pr.failedToDeleteComment'))
})
}
})
break
}
case CommentAction.UPDATE: {
await updateComment({ text: value }, { pathParams: { id } })
.then(newComment => {
updatedItem = activityToCommentItem(newComment)
})
.catch(exception => {
result = false
showError(getErrorMessage(exception), 0)
})
break
}
}
return [result, updatedItem]
}}
/>, />,
element element
) )
@ -318,7 +391,19 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
// } // }
}) })
}, },
[comments, viewStyle, getString, currentUser, executeDeleteComentConfirmation, readOnly] [
comments,
viewStyle,
getString,
currentUser,
readOnly,
diff,
saveComment,
showError,
updateComment,
deleteComment,
confirmAct
]
) )
useEffect(function cleanUpCommentBoxRendering() { useEffect(function cleanUpCommentBoxRendering() {
@ -361,7 +446,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({ diff, index, viewStyle,
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>
<Text inline className={css.fname}> <Text inline className={css.fname}>
{diff.isDeleted ? diff.oldName : diff.isRename ? `${diff.oldName} -> ${diff.newName}` : diff.newName} {diff.fileTitle}
</Text> </Text>
<Button variation={ButtonVariation.ICON} icon={CodeIcon.Copy} size={ButtonSize.SMALL} /> <Button variation={ButtonVariation.ICON} icon={CodeIcon.Copy} size={ButtonSize.SMALL} />
<FlexExpander /> <FlexExpander />

View File

@ -1,22 +1,43 @@
import type * as Diff2Html from 'diff2html' import type * as Diff2Html from 'diff2html'
import HoganJsUtils from 'diff2html/lib/hoganjs-utils' import HoganJsUtils from 'diff2html/lib/hoganjs-utils'
import type { CommentItem } from 'components/CommentBox/CommentBox' import type { CommentItem } from 'components/CommentBox/CommentBox'
import type { TypesPullReqActivity } from 'services/code'
import type { DiffFileEntry } from 'utils/types'
export enum ViewStyle { export enum ViewStyle {
SIDE_BY_SIDE = 'side-by-side', SIDE_BY_SIDE = 'side-by-side',
LINE_BY_LINE = 'line-by-line' LINE_BY_LINE = 'line-by-line'
} }
export enum CommentType {
PR_CODE_COMMENT = 'pr_code_comment'
}
export const PR_CODE_COMMENT_PAYLOAD_VERSION = '0.1'
export interface PullRequestCodeCommentPayload {
type: CommentType
version: string // used to avoid rendering old payload structure
file_id: string // unique id of the changed file
file_title: string
language: string
is_on_left: boolean // comment made on the left side pane
at_line_number: number
line_number_range: number[]
range_text_content: string // raw text content where the comment is made
diff_html_snapshot: string // snapshot used to render diff in comment (PR Conversation). Could be used to send email notification too (with more work on capturing CSS styles and put them inline)
}
export const DIFF_VIEWER_HEADER_HEIGHT = 36 export const DIFF_VIEWER_HEADER_HEIGHT = 36
// const DIFF_MAX_CHANGES = 100 // const DIFF_MAX_CHANGES = 100
// const DIFF_MAX_LINE_LENGTH = 100 // const DIFF_MAX_LINE_LENGTH = 100
export interface DiffCommentItem { export interface DiffCommentItem<T = Unknown> {
left: boolean left: boolean
right: boolean right: boolean
lineNumber: number lineNumber: number
height: number height: number
commentItems: CommentItem[] commentItems: CommentItem<T>[]
} }
export const DIFF2HTML_CONFIG = { export const DIFF2HTML_CONFIG = {
@ -159,3 +180,60 @@ export function renderCommentOppositePlaceHolder(annotation: DiffCommentItem, op
` `
oppositeRowElement.after(placeHolderRow) oppositeRowElement.after(placeHolderRow)
} }
export const activityToCommentItem = (activity: TypesPullReqActivity): CommentItem<TypesPullReqActivity> => ({
author: activity.author?.display_name as string,
created: activity.created as number,
updated: activity.edited as number,
deleted: activity.deleted as number,
content: (activity.text || activity.payload?.Message) as string,
payload: activity
})
/**
* Take a small HTML snapshot of a diff in order to render code comment.
* @param atRow Row element where the comment is placed.
* @param maxNumberOfLines Maximum number of lines to take.
* @returns HTML content of the diff.
*/
export function getDiffHTMLSnapshot(atRow: HTMLTableRowElement, maxNumberOfLines = 3) {
const diffSnapshot = [atRow.outerHTML.trim()]
let prev = atRow.previousElementSibling
while (prev && diffSnapshot.length < maxNumberOfLines) {
diffSnapshot.unshift((prev.outerHTML || '').trim())
prev = prev.previousElementSibling
}
return diffSnapshot.join('')
}
export function getRawTextInRange(diff: DiffFileEntry, lineNumberRange: number[]) {
return (
(diff?.blocks[0]?.lines || [])
.filter(line => lineNumberRange.includes(line.newNumber as number))
.map(line => line.content)
.join('\n') || ''
)
}
export function activitiesToDiffCommentItems(diff: DiffFileEntry): DiffCommentItem<TypesPullReqActivity>[] {
return (
diff.fileActivities?.map(activity => {
const payload = activity.payload as PullRequestCodeCommentPayload
const replyComments =
diff.activities
?.filter(replyActivity => replyActivity.parent_id === activity.id)
.map(_activity => activityToCommentItem(_activity)) || []
return {
left: payload.is_on_left,
right: !payload.is_on_left,
height: 0,
lineNumber: payload.at_line_number,
commentItems: [activityToCommentItem(activity)].concat(replyComments)
}
}) || []
)
}

View File

@ -1,5 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import cx from 'classnames' import cx from 'classnames'
import { omit } from 'lodash-es'
import { Classes, IMenuItemProps, Menu } from '@blueprintjs/core' import { Classes, IMenuItemProps, Menu } from '@blueprintjs/core'
import { Button, ButtonProps } from '@harness/uicore' import { Button, ButtonProps } from '@harness/uicore'
import type { PopoverProps } from '@harness/uicore/dist/components/Popover/Popover' import type { PopoverProps } from '@harness/uicore/dist/components/Popover/Popover'
@ -38,7 +39,7 @@ export const OptionsMenuButton = ({
<Menu.Item <Menu.Item
key={(item as React.ComponentProps<typeof Menu.Item>)?.text as string} key={(item as React.ComponentProps<typeof Menu.Item>)?.text as string}
className={cx(Classes.POPOVER_DISMISS, { [css.danger]: (item as OptionsMenuItem).isDanger })} className={cx(Classes.POPOVER_DISMISS, { [css.danger]: (item as OptionsMenuItem).isDanger })}
{...(item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>)} {...omit(item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>, 'isDanger')}
/> />
) )
)} )}

View File

@ -24,6 +24,7 @@ import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButto
import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview' import { MarkdownEditorWithPreview } from 'components/MarkdownEditorWithPreview/MarkdownEditorWithPreview'
import { useConfirmAct } from 'hooks/useConfirmAction' import { useConfirmAct } from 'hooks/useConfirmAction'
import { getErrorMessage } from 'utils/Utils' import { getErrorMessage } from 'utils/Utils'
import { activityToCommentItem } from 'components/DiffViewer/DiffViewerUtils'
import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper' import { PullRequestTabContentWrapper } from '../PullRequestTabContentWrapper'
import { PullRequestStatusInfo } from './PullRequestStatusInfo/PullRequestStatusInfo' import { PullRequestStatusInfo } from './PullRequestStatusInfo/PullRequestStatusInfo'
import css from './Conversation.module.scss' import css from './Conversation.module.scss'
@ -53,7 +54,7 @@ export const Conversation: React.FC<ConversationProps> = ({
const threads: Record<number, CommentItem<TypesPullReqActivity>[]> = {} const threads: Record<number, CommentItem<TypesPullReqActivity>[]> = {}
activities?.forEach(activity => { activities?.forEach(activity => {
const thread: CommentItem<TypesPullReqActivity> = toCommentItem(activity) const thread: CommentItem<TypesPullReqActivity> = activityToCommentItem(activity)
if (activity.parent_id) { if (activity.parent_id) {
threads[activity.parent_id].push(thread) threads[activity.parent_id].push(thread)
@ -64,7 +65,7 @@ export const Conversation: React.FC<ConversationProps> = ({
}) })
newComments.forEach(newComment => { newComments.forEach(newComment => {
threads[newComment.id as number] = [toCommentItem(newComment)] threads[newComment.id as number] = [activityToCommentItem(newComment)]
}) })
return threads return threads
@ -138,7 +139,7 @@ export const Conversation: React.FC<ConversationProps> = ({
case CommentAction.REPLY: case CommentAction.REPLY:
await saveComment({ text: value, parent_id: Number(threadId) }) await saveComment({ text: value, parent_id: Number(threadId) })
.then(newComment => { .then(newComment => {
updatedItem = toCommentItem(newComment) updatedItem = activityToCommentItem(newComment)
}) })
.catch(exception => { .catch(exception => {
result = false result = false
@ -149,7 +150,7 @@ export const Conversation: React.FC<ConversationProps> = ({
case CommentAction.UPDATE: case CommentAction.UPDATE:
await updateComment({ text: value }, { pathParams: { id } }) await updateComment({ text: value }, { pathParams: { id } })
.then(newComment => { .then(newComment => {
updatedItem = toCommentItem(newComment) updatedItem = activityToCommentItem(newComment)
}) })
.catch(exception => { .catch(exception => {
result = false result = false
@ -177,7 +178,7 @@ export const Conversation: React.FC<ConversationProps> = ({
await saveComment({ text: value }) await saveComment({ text: value })
.then((newComment: TypesPullReqActivity) => { .then((newComment: TypesPullReqActivity) => {
updatedItem = toCommentItem(newComment) updatedItem = activityToCommentItem(newComment)
setNewComments([...newComments, newComment]) setNewComments([...newComments, newComment])
}) })
.catch(exception => { .catch(exception => {
@ -320,12 +321,3 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
console.warn('Unable to render system type activity', commentItems) console.warn('Unable to render system type activity', commentItems)
return null return null
} }
const toCommentItem = (activity: TypesPullReqActivity) => ({
author: activity.author?.display_name as string,
created: activity.created as number,
updated: activity.edited as number,
deleted: activity.deleted as number,
content: (activity.text || activity.payload?.Message) as string,
payload: activity
})

View File

@ -1,6 +1,11 @@
import type { DiffFile } from 'diff2html/lib/types' import type { DiffFile } from 'diff2html/lib/types'
import type { TypesPullReqActivity } from 'services/code'
export interface DiffFileEntry extends DiffFile { export interface DiffFileEntry extends DiffFile {
fileId: string
fileTitle: string
containerId: string containerId: string
contentId: string contentId: string
fileActivities?: TypesPullReqActivity[]
activities?: TypesPullReqActivity[]
} }