-
Notifications
You must be signed in to change notification settings - Fork 6
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
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ebb2805
fix: [PIPE-24077]: pull request changes page diff viewer comments and…
shaurya-harness 96e26f8
fix: [PIPE-24077]: pull request changes page diff viewer comments and…
shaurya-harness e3c813e
fix: [PIPE-24077]: pull request changes page diff viewer comments and…
shaurya-harness 78219f7
fix: [PIPE-24077]: pull request changes page diff viewer comments and…
shaurya-harness ee0f992
fix: [PIPE-24077]: pull request changes page diff viewer comments and…
shaurya-harness File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -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, | ||
|
@@ -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) | ||
}) | ||
} | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
}) | ||
} | ||
const submitReview = useCallback( | ||
(decision: PullReqReviewDecision) => { | ||
reviewSubmitPullReq({ | ||
|
@@ -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]) | ||
|
||
|
@@ -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 => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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 || | ||
|
@@ -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 ( | ||
<> | ||
|
@@ -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} | ||
/> | ||
</> | ||
) | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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