drone/web/src/pages/PullRequest/Conversation/PullRequestActionsBox/PullRequestActionsBox.tsx
2023-04-10 15:06:50 -06:00

274 lines
11 KiB
TypeScript

import React, { useState } from 'react'
import {
Button,
ButtonVariation,
Color,
Container,
FlexExpander,
Icon,
Layout,
SplitButton,
StringSubstitute,
Text,
useToaster
} from '@harness/uicore'
import { useMutate } from 'restful-react'
import { Case, Else, Match, Render, Truthy } from 'react-jsx-match'
import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import type {
EnumMergeMethod,
EnumPullReqState,
OpenapiMergePullReq,
OpenapiStatePullReqRequest,
TypesPullReq
} from 'services/code'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
import { getErrorMessage } from 'utils/Utils'
import ReviewSplitButton from 'components/Changes/ReviewSplitButton/ReviewSplitButton'
import css from './PullRequestActionsBox.module.scss'
interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
onPRStateChanged: () => void
}
interface PRMergeOption {
method: EnumMergeMethod | 'close'
title: string
desc: string
disabled?: boolean
}
export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
repoMetadata,
pullRequestMetadata,
onPRStateChanged
}) => {
const { getString } = useStrings()
const { showError } = useToaster()
const { mutate: mergePR, loading } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/merge`
})
const { mutate: updatePRState, loading: loadingState } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata.number}/state`
})
const mergeable = pullRequestMetadata.merge_check_status === 'mergeable'
const isDraft = pullRequestMetadata.is_draft
const mergeOptions: PRMergeOption[] = [
{
method: 'squash',
title: getString('pr.mergeOptions.squashAndMerge'),
desc: getString('pr.mergeOptions.squashAndMergeDesc'),
disabled: true
},
{
method: 'merge',
title: getString('pr.mergeOptions.createMergeCommit'),
desc: getString('pr.mergeOptions.createMergeCommitDesc')
},
{
method: 'rebase',
title: getString('pr.mergeOptions.rebaseAndMerge'),
desc: getString('pr.mergeOptions.rebaseAndMergeDesc'),
disabled: true
},
{
method: 'close',
title: getString('pr.mergeOptions.close'),
desc: getString('pr.mergeOptions.closeDesc')
}
]
const [mergeOption, setMergeOption] = useState<PRMergeOption>(mergeOptions[1])
if (pullRequestMetadata.state === PullRequestFilterOption.MERGED) {
return <MergeInfo pullRequestMetadata={pullRequestMetadata} />
}
return (
<Container
className={cx(css.main, {
[css.error]: mergeable === false
})}>
<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}>
{getString(
isDraft ? 'prState.draftHeading' : mergeable === false ? 'pr.cantBeMerged' : 'pr.branchHasNoConflicts'
)}
</Text>
<FlexExpander />
<Render when={loading || loadingState}>
<Icon name={CodeIcon.InputSpinner} size={16} margin={{ right: 'xsmall' }} />
</Render>
<Match expr={isDraft}>
<Truthy>
<Button
className={css.secondaryButton}
text={getString('pr.readyForReview')}
variation={ButtonVariation.TERTIARY}
onClick={() => {
const payload: OpenapiStatePullReqRequest = { is_draft: false, state: 'open' }
updatePRState(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
}}
/>
</Truthy>
<Else>
<Container>
<Match expr={pullRequestMetadata.state}>
<Case val={PullRequestState.CLOSED}>
<Button
className={css.secondaryButton}
text={getString('pr.openForReview')}
variation={ButtonVariation.TERTIARY}
onClick={() => {
const payload: OpenapiStatePullReqRequest = { state: 'open' }
updatePRState(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
}}
/>
</Case>
<Case val={PullRequestState.OPEN}>
<Layout.Horizontal>
<ReviewSplitButton
shouldHide={(pullRequestMetadata?.state as EnumPullReqState) === 'merged'}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
/>
<Container
inline
padding={{ left: 'medium' }}
className={cx({
[css.btnWrapper]: mergeOption.method !== 'close',
[css.hasError]: mergeable === false
})}>
<SplitButton
text={mergeOption.title}
disabled={loading}
className={cx({
[css.secondaryButton]: mergeOption.method === 'close' || mergeable === false
})}
variation={
mergeOption.method === 'close' || mergeable === false
? ButtonVariation.TERTIARY
: ButtonVariation.PRIMARY
}
popoverProps={{
interactionKind: 'click',
usePortal: true,
popoverClassName: css.popover,
position: PopoverPosition.BOTTOM_RIGHT,
transitionDuration: 1000
}}
onClick={() => {
if (mergeOption.method !== 'close') {
const payload: OpenapiMergePullReq = { method: mergeOption.method }
mergePR(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
} else {
const payload: OpenapiStatePullReqRequest = { state: 'closed' }
updatePRState(payload)
.then(onPRStateChanged)
.catch(exception => showError(getErrorMessage(exception)))
}
}}>
{/* TODO: These two items are used for creating a PR
<Menu.Item
className={css.menuItem}
text={
<>
<BIcon icon="blank" />
<strong>Create pull request</strong>
<p>Open a pull request that is ready for review</p>
<p>Automatically request reviews from code owners</p>
</>
}
/>
<Menu.Item
className={css.menuItem}
text={
<>
<BIcon icon="blank" />
<strong>Create draft pull request</strong>
<p>Does not request code reviews and cannot be merged</p>
<p>Cannot be merged until marked ready for review</p>
</>
}
/> */}
{mergeOptions.map(option => {
return (
<Menu.Item
key={option.method}
className={css.menuItem}
disabled={option.disabled}
text={
<>
<BIcon icon={mergeOption.method === option.method ? 'tick' : 'blank'} />
<strong>{option.title}</strong>
<p>{option.desc}</p>
</>
}
onClick={() => setMergeOption(option)}
/>
)
})}
</SplitButton>
</Container>
</Layout.Horizontal>
</Case>
</Match>
</Container>
</Else>
</Match>
</Layout.Horizontal>
</Container>
</Layout.Vertical>
</Container>
)
}
const MergeInfo: React.FC<{ pullRequestMetadata: TypesPullReq }> = ({ pullRequestMetadata }) => {
const { getString } = useStrings()
return (
<Container className={cx(css.main, css.merged)}>
<Layout.Horizontal spacing="medium" flex={{ alignItems: 'center' }} className={css.layout}>
<Container width={24} height={24} className={css.mergeContainer}>
<Icon name={CodeIcon.Merged} size={20} color={Color.PURPLE_700} />
</Container>
<Container>
{/* <Text className={css.heading}>{getString('pr.prMerged')}</Text> */}
<Text className={css.sub}>
<StringSubstitute
str={getString('pr.prMergedInfo')}
vars={{
user: <strong>{pullRequestMetadata.merger?.display_name}</strong>,
source: <strong>{pullRequestMetadata.source_branch}</strong>,
target: <strong>{pullRequestMetadata.target_branch} </strong>,
time: <ReactTimeago date={pullRequestMetadata.merged as number} />
}}
/>
</Text>
</Container>
<FlexExpander />
</Layout.Horizontal>
</Container>
)
}