Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [PIPE-24077]: pull request changes page diff viewer comments and filters #634

Merged
merged 5 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/gitness/src/components-v2/file-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const FileEditor: FC<FileEditorProps> = ({ repoDetails, defaultBranch })
if (!isNewBranch) {
navigate(`/${spaceId}/repos/${repoId}/code/${fullGitRef}/~/${fileResourcePath}`)
} else {
navigate(`/${spaceId}/repos/${repoId}/pull-requests/compare/${defaultBranch}...${newBranchName}`)
navigate(`/${spaceId}/repos/${repoId}/pulls/compare/${defaultBranch}...${newBranchName}`)
}
}}
currentBranch={fullGitRef || selectedBranchTag?.name}
Expand Down
267 changes: 234 additions & 33 deletions apps/gitness/src/pages-v2/pull-request/pull-request-changes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'

import { DiffModeEnum } from '@git-diff-view/react'
import copy from 'clipboard-copy'
import * as Diff2Html from 'diff2html'
import { atom, useAtom } from 'jotai'
import { compact } from 'lodash-es'
import { useQueryState } from 'nuqs'

import {
commentCreatePullReq,
commentDeletePullReq,
commentUpdatePullReq,
EnumPullReqReviewDecision,
reviewSubmitPullReq,
TypesPullReqActivity,
useFileViewAddPullReqMutation,
useFileViewDeletePullReqMutation,
useFileViewListPullReqQuery,
useListPullReqActivitiesQuery,
useRawDiffQuery,
useReviewerListPullReqQuery
} from '@harnessio/code-service-client'
import { DiffViewerExchangeState, PullRequestChangesPage } from '@harnessio/ui/views'
import {
CommitFilterItemProps,
CreateCommentPullReqRequest,
DiffViewerExchangeState,
FILE_VIEWED_OBSOLETE_SHA,
PullRequestChangesPage
} from '@harnessio/ui/views'

import { useAppContext } from '../../framework/context/AppContext'
import { useGetRepoRef } from '../../framework/hooks/useGetRepoPath'
Expand All @@ -27,22 +42,35 @@ import { usePullRequestProviderStore } from './stores/pull-request-provider-stor

export const changesInfoAtom = atom<{ path?: string; raw?: string; fileViews?: Map<string, string> }>({})

const sortSelectedCommits = (selectedCommits: string[], sortedCommits?: string[]) => {
return selectedCommits
.sort((commitA, commitB) => {
const commitAIdx = sortedCommits?.indexOf(commitA)
const commitBIdx = sortedCommits?.indexOf(commitB)
if (commitBIdx && commitAIdx) {
return commitBIdx + commitAIdx
}
return 0
})
.reverse()
}

