mirror of
https://github.com/harness/drone.git
synced 2025-05-31 11:43:15 +00:00
feat: [CODE-298]: Code Comment API integration + Optimize PR polling + implement unchecked status
This commit is contained in:
parent
f3eed8ba00
commit
4e18c009bb
@ -7,8 +7,7 @@ import {
|
||||
Text,
|
||||
StringSubstitute,
|
||||
Button,
|
||||
PageError,
|
||||
ButtonSize
|
||||
PageError
|
||||
} from '@harness/uicore'
|
||||
import { Match, Case, Render } from 'react-jsx-match'
|
||||
import * as Diff2Html from 'diff2html'
|
||||
@ -47,7 +46,6 @@ interface ChangesProps extends Pick<GitInfoProps, 'repoMetadata'> {
|
||||
className?: string
|
||||
onCommentUpdate: () => void
|
||||
prHasChanged?: boolean
|
||||
handleRefresh?: () => void
|
||||
}
|
||||
|
||||
export const Changes: React.FC<ChangesProps> = ({
|
||||
@ -60,8 +58,7 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
pullRequestMetadata,
|
||||
onCommentUpdate,
|
||||
className,
|
||||
prHasChanged,
|
||||
handleRefresh
|
||||
prHasChanged
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const [viewStyle, setViewStyle] = useUserPreference(UserPreference.DIFF_VIEW_STYLE, ViewStyle.SIDE_BY_SIDE)
|
||||
@ -84,8 +81,8 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
const {
|
||||
data: activities,
|
||||
loading: loadingActivities,
|
||||
error: errorActivities
|
||||
// refetch: refetchActivities
|
||||
error: errorActivities,
|
||||
refetch: refetchActivities
|
||||
} = useGet<TypesPullReqActivity[]>({
|
||||
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/activities`,
|
||||
lazy: !pullRequestMetadata?.number
|
||||
@ -104,6 +101,12 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
[diffs]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (prHasChanged) {
|
||||
refetchActivities()
|
||||
}
|
||||
}, [prHasChanged, refetchActivities])
|
||||
|
||||
useEffect(() => {
|
||||
const _raw = rawDiff && typeof rawDiff === 'string' ? rawDiff : ''
|
||||
|
||||
@ -115,7 +118,7 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
const contentId = `content-${fileId}`
|
||||
const filePath = diff.isDeleted ? diff.oldName : diff.newName
|
||||
const fileActivities: TypesPullReqActivity[] | undefined = activities?.filter(
|
||||
activity => filePath === activity.code_comment_path
|
||||
activity => filePath === activity.code_comment?.path
|
||||
)
|
||||
|
||||
return {
|
||||
@ -171,18 +174,6 @@ export const Changes: React.FC<ChangesProps> = ({
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
{!prHasChanged ? null : (
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
iconProps={{ className: css.refreshIcon, size: 12 }}
|
||||
icon="repeat"
|
||||
text={getString('refresh')}
|
||||
variation={ButtonVariation.SECONDARY}
|
||||
size={ButtonSize.SMALL}
|
||||
padding={{ left: 'small' }}
|
||||
className={css.repeatBtn}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Show "Scroll to top" button */}
|
||||
<Render when={isSticky}>
|
||||
|
@ -333,7 +333,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
|
||||
|
||||
case CommentAction.REPLY: {
|
||||
const parentComment = diff.fileActivities?.find(
|
||||
activity => diff.filePath === activity.code_comment_path
|
||||
activity => diff.filePath === activity.code_comment?.path
|
||||
)
|
||||
|
||||
if (parentComment) {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type * as Diff2Html from 'diff2html'
|
||||
// import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui'
|
||||
import HoganJsUtils from 'diff2html/lib/hoganjs-utils'
|
||||
// import type { DiffLine } from 'diff2html/lib/types'
|
||||
import { get } from 'lodash-es'
|
||||
import type { CommentItem } from 'components/CommentBox/CommentBox'
|
||||
import type { TypesPullReqActivity } from 'services/code'
|
||||
import type { DiffFileEntry } from 'utils/types'
|
||||
@ -208,14 +207,13 @@ export function activitiesToDiffCommentItems(diff: DiffFileEntry): DiffCommentIt
|
||||
diff.activities
|
||||
?.filter(replyActivity => replyActivity.parent_id === activity.id)
|
||||
.map(_activity => activityToCommentItem(_activity)) || []
|
||||
// TODO: Use backend support when it's ready https://harness.slack.com/archives/C03Q1Q4C9J8/p1682609265294089
|
||||
const left = activity.payload?.line_start_new || false
|
||||
const right = get(activity.payload, 'line_start_new', false)
|
||||
|
||||
return {
|
||||
left,
|
||||
right: !left,
|
||||
left: !right,
|
||||
right,
|
||||
height: 0,
|
||||
lineNumber: (left ? activity.code_comment_line_old : activity.code_comment_line_new) as number,
|
||||
lineNumber: (right ? activity.code_comment?.line_new : activity.code_comment?.line_old) as number,
|
||||
commentItems: [activityToCommentItem(activity)].concat(replyComments)
|
||||
}
|
||||
}) || []
|
||||
|
@ -192,6 +192,7 @@ export interface StringsMap {
|
||||
'pr.branchHasNoConflicts': string
|
||||
'pr.cantBeMerged': string
|
||||
'pr.cantMerge': string
|
||||
'pr.checkingToMerge': string
|
||||
'pr.createDraftPR': string
|
||||
'pr.descriptionPlaceHolder': string
|
||||
'pr.diffStatsLabel': string
|
||||
|
@ -184,6 +184,7 @@ pr:
|
||||
reviewChanges: Review changes
|
||||
mergePR: Merge pull request
|
||||
branchHasNoConflicts: This branch has no conflicts with the base branch
|
||||
checkingToMerge: Checking for ability to merge automatically...
|
||||
prCanBeMerged: Mergeing can be performed automatically.
|
||||
enterDesc: Enter description here
|
||||
failedToUpdate: Failed to update Pull Request. Please try again.
|
||||
|
@ -5,6 +5,8 @@ import PrRejected from 'images/pull-request-rejected.svg'
|
||||
import PrDraft from 'images/pull-request-draft.svg'
|
||||
import EmptyState from 'images/empty-state.svg'
|
||||
import error404 from 'images/404-error.svg'
|
||||
import PrUnchecked from 'images/pull-request-unchecked.svg'
|
||||
|
||||
export const Images = {
|
||||
PrOpen,
|
||||
PrMerged,
|
||||
@ -12,5 +14,6 @@ export const Images = {
|
||||
PrRejected,
|
||||
PrDraft,
|
||||
error404,
|
||||
EmptyState
|
||||
EmptyState,
|
||||
PrUnchecked
|
||||
}
|
||||
|
1
web/src/images/pull-request-unchecked.svg
Normal file
1
web/src/images/pull-request-unchecked.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 24 25" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="a" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="24" width="24" x="0" y=".5"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy=".5"/><feGaussianBlur stdDeviation="1"/><feColorMatrix type="matrix" values="0 0 0 0 0.376471 0 0 0 0 0.380392 0 0 0 0 0.439216 0 0 0 0.16 0"/><feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow_3325_360836"/><feColorMatrix in="SourceAlpha" result="hardAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation=".5"/><feColorMatrix type="matrix" values="0 0 0 0 0.156863 0 0 0 0 0.160784 0 0 0 0 0.239216 0 0 0 0.08 0"/><feBlend in2="effect1_dropShadow_3325_360836" mode="normal" result="effect2_dropShadow_3325_360836"/><feBlend in="SourceGraphic" in2="effect2_dropShadow_3325_360836" mode="normal" result="shape"/></filter><clipPath id="b"><path d="m4.5 3.25h16.25v16.25h-16.25z"/></clipPath><clipPath id="c"><path d="m4.875 4.5h15v15h-15z"/></clipPath><g filter="url(#a)"><circle cx="12" cy="12" fill="#fff" r="10"/></g><circle cx="12" cy="12" fill="#ff7020" r="9.375"/><g clip-path="url(#b)"><g clip-path="url(#c)"><path d="m12.3687 5.75c-3.44995 0-6.2437 2.8-6.2437 6.25s2.79375 6.25 6.2437 6.25c3.4563 0 6.2563-2.8 6.2563-6.25s-2.8-6.25-6.2563-6.25zm.0063 11.25c-2.7625 0-5-2.2375-5-5s2.2375-5 5-5 5 2.2375 5 5-2.2375 5-5 5zm.3125-8.125h-.9375v3.75l3.2812 1.9688.4688-.7688-2.8125-1.6688z" fill="#fff"/></g></g></svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -1,9 +1,6 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
ButtonSize,
|
||||
ButtonVariation,
|
||||
Color,
|
||||
Container,
|
||||
FlexExpander,
|
||||
@ -44,7 +41,6 @@ import css from './Conversation.module.scss'
|
||||
export interface ConversationProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
||||
onCommentUpdate: () => void
|
||||
prHasChanged?: boolean
|
||||
handleRefresh?: () => void
|
||||
}
|
||||
|
||||
export enum prSortState {
|
||||
@ -58,8 +54,7 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
onCommentUpdate,
|
||||
prHasChanged,
|
||||
handleRefresh
|
||||
prHasChanged
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const { currentUser } = useAppContext()
|
||||
@ -76,9 +71,7 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
})
|
||||
const { showError } = useToaster()
|
||||
const [newComments, setNewComments] = useState<TypesPullReqActivity[]>([])
|
||||
|
||||
const [dateOrderSort, setDateOrderSort] = useState<boolean | 'desc' | 'asc'>(orderSortDate.ASC)
|
||||
|
||||
const [prShowState, setPrShowState] = useState<SelectOption>({
|
||||
label: `Show Everything `,
|
||||
value: 'showEverything'
|
||||
@ -178,12 +171,17 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
const [commentCreated, setCommentCreated] = useState(false)
|
||||
const [dirtyNewComment, setDirtyNewComment] = useState(false)
|
||||
const [dirtyCurrentComments, setDirtyCurrentComments] = useState(false)
|
||||
|
||||
const refreshPR = useCallback(() => {
|
||||
const onPRStateChanged = useCallback(() => {
|
||||
onCommentUpdate()
|
||||
refetchActivities()
|
||||
}, [onCommentUpdate, refetchActivities])
|
||||
|
||||
useEffect(() => {
|
||||
if (prHasChanged) {
|
||||
refetchActivities()
|
||||
}
|
||||
}, [prHasChanged, refetchActivities])
|
||||
|
||||
useAnimateNewCommentBox(commentCreated, setCommentCreated)
|
||||
|
||||
return (
|
||||
@ -193,24 +191,9 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
<PullRequestActionsBox
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
onPRStateChanged={refreshPR}
|
||||
onPRStateChanged={onPRStateChanged}
|
||||
/>
|
||||
<Container>
|
||||
<Layout.Horizontal width={`70%`}>
|
||||
<FlexExpander />
|
||||
{!prHasChanged ? null : (
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
iconProps={{ className: css.refreshIcon, size: 12 }}
|
||||
icon="repeat"
|
||||
text={getString('refresh')}
|
||||
variation={ButtonVariation.SECONDARY}
|
||||
size={ButtonSize.SMALL}
|
||||
margin={{ bottom: 'small' }}
|
||||
/>
|
||||
)}
|
||||
</Layout.Horizontal>
|
||||
|
||||
<Layout.Horizontal>
|
||||
<Container width={`70%`}>
|
||||
<Layout.Vertical spacing="xlarge">
|
||||
@ -378,7 +361,7 @@ export const Conversation: React.FC<ConversationProps> = ({
|
||||
}}
|
||||
outlets={{
|
||||
[CommentBoxOutletPosition.TOP_OF_FIRST_COMMENT]: isCodeComment(commentItems) && (
|
||||
<CodeCommentHeader commentItems={commentItems} />
|
||||
<CodeCommentHeader commentItems={commentItems} threadId={threadId} />
|
||||
),
|
||||
[CommentBoxOutletPosition.LEFT_OF_OPTIONS_MENU]: (
|
||||
<Select
|
||||
@ -456,20 +439,23 @@ function isCodeComment(commentItems: CommentItem<TypesPullReqActivity>[]) {
|
||||
|
||||
interface CodeCommentHeaderProps {
|
||||
commentItems: CommentItem<TypesPullReqActivity>[]
|
||||
threadId: number | undefined
|
||||
}
|
||||
|
||||
const CodeCommentHeader: React.FC<CodeCommentHeaderProps> = ({ commentItems }) => {
|
||||
const CodeCommentHeader: React.FC<CodeCommentHeaderProps> = ({ commentItems, threadId }) => {
|
||||
const _isCodeComment = isCodeComment(commentItems)
|
||||
const id = `code-comment-snapshot-${commentItems[0]?.payload?.code_comment_path}`
|
||||
const id = `code-comment-snapshot-${threadId}`
|
||||
|
||||
useEffect(() => {
|
||||
if (_isCodeComment) {
|
||||
// Note: Since payload does not have information about the file path, mode, and index, and we
|
||||
// don't render them anyway in the UI, we just use dummy info for them.
|
||||
const codeDiffSnapshot = [
|
||||
`diff --git a/hello-world.md b/hello-world.md`,
|
||||
`diff --git a/src b/dest`,
|
||||
`new file mode 100644`,
|
||||
'index 0000000..0000000',
|
||||
'--- /dev/null',
|
||||
'+++ b/hello-world.md',
|
||||
'--- a/src',
|
||||
'+++ b/dest',
|
||||
get(commentItems[0], 'payload.payload.title', ''),
|
||||
...get(commentItems[0], 'payload.payload.lines', [])
|
||||
].join('\n')
|
||||
@ -480,14 +466,14 @@ const CodeCommentHeader: React.FC<CodeCommentHeaderProps> = ({ commentItems }) =
|
||||
Object.assign({}, DIFF2HTML_CONFIG, { outputFormat: ViewStyle.LINE_BY_LINE })
|
||||
).draw()
|
||||
}
|
||||
}, [id, commentItems, _isCodeComment])
|
||||
}, [id, commentItems, _isCodeComment, threadId])
|
||||
|
||||
return _isCodeComment ? (
|
||||
<Container className={css.snapshot}>
|
||||
<Layout.Vertical>
|
||||
<Container className={css.title}>
|
||||
<Text inline className={css.fname}>
|
||||
{commentItems[0].payload?.code_comment_path}
|
||||
{commentItems[0].payload?.code_comment?.path}
|
||||
</Text>
|
||||
</Container>
|
||||
<Container className={css.snapshotContent} id={id} />
|
||||
@ -568,13 +554,11 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
|
||||
return (
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }} className={css.mergedBox}>
|
||||
{/* <Container width={24} height={24} className={css.mergeContainer}> */}
|
||||
<Icon
|
||||
margin={{ left: 'small' }}
|
||||
padding={{ right: 'small' }}
|
||||
{...generateReviewDecisionIcon((payload?.payload as Unknown)?.decision)}
|
||||
/>
|
||||
{/* </Container> */}
|
||||
|
||||
<Avatar name={payload?.author?.display_name as string} size="small" hoverCard={false} />
|
||||
<Text color={Color.GREY_500}>
|
||||
|
@ -16,6 +16,10 @@
|
||||
background-color: var(--red-50) !important;
|
||||
}
|
||||
|
||||
&.unchecked {
|
||||
background-color: #fcf4e3 !important; // Note: No UICore color variable for this background
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: var(--bar-height);
|
||||
padding: 0 var(--spacing-xlarge) !important;
|
||||
@ -43,6 +47,11 @@
|
||||
font-size: 13px !important;
|
||||
line-height: 20px !important;
|
||||
color: var(--grey-700) !important;
|
||||
|
||||
&.unchecked {
|
||||
color: #c05809 !important; // Note: No UICore color variable for this text
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +95,8 @@
|
||||
--background-color-active: var(--grey-100) !important;
|
||||
}
|
||||
|
||||
a, button {
|
||||
a,
|
||||
button {
|
||||
--background-color: var(--green-800) !important;
|
||||
--background-color-hover: var(--green-900) !important;
|
||||
--background-color-active: var(--green-900) !important;
|
||||
|
@ -4,6 +4,7 @@ declare const styles: {
|
||||
readonly main: string
|
||||
readonly merged: string
|
||||
readonly error: string
|
||||
readonly unchecked: string
|
||||
readonly layout: string
|
||||
readonly secondaryButton: string
|
||||
readonly btn: string
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
ButtonVariation,
|
||||
@ -28,7 +28,8 @@ import { useStrings } from 'framework/strings'
|
||||
import { CodeIcon, GitInfoProps, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
|
||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { getErrorMessage, permissionProps } from 'utils/Utils'
|
||||
import { Images } from 'images'
|
||||
import { getErrorMessage, MergeCheckStatus, permissionProps } from 'utils/Utils'
|
||||
import ReviewSplitButton from 'components/Changes/ReviewSplitButton/ReviewSplitButton'
|
||||
import css from './PullRequestActionsBox.module.scss'
|
||||
|
||||
@ -60,7 +61,14 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/state`
|
||||
})
|
||||
const mergeable = pullRequestMetadata.merge_check_status === 'mergeable'
|
||||
const mergeable = useMemo(
|
||||
() => pullRequestMetadata.merge_check_status === MergeCheckStatus.MERGEABLE,
|
||||
[pullRequestMetadata]
|
||||
)
|
||||
const unchecked = useMemo(
|
||||
() => pullRequestMetadata.merge_check_status === MergeCheckStatus.UNCHCKED,
|
||||
[pullRequestMetadata]
|
||||
)
|
||||
const isDraft = pullRequestMetadata.is_draft
|
||||
const mergeOptions: PRMergeOption[] = [
|
||||
{
|
||||
@ -104,19 +112,28 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
return (
|
||||
<Container
|
||||
className={cx(css.main, {
|
||||
[css.error]: mergeable === false
|
||||
[css.error]: mergeable === false && !unchecked,
|
||||
[css.unchecked]: unchecked
|
||||
})}>
|
||||
<Layout.Vertical spacing="xlarge">
|
||||
<Container>
|
||||
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }} className={css.layout}>
|
||||
<Icon
|
||||
name={isDraft ? CodeIcon.Draft : mergeable === false ? 'warning-sign' : 'tick-circle'}
|
||||
size={20}
|
||||
color={isDraft ? Color.ORANGE_900 : mergeable === false ? Color.RED_500 : Color.GREEN_700}
|
||||
/>
|
||||
<Text className={css.sub}>
|
||||
{(unchecked && <img src={Images.PrUnchecked} width={20} height={20} />) || (
|
||||
<Icon
|
||||
name={isDraft ? CodeIcon.Draft : mergeable === false ? 'warning-sign' : 'tick-circle'}
|
||||
size={20}
|
||||
color={isDraft ? Color.ORANGE_900 : mergeable === false ? Color.RED_500 : Color.GREEN_700}
|
||||
/>
|
||||
)}
|
||||
<Text className={cx(css.sub, { [css.unchecked]: unchecked })}>
|
||||
{getString(
|
||||
isDraft ? 'prState.draftHeading' : mergeable === false ? 'pr.cantBeMerged' : 'pr.branchHasNoConflicts'
|
||||
isDraft
|
||||
? 'prState.draftHeading'
|
||||
: unchecked
|
||||
? 'pr.checkingToMerge'
|
||||
: mergeable === false
|
||||
? 'pr.cantBeMerged'
|
||||
: 'pr.branchHasNoConflicts'
|
||||
)}
|
||||
</Text>
|
||||
|
||||
|
@ -23,9 +23,9 @@ import { useAppContext } from 'AppContext'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
|
||||
import { voidFn, getErrorMessage, PR_POLLING_LIMIT } from 'utils/Utils'
|
||||
import { voidFn, getErrorMessage, MergeCheckStatus } from 'utils/Utils'
|
||||
import { CodeIcon, GitInfoProps } from 'utils/GitUtils'
|
||||
import type { TypesPullReq, TypesRepository } from 'services/code'
|
||||
import type { TypesPullReq, TypesPullReqStats, TypesRepository } from 'services/code'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { PullRequestMetaLine } from './PullRequestMetaLine'
|
||||
import { Conversation } from './Conversation/Conversation'
|
||||
@ -34,18 +34,10 @@ import { Changes } from '../../components/Changes/Changes'
|
||||
import { PullRequestCommits } from './PullRequestCommits/PullRequestCommits'
|
||||
import css from './PullRequest.module.scss'
|
||||
|
||||
enum PullRequestSection {
|
||||
CONVERSATION = 'conversation',
|
||||
COMMITS = 'commits',
|
||||
FILES_CHANGED = 'changes',
|
||||
CHECKS = 'checks'
|
||||
}
|
||||
|
||||
export default function PullRequest() {
|
||||
const history = useHistory()
|
||||
const { getString } = useStrings()
|
||||
const { routes } = useAppContext()
|
||||
const [prHasChanged, setPrHasChanged] = useState(false)
|
||||
const {
|
||||
repoMetadata,
|
||||
error,
|
||||
@ -54,49 +46,59 @@ export default function PullRequest() {
|
||||
pullRequestId,
|
||||
pullRequestSection = PullRequestSection.CONVERSATION
|
||||
} = useGetRepositoryMetadata()
|
||||
|
||||
const path = useMemo(
|
||||
() => `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
|
||||
[repoMetadata?.path, pullRequestId]
|
||||
)
|
||||
const {
|
||||
data: prData,
|
||||
error: prError,
|
||||
loading: prLoading,
|
||||
refetch: refetchPullRequest
|
||||
} = useGet<TypesPullReq>({
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
|
||||
path,
|
||||
lazy: !repoMetadata
|
||||
})
|
||||
const {
|
||||
data: pollPrData,
|
||||
error: pollPrError,
|
||||
refetch: refetchPollPullRequest
|
||||
} = useGet<TypesPullReq>({
|
||||
path: `/api/v1/repos/${repoMetadata?.path}/+/pullreq/${pullRequestId}`,
|
||||
lazy: !repoMetadata
|
||||
})
|
||||
const [newPrData, setNewPrData] = useState<TypesPullReq>()
|
||||
|
||||
const handleRefresh = () => {
|
||||
refetchPullRequest()
|
||||
setNewPrData(prData as TypesPullReq)
|
||||
setPrHasChanged(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setTimeout(() => {
|
||||
refetchPollPullRequest()
|
||||
setNewPrData(pollPrData as TypesPullReq)
|
||||
}, PR_POLLING_LIMIT)
|
||||
return () => window.clearTimeout(interval)
|
||||
}, [pollPrData, refetchPollPullRequest])
|
||||
|
||||
useEffect(() => {
|
||||
if (prData?.stats && newPrData?.stats) {
|
||||
const prStatsChanged =
|
||||
prData.stats.commits !== newPrData.stats.commits || prData.stats.files_changed !== newPrData.stats.files_changed
|
||||
if (prStatsChanged) {
|
||||
setPrHasChanged(prStatsChanged)
|
||||
const showSpinner = useMemo(() => {
|
||||
return loading || (prLoading && !prData)
|
||||
}, [loading, prLoading, prData])
|
||||
const [stats, setStats] = useState<TypesPullReqStats>()
|
||||
const prHasChanged = useMemo(() => {
|
||||
if (stats && prData?.stats) {
|
||||
if (
|
||||
stats.commits !== prData.stats.commits ||
|
||||
stats.conversations !== prData.stats.conversations ||
|
||||
stats.files_changed !== prData.stats.files_changed
|
||||
) {
|
||||
window.setTimeout(() => setStats(prData.stats), 50)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}, [newPrData, refetchPollPullRequest, prData?.stats])
|
||||
return false
|
||||
}, [prData?.stats, stats])
|
||||
const mergeable = useMemo(() => prData?.merge_check_status === MergeCheckStatus.MERGEABLE, [prData])
|
||||
|
||||
useEffect(
|
||||
function setStatsIfNotSet() {
|
||||
if (!stats && prData?.stats) {
|
||||
setStats(prData.stats)
|
||||
}
|
||||
},
|
||||
[prData?.stats, stats]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const fn = () => {
|
||||
if (repoMetadata) {
|
||||
refetchPullRequest().then(() => {
|
||||
interval = window.setTimeout(fn, mergeable ? PR_POLLING_INTERVAL : PR_POLLING_INTERVAL_WHEN_NOT_MERGEABLE)
|
||||
})
|
||||
}
|
||||
}
|
||||
let interval = window.setTimeout(fn, mergeable ? PR_POLLING_INTERVAL : PR_POLLING_INTERVAL_WHEN_NOT_MERGEABLE)
|
||||
|
||||
return () => window.clearTimeout(interval)
|
||||
}, [repoMetadata, refetchPullRequest, path, mergeable])
|
||||
|
||||
const activeTab = useMemo(
|
||||
() =>
|
||||
@ -121,8 +123,8 @@ export default function PullRequest() {
|
||||
]
|
||||
}
|
||||
/>
|
||||
<PageBody error={getErrorMessage(error || prError || pollPrError)} retryOnError={voidFn(refetch)}>
|
||||
<LoadingSpinner visible={loading || prLoading} withBorder={!!prData && prLoading} />
|
||||
<PageBody error={getErrorMessage(error || prError)} retryOnError={voidFn(refetch)}>
|
||||
<LoadingSpinner visible={showSpinner} />
|
||||
|
||||
<Render when={repoMetadata && prData}>
|
||||
<>
|
||||
@ -157,7 +159,6 @@ export default function PullRequest() {
|
||||
pullRequestMetadata={prData as TypesPullReq}
|
||||
onCommentUpdate={voidFn(refetchPullRequest)}
|
||||
prHasChanged={prHasChanged}
|
||||
handleRefresh={voidFn(handleRefresh)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@ -176,7 +177,7 @@ export default function PullRequest() {
|
||||
repoMetadata={repoMetadata as TypesRepository}
|
||||
pullRequestMetadata={prData as TypesPullReq}
|
||||
prHasChanged={prHasChanged}
|
||||
handleRefresh={voidFn(handleRefresh)}
|
||||
handleRefresh={voidFn(refetchPullRequest)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@ -201,14 +202,13 @@ export default function PullRequest() {
|
||||
emptyMessage={getString('noChangesPR')}
|
||||
onCommentUpdate={voidFn(refetchPullRequest)}
|
||||
prHasChanged={prHasChanged}
|
||||
handleRefresh={voidFn(handleRefresh)}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: PullRequestSection.CHECKS,
|
||||
disabled: true,
|
||||
disabled: window.location.hostname !== 'localhost', // TODO: Remove when API supports checks
|
||||
title: (
|
||||
<TabTitle
|
||||
icon={CodeIcon.ChecksSuccess}
|
||||
@ -337,3 +337,13 @@ const TabTitle: React.FC<{ icon: IconName; title: string; count?: number; paddin
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
enum PullRequestSection {
|
||||
CONVERSATION = 'conversation',
|
||||
COMMITS = 'commits',
|
||||
FILES_CHANGED = 'changes',
|
||||
CHECKS = 'checks'
|
||||
}
|
||||
|
||||
const PR_POLLING_INTERVAL = 15000
|
||||
const PR_POLLING_INTERVAL_WHEN_NOT_MERGEABLE = 5000
|
||||
|
@ -50,6 +50,7 @@ export default function Repository() {
|
||||
setFileNotExist(false)
|
||||
}
|
||||
}, [resourceError])
|
||||
|
||||
return (
|
||||
<Container className={cx(css.main, !!resourceContent && css.withFileViewer)}>
|
||||
<Match expr={fileNotExist}>
|
||||
@ -101,12 +102,7 @@ export default function Repository() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isRepositoryEmpty && (
|
||||
<EmptyRepositoryInfo
|
||||
repoMetadata={repoMetadata}
|
||||
resourceContent={resourceContent as OpenapiGetContentOutput}
|
||||
/>
|
||||
)}
|
||||
{isRepositoryEmpty && <EmptyRepositoryInfo repoMetadata={repoMetadata} />}
|
||||
</>
|
||||
)}
|
||||
</PageBody>
|
||||
@ -116,10 +112,7 @@ export default function Repository() {
|
||||
)
|
||||
}
|
||||
|
||||
const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'resourceContent'>> = (
|
||||
{ repoMetadata },
|
||||
resourceContent
|
||||
) => {
|
||||
const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata'>> = ({ repoMetadata }) => {
|
||||
const history = useHistory()
|
||||
const { routes } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
@ -146,12 +139,6 @@ const EmptyRepositoryInfo: React.FC<Pick<GitInfoProps, 'repoMetadata' | 'resourc
|
||||
useDisableCodeMainLinks(true)
|
||||
return (
|
||||
<Container className={css.emptyRepo}>
|
||||
<ContentHeader
|
||||
repoMetadata={repoMetadata}
|
||||
gitRef={repoMetadata.default_branch as string}
|
||||
resourcePath={''}
|
||||
resourceContent={resourceContent}
|
||||
/>
|
||||
<Container
|
||||
margin={{ bottom: 'xxlarge' }}
|
||||
padding={{ top: 'xxlarge', bottom: 'xxlarge', left: 'xxlarge', right: 'xxlarge' }}
|
||||
|
@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
.readmeContent {
|
||||
max-width: var(--max-width, calc(100vw - 320px));
|
||||
max-width: 100%;
|
||||
padding: var(--spacing-xxlarge) !important;
|
||||
}
|
||||
}
|
||||
|
@ -353,6 +353,17 @@ export interface RepoSymlinkContent {
|
||||
target?: string
|
||||
}
|
||||
|
||||
export interface TypesCodeCommentFields {
|
||||
line_new?: number
|
||||
line_old?: number
|
||||
merge_base_sha?: string
|
||||
outdated?: boolean
|
||||
path?: string
|
||||
source_sha?: string
|
||||
span_new?: number
|
||||
span_old?: number
|
||||
}
|
||||
|
||||
export interface TypesCommit {
|
||||
author?: TypesSignature
|
||||
committer?: TypesSignature
|
||||
@ -419,13 +430,7 @@ export interface TypesPullReq {
|
||||
|
||||
export interface TypesPullReqActivity {
|
||||
author?: TypesPrincipalInfo
|
||||
code_comment_line_new?: number | null
|
||||
code_comment_line_old?: number | null
|
||||
code_comment_merge_base_sha?: string | null
|
||||
code_comment_path?: string | null
|
||||
code_comment_source_sha?: string | null
|
||||
code_comment_span_new?: number | null
|
||||
code_comment_span_old?: number | null
|
||||
code_comment?: TypesCodeCommentFields
|
||||
created?: number
|
||||
deleted?: number | null
|
||||
edited?: number
|
||||
@ -433,7 +438,6 @@ export interface TypesPullReqActivity {
|
||||
kind?: EnumPullReqActivityKind
|
||||
metadata?: { [key: string]: any } | null
|
||||
order?: number
|
||||
outdated?: boolean | null
|
||||
parent_id?: number | null
|
||||
payload?: {}
|
||||
pullreq_id?: number
|
||||
|
@ -4268,6 +4268,25 @@ components:
|
||||
target:
|
||||
type: string
|
||||
type: object
|
||||
TypesCodeCommentFields:
|
||||
properties:
|
||||
line_new:
|
||||
type: integer
|
||||
line_old:
|
||||
type: integer
|
||||
merge_base_sha:
|
||||
type: string
|
||||
outdated:
|
||||
type: boolean
|
||||
path:
|
||||
type: string
|
||||
source_sha:
|
||||
type: string
|
||||
span_new:
|
||||
type: integer
|
||||
span_old:
|
||||
type: integer
|
||||
type: object
|
||||
TypesCommit:
|
||||
properties:
|
||||
author:
|
||||
@ -4386,27 +4405,8 @@ components:
|
||||
properties:
|
||||
author:
|
||||
$ref: '#/components/schemas/TypesPrincipalInfo'
|
||||
code_comment_line_new:
|
||||
nullable: true
|
||||
type: integer
|
||||
code_comment_line_old:
|
||||
nullable: true
|
||||
type: integer
|
||||
code_comment_merge_base_sha:
|
||||
nullable: true
|
||||
type: string
|
||||
code_comment_path:
|
||||
nullable: true
|
||||
type: string
|
||||
code_comment_source_sha:
|
||||
nullable: true
|
||||
type: string
|
||||
code_comment_span_new:
|
||||
nullable: true
|
||||
type: integer
|
||||
code_comment_span_old:
|
||||
nullable: true
|
||||
type: integer
|
||||
code_comment:
|
||||
$ref: '#/components/schemas/TypesCodeCommentFields'
|
||||
created:
|
||||
type: integer
|
||||
deleted:
|
||||
@ -4424,9 +4424,6 @@ components:
|
||||
type: object
|
||||
order:
|
||||
type: integer
|
||||
outdated:
|
||||
nullable: true
|
||||
type: boolean
|
||||
parent_id:
|
||||
nullable: true
|
||||
type: integer
|
||||
|
@ -10,9 +10,9 @@ export const DEFAULT_BRANCH_NAME = 'main'
|
||||
export const REGEX_VALID_REPO_NAME = /^[a-zA-Z_][0-9a-zA-Z-_.$]*$/
|
||||
export const SUGGESTED_BRANCH_NAMES = [DEFAULT_BRANCH_NAME, 'master']
|
||||
export const FILE_SEPERATOR = '/'
|
||||
export const PR_POLLING_LIMIT = 15000
|
||||
export const INITIAL_ZOOM_LEVEL = 1
|
||||
export const ZOOM_INC_DEC_LEVEL = 0.1
|
||||
|
||||
/** This utility shows a toaster without being bound to any component.
|
||||
* It's useful to show cross-page/component messages */
|
||||
export function showToaster(message: string, props?: Partial<IToastProps>): IToaster {
|
||||
@ -235,3 +235,8 @@ export function waitUntil(condition: () => boolean, callback: () => void, maxCou
|
||||
export const voidFn = (f: Function) => () => {
|
||||
f()
|
||||
}
|
||||
|
||||
export enum MergeCheckStatus {
|
||||
MERGEABLE = 'mergeable',
|
||||
UNCHCKED = 'unchecked'
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user