feat: [code-218]: pr review button

This commit is contained in:
calvin 2023-04-10 15:00:23 -06:00
parent abd3110a5b
commit c55cb3ae0a
12 changed files with 273 additions and 68 deletions

View File

@ -56,3 +56,45 @@
.repeatBtn {
margin-left: var(--spacing-xsmall) !important;
}
.popover {
transform: translateY(5px) !important;
.menuItem {
strong {
display: inline-block;
margin-left: 10px;
}
p {
font-size: 12px;
padding-left: 27px;
line-height: 16px;
margin: 5px 0;
max-width: 320px;
}
}
.menuReviewItem {
.reviewIcon {
margin-top: 2px;
}
strong {
display: inline-block;
margin-left: 10px;
}
p {
font-size: 12px;
padding-left: var(--spacing-small);
line-height: 16px;
max-width: 320px;
}
}
}
.btn {
&.hide {
visibility: hidden;
}
}

View File

@ -11,5 +11,11 @@ declare const styles: {
readonly hideBtn: string
readonly refreshIcon: string
readonly repeatBtn: string
readonly popover: string
readonly menuItem: string
readonly menuReviewItem: string
readonly reviewIcon: string
readonly btn: string
readonly hide: string
}
export default styles

View File

@ -35,7 +35,7 @@ import { useShowRequestError } from 'hooks/useShowRequestError'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { ChangesDropdown } from './ChangesDropdown'
import { DiffViewConfiguration } from './DiffViewConfiguration'
import { ReviewDecisionButton } from './ReviewDecisionButton/ReviewDecisionButton'
import ReviewSplitButton from './ReviewSplitButton/ReviewSplitButton'
import css from './Changes.module.scss'
const STICKY_TOP_POSITION = 64
@ -73,6 +73,7 @@ export const Changes: React.FC<ChangesProps> = ({
const [lineBreaks, setLineBreaks] = useUserPreference(UserPreference.DIFF_LINE_BREAKS, false)
const [diffs, setDiffs] = useState<DiffFileEntry[]>([])
const [isSticky, setSticky] = useState(false)
const {
data: rawDiff,
error,
@ -209,11 +210,16 @@ export const Changes: React.FC<ChangesProps> = ({
</Container>
<FlexExpander />
<ReviewDecisionButton
<ReviewSplitButton
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
/>
{/* <ReviewDecisionButton
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
/>
/> */}
</Layout.Horizontal>
</Container>

View File

@ -0,0 +1,121 @@
import {
ButtonVariation,
Color,
Container,
Icon,
IconName,
SplitButton,
useToaster,
Text,
FontVariation,
Layout
} from '@harness/uicore'
import { Menu, PopoverPosition } from '@blueprintjs/core'
import cx from 'classnames'
import { useMutate } from 'restful-react'
import React, { useCallback, useState } from 'react'
import { useStrings } from 'framework/strings'
import type { EnumPullReqReviewDecision, TypesPullReq } from 'services/code'
import type { GitInfoProps } from 'utils/GitUtils'
import { getErrorMessage } from 'utils/Utils'
import css from '../Changes.module.scss'
interface PrReviewOption {
method: EnumPullReqReviewDecision | 'reject'
title: string
disabled?: boolean
icon: IconName
color: Color
}
interface ReviewSplitButtonProps extends Pick<GitInfoProps, 'repoMetadata'> {
shouldHide: boolean
pullRequestMetadata?: TypesPullReq
}
const ReviewSplitButton = (props: ReviewSplitButtonProps) => {
const { pullRequestMetadata, repoMetadata, shouldHide } = props
const { getString } = useStrings()
const { showError, showSuccess } = useToaster()
const prDecisionOptions: PrReviewOption[] = [
{
method: 'approved',
title: getString('approve'),
icon: 'tick-circle' as IconName,
color: Color.GREEN_700
},
{
method: 'changereq',
title: getString('requestChanges'),
icon: 'error' as IconName,
color: Color.ORANGE_700
},
{
method: 'reject',
title: getString('reject'),
disabled: true,
icon: 'danger-icon' as IconName,
color: Color.RED_700
}
]
const [decisionOption, setDecisionOption] = useState<PrReviewOption>(prDecisionOptions[0])
const { mutate, loading } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullRequestMetadata?.number}/reviews`
})
const submitReview = useCallback(() => {
mutate({ decision: decisionOption.method })
.then(() => {
// setReset(true)
showSuccess(getString('pr.reviewSubmitted'))
})
.catch(exception => showError(getErrorMessage(exception)))
}, [decisionOption, mutate, showError, showSuccess, getString])
return (
<Container className={cx(css.btn, { [css.hide]: shouldHide })}>
<SplitButton
text={decisionOption.title}
disabled={loading}
variation={ButtonVariation.SECONDARY}
popoverProps={{
interactionKind: 'click',
usePortal: true,
popoverClassName: css.popover,
position: PopoverPosition.BOTTOM_RIGHT,
transitionDuration: 1000
}}
onClick={() => {
submitReview()
}}>
{prDecisionOptions.map(option => {
return (
<Menu.Item
key={option.method}
className={css.menuReviewItem}
disabled={option.disabled}
text={
<Layout.Horizontal>
<Icon
className={css.reviewIcon}
{...(option.icon === 'danger-icon' ? null : { color: option.color })}
size={16}
name={option.icon}
/>
<Text flex width={'fit-content'} font={{ variation: FontVariation.BODY }}>
{option.title}
</Text>
</Layout.Horizontal>
}
onClick={() => {
setDecisionOption(option)
}}
/>
)
})}
</SplitButton>
</Container>
)
}
export default ReviewSplitButton

View File

@ -29,11 +29,11 @@
> ::before {
position: absolute;
top: 34px;
top: 28px;
left: 15px;
content: '';
width: 1px;
height: 28px;
height: 25px;
border: 1px dashed var(--grey-200);
opacity: 0.7;
z-index: 2;
@ -45,7 +45,7 @@
> ::after {
position: absolute;
bottom: -12px;
bottom: -10px;
left: 15px;
content: '';
width: 1px;

View File

@ -241,6 +241,7 @@ export interface StringsMap {
quote: string
readMe: string
refresh: string
reject: string
rejected: string
remove: string
renameFile: string

View File

@ -224,6 +224,7 @@ open: Open
merged: Merged
enabled: Enabled
closed: Closed
reject: Reject
rejected: Rejected
yours: Yours
all: All

View File

@ -538,7 +538,7 @@ 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}>
<Container margin={{ left: 'xsmall' }} width={24} height={24} className={css.mergeContainer}>
<Icon name={CodeIcon.Merged} size={16} color={Color.PURPLE_700} />
</Container>

View File

@ -63,6 +63,20 @@
max-width: 320px;
}
}
.menuReviewItem {
strong {
display: inline-block;
margin-left: 10px;
}
p {
font-size: 12px;
padding-left: 2px;
line-height: 16px;
margin: 0px 1px;
max-width: 320px;
}
}
}
.btnWrapper {

View File

@ -11,6 +11,7 @@ declare const styles: {
readonly sub: string
readonly popover: string
readonly menuItem: string
readonly menuReviewItem: string
readonly btnWrapper: string
readonly hasError: string
readonly mergeContainer: string

View File

@ -17,10 +17,17 @@ 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, OpenapiMergePullReq, OpenapiStatePullReqRequest, TypesPullReq } from 'services/code'
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'> {
@ -135,46 +142,53 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
/>
</Case>
<Case val={PullRequestState.OPEN}>
<Container
inline
className={cx({
[css.btnWrapper]: mergeOption.method !== 'close',
[css.hasError]: mergeable === false
})}>
<SplitButton
text={mergeOption.title}
disabled={loading}
<Layout.Horizontal>
<ReviewSplitButton
shouldHide={(pullRequestMetadata?.state as EnumPullReqState) === 'merged'}
repoMetadata={repoMetadata}
pullRequestMetadata={pullRequestMetadata}
/>
<Container
inline
padding={{ left: 'medium' }}
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)))
[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
}
}}>
{/* TODO: These two items are used for creating a PR
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={
@ -197,25 +211,26 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
</>
}
/> */}
{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>
{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>

View File

@ -172,7 +172,6 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
setEditDesc(ACCESS_MODES.EDIT)
}}
{...permissionProps(permEditResult, standalone)}
/>
</Text>
)}
@ -191,8 +190,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
}}
variation={ButtonVariation.SECONDARY}
text={getString('delete')}
{...permissionProps(permDeleteResult, standalone)}
></Button>
{...permissionProps(permDeleteResult, standalone)}></Button>
</Container>
</Container>
</Layout.Vertical>