export default function PullRequestChanges() {
const { pullReqMetadata, refetchPullReq, refetchActivities, setDiffs } = usePullRequestProviderStore()
const { pullReqMetadata, refetchPullReq, refetchActivities, diffs, setDiffs, pullReqCommits } =
usePullRequestProviderStore()
const { currentUser } = useAppContext()
const repoRef = useGetRepoRef()
const commitSHA = '' // TODO: when you implement commit filter will need commitSHA
const defaultCommitRange = compact(commitSHA?.split(/~1\.\.\.|\.\.\./g))
const [
commitRange
// setCommitRange TODO: add commit view filter dropdown to manage different commits
] = useState(defaultCommitRange)
const [commitRange, setCommitRange] = useState<string[]>()
const allCommitsSHA = useMemo(
() => pullReqCommits?.commits?.map(commit => commit.sha as string),
[pullReqCommits?.commits]
)
const [diffMode, setDiffMode] = useState<DiffModeEnum>(DiffModeEnum.Split)
const targetRef = useMemo(() => pullReqMetadata?.merge_base_sha, [pullReqMetadata?.merge_base_sha])
const sourceRef = useMemo(() => pullReqMetadata?.source_sha, [pullReqMetadata?.source_sha])
// const [diffs, setDiffs] = useState<DiffFileEntry[]>()
const { pullRequestId } = useParams<PathParams>()
const prId = (pullRequestId && Number(pullRequestId)) || -1
const [commentId] = useQueryState('commentId', { defaultValue: '' })
const {
data: { body: reviewers } = {},
refetch: refetchReviewers,
Expand All @@ -51,6 +79,25 @@ export default function PullRequestChanges() {
repo_ref: repoRef,
pullreq_number: prId
})

const deleteComment = (id: number) => {
commentDeletePullReq({ repo_ref: repoRef, pullreq_number: prId, pullreq_comment_id: id })
.then(() => {
refetchActivities()
})
.catch(error => {
console.error('Failed to delete comment:', error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should show error inline in the UI, instead of dumping them in the console

})
}
const updateComment = (id: number, comment: string) => {
commentUpdatePullReq({ repo_ref: repoRef, pullreq_number: prId, pullreq_comment_id: id, body: { text: comment } })
.then(() => {
refetchActivities()
})
.catch(error => {
console.error('Failed to update comment:', error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

})
}
const submitReview = useCallback(
(decision: PullReqReviewDecision) => {
reviewSubmitPullReq({
Expand All @@ -71,15 +118,12 @@ export default function PullRequestChanges() {
},
[refetchActivities, refetchPullReq, pullReqMetadata?.source_sha, refetchReviewers, repoRef, prId]
)
const diffApiPath = useMemo(
() =>
// show range of commits and user selected subrange
commitRange.length > 0
? `${commitRange[0]}~1...${commitRange[commitRange.length - 1]}`
: // show range of commits and user did not select a subrange
`${normalizeGitRef(targetRef)}...${normalizeGitRef(sourceRef)}`,
[commitRange, targetRef, sourceRef]
)
const diffApiPath = useMemo(() => {
// show range of commits and user selected subrange
return commitRange?.length
? `${commitRange[0]}~1...${commitRange[commitRange.length - 1]}`
: `${normalizeGitRef(targetRef)}...${normalizeGitRef(sourceRef)}`
}, [commitRange, targetRef, sourceRef])
const [cachedDiff, setCachedDiff] = useAtom(changesInfoAtom)
const path = useMemo(() => `/api/v1/repos/${repoRef}/+/${diffApiPath}`, [repoRef, diffApiPath])

Expand All @@ -95,30 +139,87 @@ export default function PullRequestChanges() {
}
)

const { data: { body: fileViewsData } = {}, refetch: refetchFileViews } = useFileViewListPullReqQuery({
repo_ref: repoRef,
pullreq_number: prId
})

const { mutateAsync: markViewed } = useFileViewAddPullReqMutation({
repo_ref: repoRef,
pullreq_number: prId
})

const handleMarkViewed = (filePath: string, checksumAfter: string) => {
if (diffs) {
const newDiffs = diffs.map(diff => {
if (diff.fileViews) {
const newFileViews = new Map(diff.fileViews)
newFileViews.set(filePath, checksumAfter ?? 'unknown')
return { ...diff, fileViews: newFileViews }
}
return diff
})
setDiffs(newDiffs)
}
markViewed({
body: {
commit_sha: pullReqMetadata?.source_sha,
path: filePath
}
}).then(() => refetchFileViews())
}

const { mutateAsync: unmarkViewed } = useFileViewDeletePullReqMutation({
repo_ref: repoRef,
pullreq_number: prId
})

const handleUnmarkViewed = (filePath: string) => {
if (diffs) {
const newDiffs = diffs.map(diff => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this business logic be moved to the store instead?

if (diff.fileViews) {
const newFileViews = new Map(diff.fileViews)
newFileViews.delete(filePath)
return { ...diff, fileViews: newFileViews }
}
return diff
})
setDiffs(newDiffs)
}
unmarkViewed({
file_path: filePath
}).then(() => refetchFileViews())
}

useEffect(() => {
if (prId && !fileViewsData && !cachedDiff.fileViews) {
refetchFileViews()
}
}, [prId, fileViewsData, refetchFileViews, cachedDiff.fileViews])

useEffect(
function updateCacheWhenDiffDataArrives() {
if (path && rawDiff && typeof rawDiff === 'string') {
// const fileViews = readOnly
// ? new Map<string, string>()
// : fileViewsData
// ?.filter(({ path: _path, sha }) => _path && sha)
// .reduce((map, { path: _path, sha, obsolete }) => {
// map.set(_path as string, (obsolete ? FILE_VIEWED_OBSOLETE_SHA : sha) as string)
// return map
// }, new Map<string, string>())
const fileViews = fileViewsData
?.filter(({ path: _path, sha }) => _path && sha)
.reduce((map, { path: _path, sha, obsolete }) => {
map.set(_path as string, (obsolete ? FILE_VIEWED_OBSOLETE_SHA : sha) as string)
return map
}, new Map<string, string>())

setCachedDiff({
path,
raw: rawDiff
// fileViews
raw: rawDiff,
fileViews
})
}
},
[
rawDiff,
path,
setCachedDiff
// fileViewsData, readOnly
setCachedDiff,
fileViewsData
// readOnly
]
)
// Diffs are rendered in blocks that can be destroyed when they go off-screen. Hence their internal
Expand All @@ -127,9 +228,7 @@ export default function PullRequestChanges() {
// Map entry: <diff.filePath, DiffViewerExchangeState>
const memorizedState = useMemo(() => new Map<string, DiffViewerExchangeState>(), [])

//
// Parsing diff and construct data structure to pass into DiffViewer component
//
useEffect(() => {
if (
loadingRawDiff ||
Expand Down Expand Up @@ -174,6 +273,96 @@ export default function PullRequestChanges() {
loadingRawDiff,
memorizedState
])
const { data: { body: activityData } = {} } = useListPullReqActivitiesQuery({
repo_ref: repoRef,
pullreq_number: prId,
queryParams: {}
})
const [activities, setActivities] = useState(activityData)

useEffect(() => {
setActivities(
activityData?.map((item: TypesPullReqActivity) => {
return {
author: item?.author,
created: item?.created,
deleted: item?.deleted,
edited: item?.edited,
id: item?.id,
kind: item?.kind,
mentions: item?.mentions,
metadata: item?.metadata,
order: item?.order,
parent_id: item?.parent_id,
payload: item as TypesPullReqActivity,
pullreq_id: item?.pullreq_id,
repo_id: item?.repo_id,
resolved: item?.resolved,
resolver: item?.resolver,
sub_order: item?.sub_order,
text: item?.text,
type: item?.type,
updated: item?.updated
}
})
)
}, [activityData])

const handleSaveComment = (comment: string, parentId?: number, extra?: CreateCommentPullReqRequest) => {
const reqBody = parentId
? {
text: comment,
parent_id: parentId
}
: {
text: comment,
line_end: extra?.line_end,
line_end_new: extra?.line_end_new,
line_start: extra?.line_start,
line_start_new: extra?.line_start_new,
path: extra?.path,
source_commit_sha: sourceRef,
target_commit_sha: targetRef
}
commentCreatePullReq({
repo_ref: repoRef,
pullreq_number: prId,
body: reqBody
})
.then(() => {
refetchActivities()
})
.catch(error => {
// TODO Handle error (e.g., remove the temporary comment or show an error message)
console.error('Failed to save comment:', error)
})
}

const defaultCommitFilter: CommitFilterItemProps = {
name: `All Commits (${pullReqCommits?.commits?.length || 0})`,
value: 'ALL'
}

const [selectedCommits, setSelectedCommits] = useState<CommitFilterItemProps[]>([defaultCommitFilter])

const onCopyClick = (commentId?: number) => {
if (commentId) {
const url = new URL(window.location.href)
url.searchParams.set('commentId', commentId.toString())
copy(url.toString())
}
}

useEffect(() => {
const commitSHA: string[] = []
selectedCommits.map(commit => {
if (commit.value !== defaultCommitFilter.value) {
commitSHA.push(commit.value)
}
})
const newCommitRange = sortSelectedCommits(commitSHA, allCommitsSHA)
setCommitRange(newCommitRange)
}, [selectedCommits])

return (
<>
Expand All @@ -189,6 +378,18 @@ export default function PullRequestChanges() {
currentUser={currentUser}
pullReqMetadata={pullReqMetadata}
loadingRawDiff={loadingRawDiff}
handleSaveComment={handleSaveComment}
pullReqCommits={pullReqCommits?.commits || []}
deleteComment={deleteComment}
updateComment={updateComment}
defaultCommitFilter={defaultCommitFilter}
selectedCommits={selectedCommits}
setSelectedCommits={setSelectedCommits}
markViewed={handleMarkViewed}
unmarkViewed={handleUnmarkViewed}
activities={activities}
commentId={commentId}
onCopyClick={onCopyClick}
/>
</>
)
Expand Down
Loading