mirror of
https://github.com/harness/drone.git
synced 2025-05-05 04:49:32 +08:00
feat: [code-218]: pr review button
This commit is contained in:
parent
abd3110a5b
commit
c55cb3ae0a
@ -56,3 +56,45 @@
|
|||||||
.repeatBtn {
|
.repeatBtn {
|
||||||
margin-left: var(--spacing-xsmall) !important;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,5 +11,11 @@ declare const styles: {
|
|||||||
readonly hideBtn: string
|
readonly hideBtn: string
|
||||||
readonly refreshIcon: string
|
readonly refreshIcon: string
|
||||||
readonly repeatBtn: 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
|
export default styles
|
||||||
|
@ -35,7 +35,7 @@ import { useShowRequestError } from 'hooks/useShowRequestError'
|
|||||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||||
import { ChangesDropdown } from './ChangesDropdown'
|
import { ChangesDropdown } from './ChangesDropdown'
|
||||||
import { DiffViewConfiguration } from './DiffViewConfiguration'
|
import { DiffViewConfiguration } from './DiffViewConfiguration'
|
||||||
import { ReviewDecisionButton } from './ReviewDecisionButton/ReviewDecisionButton'
|
import ReviewSplitButton from './ReviewSplitButton/ReviewSplitButton'
|
||||||
import css from './Changes.module.scss'
|
import css from './Changes.module.scss'
|
||||||
|
|
||||||
const STICKY_TOP_POSITION = 64
|
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 [lineBreaks, setLineBreaks] = useUserPreference(UserPreference.DIFF_LINE_BREAKS, false)
|
||||||
const [diffs, setDiffs] = useState<DiffFileEntry[]>([])
|
const [diffs, setDiffs] = useState<DiffFileEntry[]>([])
|
||||||
const [isSticky, setSticky] = useState(false)
|
const [isSticky, setSticky] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: rawDiff,
|
data: rawDiff,
|
||||||
error,
|
error,
|
||||||
@ -209,11 +210,16 @@ export const Changes: React.FC<ChangesProps> = ({
|
|||||||
</Container>
|
</Container>
|
||||||
<FlexExpander />
|
<FlexExpander />
|
||||||
|
|
||||||
<ReviewDecisionButton
|
<ReviewSplitButton
|
||||||
|
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
|
pullRequestMetadata={pullRequestMetadata}
|
||||||
|
/>
|
||||||
|
{/* <ReviewDecisionButton
|
||||||
repoMetadata={repoMetadata}
|
repoMetadata={repoMetadata}
|
||||||
pullRequestMetadata={pullRequestMetadata}
|
pullRequestMetadata={pullRequestMetadata}
|
||||||
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
|
shouldHide={readOnly || pullRequestMetadata?.state === 'merged'}
|
||||||
/>
|
/> */}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
@ -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
|
@ -29,11 +29,11 @@
|
|||||||
|
|
||||||
> ::before {
|
> ::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 34px;
|
top: 28px;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
content: '';
|
content: '';
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 28px;
|
height: 25px;
|
||||||
border: 1px dashed var(--grey-200);
|
border: 1px dashed var(--grey-200);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
> ::after {
|
> ::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -12px;
|
bottom: -10px;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
content: '';
|
content: '';
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
@ -241,6 +241,7 @@ export interface StringsMap {
|
|||||||
quote: string
|
quote: string
|
||||||
readMe: string
|
readMe: string
|
||||||
refresh: string
|
refresh: string
|
||||||
|
reject: string
|
||||||
rejected: string
|
rejected: string
|
||||||
remove: string
|
remove: string
|
||||||
renameFile: string
|
renameFile: string
|
||||||
|
@ -224,6 +224,7 @@ open: Open
|
|||||||
merged: Merged
|
merged: Merged
|
||||||
enabled: Enabled
|
enabled: Enabled
|
||||||
closed: Closed
|
closed: Closed
|
||||||
|
reject: Reject
|
||||||
rejected: Rejected
|
rejected: Rejected
|
||||||
yours: Yours
|
yours: Yours
|
||||||
all: All
|
all: All
|
||||||
|
@ -538,7 +538,7 @@ const SystemBox: React.FC<SystemBoxProps> = ({ pullRequestMetadata, commentItems
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }} className={css.mergedBox}>
|
<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} />
|
<Icon name={CodeIcon.Merged} size={16} color={Color.PURPLE_700} />
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
@ -63,6 +63,20 @@
|
|||||||
max-width: 320px;
|
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 {
|
.btnWrapper {
|
||||||
|
@ -11,6 +11,7 @@ declare const styles: {
|
|||||||
readonly sub: string
|
readonly sub: string
|
||||||
readonly popover: string
|
readonly popover: string
|
||||||
readonly menuItem: string
|
readonly menuItem: string
|
||||||
|
readonly menuReviewItem: string
|
||||||
readonly btnWrapper: string
|
readonly btnWrapper: string
|
||||||
readonly hasError: string
|
readonly hasError: string
|
||||||
readonly mergeContainer: string
|
readonly mergeContainer: string
|
||||||
|
@ -17,10 +17,17 @@ import { Case, Else, Match, Render, Truthy } from 'react-jsx-match'
|
|||||||
import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
|
import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import ReactTimeago from 'react-timeago'
|
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 { useStrings } from 'framework/strings'
|
||||||
import { CodeIcon, GitInfoProps, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
|
import { CodeIcon, GitInfoProps, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
|
||||||
import { getErrorMessage } from 'utils/Utils'
|
import { getErrorMessage } from 'utils/Utils'
|
||||||
|
import ReviewSplitButton from 'components/Changes/ReviewSplitButton/ReviewSplitButton'
|
||||||
import css from './PullRequestActionsBox.module.scss'
|
import css from './PullRequestActionsBox.module.scss'
|
||||||
|
|
||||||
interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullRequestMetadata'> {
|
||||||
@ -135,8 +142,15 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Case>
|
</Case>
|
||||||
<Case val={PullRequestState.OPEN}>
|
<Case val={PullRequestState.OPEN}>
|
||||||
|
<Layout.Horizontal>
|
||||||
|
<ReviewSplitButton
|
||||||
|
shouldHide={(pullRequestMetadata?.state as EnumPullReqState) === 'merged'}
|
||||||
|
repoMetadata={repoMetadata}
|
||||||
|
pullRequestMetadata={pullRequestMetadata}
|
||||||
|
/>
|
||||||
<Container
|
<Container
|
||||||
inline
|
inline
|
||||||
|
padding={{ left: 'medium' }}
|
||||||
className={cx({
|
className={cx({
|
||||||
[css.btnWrapper]: mergeOption.method !== 'close',
|
[css.btnWrapper]: mergeOption.method !== 'close',
|
||||||
[css.hasError]: mergeable === false
|
[css.hasError]: mergeable === false
|
||||||
@ -216,6 +230,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
|||||||
})}
|
})}
|
||||||
</SplitButton>
|
</SplitButton>
|
||||||
</Container>
|
</Container>
|
||||||
|
</Layout.Horizontal>
|
||||||
</Case>
|
</Case>
|
||||||
</Match>
|
</Match>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -172,7 +172,6 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
|
|||||||
setEditDesc(ACCESS_MODES.EDIT)
|
setEditDesc(ACCESS_MODES.EDIT)
|
||||||
}}
|
}}
|
||||||
{...permissionProps(permEditResult, standalone)}
|
{...permissionProps(permEditResult, standalone)}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@ -191,8 +190,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
|
|||||||
}}
|
}}
|
||||||
variation={ButtonVariation.SECONDARY}
|
variation={ButtonVariation.SECONDARY}
|
||||||
text={getString('delete')}
|
text={getString('delete')}
|
||||||
{...permissionProps(permDeleteResult, standalone)}
|
{...permissionProps(permDeleteResult, standalone)}></Button>
|
||||||
></Button>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Container>
|
</Container>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
|
Loading…
Reference in New Issue
Block a user