mirror of https://github.com/harness/drone.git
feat: [CODE-3324] add default reviewers support (#3572)
* fix: [CODE-3324] resolve comments * fix: [CODE-3324] resolve comments * fix: [CODE-3324] min reviewer req condition * fix: [CODE-3324] approval check * fix: [CODE-3324] latest rule check * fix: [CODE-3324] null check again * fix: [CODE-3324] null check * fix: [CODE-3324] update on latest changes tag * fix: [CODE-3324] pr page fix - only default rule present * fix: [CODE-3324] inital state of default reviewers * fix: [CODE-3324] swagger indentation * feat: [CODE-3324] add default reviewers supportmain
parent
1cfdf10e08
commit
8e925410fb
|
@ -81,6 +81,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.codeCloseBtn {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.targetContainer {
|
||||
:global(.bp3-form-group) {
|
||||
margin-bottom: unset !important;
|
||||
|
@ -187,3 +191,14 @@
|
|||
flex-wrap: wrap;
|
||||
max-width: calc(100% - 100px) !important;
|
||||
}
|
||||
|
||||
.reviewerBlock {
|
||||
background-color: var(--primary-1) !important;
|
||||
padding: 3px 10px !important;
|
||||
gap: 5px !important;
|
||||
}
|
||||
|
||||
.defaultReviewerContainer {
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ export declare const checkboxLabel: string
|
|||
export declare const checkboxText: string
|
||||
export declare const checkContainer: string
|
||||
export declare const codeClose: string
|
||||
export declare const codeCloseBtn: string
|
||||
export declare const defaultReviewerContainer: string
|
||||
export declare const dividerContainer: string
|
||||
export declare const generalContainer: string
|
||||
export declare const greyButton: string
|
||||
|
@ -35,6 +37,7 @@ export declare const minText: string
|
|||
export declare const noData: string
|
||||
export declare const paddingTop: string
|
||||
export declare const popover: string
|
||||
export declare const reviewerBlock: string
|
||||
export declare const row: string
|
||||
export declare const statusWidthContainer: string
|
||||
export declare const table: string
|
||||
|
|
|
@ -35,7 +35,14 @@ import { Menu, PopoverPosition } from '@blueprintjs/core'
|
|||
import { Icon } from '@harnessio/icons'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useGet, useMutate } from 'restful-react'
|
||||
import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils'
|
||||
import {
|
||||
BranchTargetType,
|
||||
MergeStrategy,
|
||||
PrincipalUserType,
|
||||
SettingTypeMode,
|
||||
SettingsTab,
|
||||
branchTargetOptions
|
||||
} from 'utils/GitUtils'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import {
|
||||
LabelsPageScope,
|
||||
|
@ -136,10 +143,19 @@ const BranchProtectionForm = (props: {
|
|||
}
|
||||
})
|
||||
}
|
||||
const transformUserArray = transformDataToArray(rule?.users || [])
|
||||
const usersArrayCurr = transformUserArray?.map(user => `${user.id} ${user.display_name}`)
|
||||
|
||||
const usersMap = rule?.users
|
||||
|
||||
const bypassListUsers = rule?.definition?.bypass?.user_ids?.map(id => usersMap?.[id])
|
||||
const transformBypassListArray = transformDataToArray(bypassListUsers || [])
|
||||
const usersArrayCurr = transformBypassListArray?.map(user => `${user.id} ${user.display_name}`)
|
||||
const [userArrayState, setUserArrayState] = useState<string[]>(usersArrayCurr)
|
||||
|
||||
const defaultReviewersUsers = rule?.definition?.pullreq?.reviewers?.default_reviewer_ids?.map(id => usersMap?.[id])
|
||||
const transformDefaultReviewersArray = transformDataToArray(defaultReviewersUsers || [])
|
||||
const reviewerArrayCurr = transformDefaultReviewersArray?.map(user => `${user.id} ${user.display_name}`)
|
||||
const [defaultReviewersState, setDefaultReviewersState] = useState<string[]>(reviewerArrayCurr)
|
||||
|
||||
const getUpdateChecksPath = () =>
|
||||
currentRule?.scope === 0 && repoMetadata
|
||||
? `/repos/${repoMetadata?.path}/+/checks/recent`
|
||||
|
@ -173,6 +189,20 @@ const BranchProtectionForm = (props: {
|
|||
[principals]
|
||||
)
|
||||
|
||||
const userPrincipalOptions: SelectOption[] = useMemo(
|
||||
() =>
|
||||
principals?.reduce<SelectOption[]>((acc, principal) => {
|
||||
if (principal?.type === PrincipalUserType.USER) {
|
||||
acc.push({
|
||||
value: `${principal.id?.toString() as string} ${principal.uid}`,
|
||||
label: `${principal?.display_name} (${principal.email})`
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, []) || [],
|
||||
[principals]
|
||||
)
|
||||
|
||||
const handleSubmit = async (operation: Promise<OpenapiRule>, successMessage: string, resetForm: () => void) => {
|
||||
try {
|
||||
await operation
|
||||
|
@ -204,7 +234,10 @@ const BranchProtectionForm = (props: {
|
|||
const initialValues = useMemo((): RulesFormPayload => {
|
||||
if (editMode && rule) {
|
||||
const minReviewerCheck =
|
||||
((rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count as number) > 0 ? true : false
|
||||
((rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count as number) > 0
|
||||
const minDefaultReviewerCheck =
|
||||
((rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_default_reviewer_count as number) >
|
||||
0
|
||||
const isMergePresent = (rule.definition as ProtectionBranch)?.pullreq?.merge?.strategies_allowed?.includes(
|
||||
MergeStrategy.MERGE
|
||||
)
|
||||
|
@ -224,12 +257,19 @@ const BranchProtectionForm = (props: {
|
|||
const includeArr = includeList?.map((arr: string) => ['include', arr])
|
||||
const excludeArr = excludeList?.map((arr: string) => ['exclude', arr])
|
||||
const finalArray = [...includeArr, ...excludeArr]
|
||||
const usersArray = transformDataToArray(rule.users)
|
||||
const usersArray = transformDataToArray(bypassListUsers || [])
|
||||
|
||||
const bypassList =
|
||||
userArrayState.length > 0
|
||||
? userArrayState
|
||||
: usersArray?.map(user => `${user.id} ${user.display_name} (${user.email})`)
|
||||
|
||||
const reviewersArray = transformDataToArray(defaultReviewersUsers || [])
|
||||
const defaultReviewersList =
|
||||
defaultReviewersState.length > 0
|
||||
? defaultReviewersState
|
||||
: reviewersArray?.map(user => `${user.id} ${user.display_name} (${user.email})`)
|
||||
|
||||
return {
|
||||
name: rule?.identifier,
|
||||
desc: rule.description,
|
||||
|
@ -239,10 +279,16 @@ const BranchProtectionForm = (props: {
|
|||
targetList: finalArray,
|
||||
allRepoOwners: (rule.definition as ProtectionBranch)?.bypass?.repo_owners,
|
||||
bypassList: bypassList,
|
||||
defaultReviewersEnabled: (rule.definition as any)?.pullreq?.reviewers?.default_reviewer_ids?.length > 0,
|
||||
defaultReviewersList: defaultReviewersList,
|
||||
requireMinReviewers: minReviewerCheck,
|
||||
requireMinDefaultReviewers: minDefaultReviewerCheck,
|
||||
minReviewers: minReviewerCheck
|
||||
? (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count
|
||||
: '',
|
||||
minDefaultReviewers: minDefaultReviewerCheck
|
||||
? (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_default_reviewer_count
|
||||
: '',
|
||||
autoAddCodeOwner: (rule.definition as ProtectionBranch)?.pullreq?.reviewers?.request_code_owners,
|
||||
requireCodeOwner: (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_code_owners,
|
||||
requireNewChanges: (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_latest_commit,
|
||||
|
@ -269,7 +315,8 @@ const BranchProtectionForm = (props: {
|
|||
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden &&
|
||||
!(rule.definition as ProtectionBranch)?.pullreq?.merge?.block,
|
||||
targetSet: false,
|
||||
bypassSet: false
|
||||
bypassSet: false,
|
||||
defaultReviewersSet: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,6 +327,14 @@ const BranchProtectionForm = (props: {
|
|||
getEditPermissionRequestFromScope(space, currentRule?.scope ?? 0, repoMetadata),
|
||||
[space, currentRule?.scope, repoMetadata]
|
||||
)
|
||||
|
||||
const defaultReviewerProps = {
|
||||
setSearchTerm,
|
||||
userPrincipalOptions,
|
||||
settingSectionMode,
|
||||
setDefaultReviewersState
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik<RulesFormPayload>
|
||||
formName="branchProtectionRulesNewEditForm"
|
||||
|
@ -287,7 +342,34 @@ const BranchProtectionForm = (props: {
|
|||
enableReinitialize
|
||||
validationSchema={yup.object().shape({
|
||||
name: yup.string().trim().required().matches(REGEX_VALID_REPO_NAME, getString('validation.nameLogic')),
|
||||
minReviewers: yup.number().typeError(getString('enterANumber'))
|
||||
minReviewers: yup.number().typeError(getString('enterANumber')),
|
||||
minDefaultReviewers: yup.number().typeError(getString('enterANumber')),
|
||||
defaultReviewersList: yup
|
||||
.array()
|
||||
.of(yup.string())
|
||||
.test(
|
||||
'min-reviewers', // Name of the test
|
||||
getString('branchProtection.atLeastMinReviewer', { count: 1 }),
|
||||
function (defaultReviewersList) {
|
||||
const { minDefaultReviewers, requireMinDefaultReviewers, defaultReviewersEnabled } = this.parent
|
||||
const minReviewers = Number(minDefaultReviewers) || 0
|
||||
if (defaultReviewersEnabled && requireMinDefaultReviewers) {
|
||||
const isValid = defaultReviewersList && defaultReviewersList.length >= minReviewers
|
||||
|
||||
return (
|
||||
isValid ||
|
||||
this.createError({
|
||||
message:
|
||||
minReviewers > 1
|
||||
? getString('branchProtection.atLeastMinReviewers', { count: minReviewers })
|
||||
: getString('branchProtection.atLeastMinReviewer', { count: minReviewers })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
})}
|
||||
onSubmit={async (formData, { resetForm }) => {
|
||||
const stratArray = [
|
||||
|
@ -302,6 +384,7 @@ const BranchProtectionForm = (props: {
|
|||
formData?.targetList?.filter(([type]) => type === 'exclude').map(([, value]) => value) ?? []
|
||||
|
||||
const bypassList = formData?.bypassList?.map(item => parseInt(item.split(' ')[0]))
|
||||
const defaultReviewersList = formData?.defaultReviewersList?.map(item => parseInt(item.split(' ')[0]))
|
||||
const payload: OpenapiRule = {
|
||||
identifier: formData.name,
|
||||
type: 'branch',
|
||||
|
@ -321,11 +404,13 @@ const BranchProtectionForm = (props: {
|
|||
approvals: {
|
||||
require_code_owners: formData.requireCodeOwner,
|
||||
require_minimum_count: parseInt(formData.minReviewers as string),
|
||||
require_minimum_default_reviewer_count: parseInt(formData.minDefaultReviewers as string),
|
||||
require_latest_commit: formData.requireNewChanges,
|
||||
require_no_change_request: formData.reqResOfChanges
|
||||
},
|
||||
reviewers: {
|
||||
request_code_owners: formData.autoAddCodeOwner
|
||||
request_code_owners: formData.autoAddCodeOwner,
|
||||
default_reviewer_ids: defaultReviewersList
|
||||
},
|
||||
comments: {
|
||||
require_resolve_all: formData.requireCommentResolution
|
||||
|
@ -356,6 +441,9 @@ const BranchProtectionForm = (props: {
|
|||
if (!formData.requireMinReviewers) {
|
||||
delete (payload?.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count
|
||||
}
|
||||
if (!formData.requireMinDefaultReviewers) {
|
||||
delete (payload?.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_default_reviewer_count
|
||||
}
|
||||
if (editMode) {
|
||||
handleSubmit(updateRule(payload), getString('branchProtection.ruleUpdated'), resetForm)
|
||||
} else {
|
||||
|
@ -549,6 +637,7 @@ const BranchProtectionForm = (props: {
|
|||
statusChecks={statusChecks}
|
||||
limitMergeStrats={limitMergeStrats}
|
||||
setSearchStatusTerm={setSearchStatusTerm}
|
||||
defaultReviewerProps={defaultReviewerProps}
|
||||
/>
|
||||
<Container padding={{ top: 'large' }}>
|
||||
<Layout.Horizontal spacing="small">
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Container, FlexExpander, Layout, Text } from '@harnessio/uicore'
|
||||
import { Classes, Popover, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import css from '../BranchProtectionForm.module.scss'
|
||||
|
||||
const DefaultReviewersList = (props: {
|
||||
defaultReviewersList?: string[] // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void
|
||||
}) => {
|
||||
const { defaultReviewersList, setFieldValue } = props
|
||||
const defaultReviewerContent = useMemo(() => {
|
||||
return (
|
||||
<Layout.Horizontal className={cx(css.widthContainer, css.defaultReviewerContainer)} padding={{ bottom: 'large' }}>
|
||||
{defaultReviewersList?.map((owner: string, idx: number) => {
|
||||
const str = owner.slice(owner.indexOf(' ') + 1)
|
||||
const name = str.split(' (')[0]
|
||||
const email = str.split(' (')[1].replace(')', '')
|
||||
return (
|
||||
<Popover
|
||||
key={`${name}-${idx}`}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
position={PopoverPosition.TOP_LEFT}
|
||||
popoverClassName={Classes.DARK}
|
||||
content={
|
||||
<Container padding="medium">
|
||||
<Text font={{ variation: FontVariation.FORM_HELP }} color={Color.WHITE}>
|
||||
{email}
|
||||
</Text>
|
||||
</Container>
|
||||
}>
|
||||
<Layout.Horizontal key={`${name}-${idx}`} flex={{ align: 'center-center' }} className={css.reviewerBlock}>
|
||||
<Text padding={{ top: 'tiny' }} lineClamp={1}>
|
||||
{name}
|
||||
</Text>
|
||||
<FlexExpander />
|
||||
<Icon
|
||||
name="code-close"
|
||||
onClick={() => {
|
||||
const filteredData = defaultReviewersList.filter(item => !(item === owner))
|
||||
setFieldValue('defaultReviewersList', filteredData)
|
||||
}}
|
||||
className={css.codeCloseBtn}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
</Popover>
|
||||
)
|
||||
})}
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}, [defaultReviewersList, setFieldValue])
|
||||
return <>{defaultReviewerContent}</>
|
||||
}
|
||||
|
||||
export default DefaultReviewersList
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright 2023 Harness, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react'
|
||||
import cx from 'classnames'
|
||||
import { Container, FormInput, SelectOption, Text } from '@harnessio/uicore'
|
||||
import { Color } from '@harnessio/design-system'
|
||||
import type { FormikProps } from 'formik'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { RulesFormPayload } from 'utils/Utils'
|
||||
import { SettingTypeMode } from 'utils/GitUtils'
|
||||
import DefaultReviewersList from './DefaultReviewersList'
|
||||
import css from '../BranchProtectionForm.module.scss'
|
||||
|
||||
const DefaultReviewersSection = (props: {
|
||||
formik: FormikProps<RulesFormPayload>
|
||||
defaultReviewerProps: {
|
||||
setSearchTerm: React.Dispatch<React.SetStateAction<string>>
|
||||
userPrincipalOptions: SelectOption[]
|
||||
settingSectionMode: SettingTypeMode
|
||||
setDefaultReviewersState: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}
|
||||
}) => {
|
||||
const { formik, defaultReviewerProps } = props
|
||||
const { settingSectionMode, userPrincipalOptions, setSearchTerm, setDefaultReviewersState } = defaultReviewerProps
|
||||
const { getString } = useStrings()
|
||||
const setFieldValue = formik.setFieldValue
|
||||
|
||||
const defaultReviewersList =
|
||||
settingSectionMode === SettingTypeMode.EDIT || formik.values.defaultReviewersSet
|
||||
? formik.values.defaultReviewersList
|
||||
: []
|
||||
const minDefaultReviewers = formik.values.requireMinDefaultReviewers
|
||||
const defaultReviewersEnabled = formik.values.defaultReviewersEnabled
|
||||
const filteredPrincipalOptions = userPrincipalOptions.filter(
|
||||
(item: SelectOption) => !defaultReviewersList?.includes(item.value as string)
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormInput.CheckBox
|
||||
className={css.checkboxLabel}
|
||||
label={getString('branchProtection.enableDefaultReviewersTitle')}
|
||||
name={'defaultReviewersEnabled'}
|
||||
onChange={e => {
|
||||
if (!(e.target as HTMLInputElement).checked) {
|
||||
setFieldValue('requireMinDefaultReviewers', false)
|
||||
formik.setFieldValue('defaultReviewersList', [])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
|
||||
{getString('branchProtection.enableDefaultReviewersText')}
|
||||
</Text>
|
||||
|
||||
<Render when={defaultReviewersEnabled}>
|
||||
<Container padding={{ top: 'xlarge', left: 'xlarge' }}>
|
||||
<FormInput.Select
|
||||
items={filteredPrincipalOptions}
|
||||
onQueryChange={setSearchTerm}
|
||||
className={css.widthContainer}
|
||||
value={{ label: '', value: '' }}
|
||||
placeholder={getString('selectReviewers')}
|
||||
onChange={item => {
|
||||
const id = item.value?.toString().split(' ')[0]
|
||||
const displayName = item.label
|
||||
const defaultReviewerEntry = `${id} ${displayName}`
|
||||
defaultReviewersList?.push(defaultReviewerEntry)
|
||||
const uniqueArr = Array.from(new Set(defaultReviewersList))
|
||||
formik.setFieldValue('defaultReviewersList', uniqueArr)
|
||||
formik.setFieldValue('defaultReviewersSet', true)
|
||||
setDefaultReviewersState([...uniqueArr])
|
||||
}}
|
||||
name={'defaultReviewerSelect'}></FormInput.Select>
|
||||
{formik.errors.defaultReviewersList && (
|
||||
<Text color={Color.RED_350} padding={{ bottom: 'medium' }}>
|
||||
{formik.errors.defaultReviewersList}
|
||||
</Text>
|
||||
)}
|
||||
<DefaultReviewersList defaultReviewersList={defaultReviewersList} setFieldValue={formik.setFieldValue} />
|
||||
|
||||
<FormInput.CheckBox
|
||||
className={css.checkboxLabel}
|
||||
label={getString('branchProtection.requireMinDefaultReviewersTitle')}
|
||||
name={'requireMinDefaultReviewers'}
|
||||
onChange={e => {
|
||||
if ((e.target as HTMLInputElement).checked) {
|
||||
setFieldValue('minDefaultReviewers', 1)
|
||||
setFieldValue('defaultReviewersEnabled', true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
|
||||
{getString('branchProtection.requireMinDefaultReviewersContent')}
|
||||
</Text>
|
||||
{minDefaultReviewers && (
|
||||
<Container padding={{ left: 'xlarge', top: 'medium' }}>
|
||||
<FormInput.Text
|
||||
className={cx(css.widthContainer, css.minText)}
|
||||
name={'minDefaultReviewers'}
|
||||
placeholder={getString('branchProtection.minNumberPlaceholder')}
|
||||
label={getString('branchProtection.minNumber')}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
</Render>
|
||||
<hr className={css.dividerContainer} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultReviewersSection
|
|
@ -22,6 +22,8 @@ import type { FormikProps } from 'formik'
|
|||
import { Classes, Popover, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { RulesFormPayload } from 'utils/Utils'
|
||||
import type { SettingTypeMode } from 'utils/GitUtils'
|
||||
import DefaultReviewersSection from './DefaultReviewersSection'
|
||||
import css from '../BranchProtectionForm.module.scss'
|
||||
|
||||
const ProtectionRulesForm = (props: {
|
||||
|
@ -29,9 +31,15 @@ const ProtectionRulesForm = (props: {
|
|||
minReviewers: boolean
|
||||
statusOptions: SelectOption[]
|
||||
statusChecks: string[]
|
||||
limitMergeStrats: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
limitMergeStrats: boolean
|
||||
setSearchStatusTerm: React.Dispatch<React.SetStateAction<string>>
|
||||
formik: FormikProps<RulesFormPayload>
|
||||
defaultReviewerProps: {
|
||||
setSearchTerm: React.Dispatch<React.SetStateAction<string>>
|
||||
userPrincipalOptions: SelectOption[]
|
||||
settingSectionMode: SettingTypeMode
|
||||
setDefaultReviewersState: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}
|
||||
}) => {
|
||||
const {
|
||||
statusChecks,
|
||||
|
@ -40,7 +48,8 @@ const ProtectionRulesForm = (props: {
|
|||
requireStatusChecks,
|
||||
statusOptions,
|
||||
limitMergeStrats,
|
||||
formik
|
||||
formik,
|
||||
defaultReviewerProps
|
||||
} = props
|
||||
const { getString } = useStrings()
|
||||
const setFieldValue = formik.setFieldValue
|
||||
|
@ -143,6 +152,7 @@ const ProtectionRulesForm = (props: {
|
|||
</Popover>
|
||||
|
||||
<hr className={css.dividerContainer} />
|
||||
<DefaultReviewersSection formik={formik} defaultReviewerProps={defaultReviewerProps} />
|
||||
<FormInput.CheckBox
|
||||
className={css.checkboxLabel}
|
||||
label={getString('branchProtection.requireMinReviewersTitle')}
|
||||
|
|
|
@ -219,6 +219,18 @@ const BranchProtectionListing = (props: {
|
|||
requiredRule: {
|
||||
[RuleFields.AUTO_ADD_CODE_OWNERS]: true
|
||||
}
|
||||
},
|
||||
requireMinDefaultReviewersTitle: {
|
||||
title: getString('branchProtection.requireMinDefaultReviewersTitle'),
|
||||
requiredRule: {
|
||||
[RuleFields.APPROVALS_REQUIRE_MINIMUM_DEFAULT_REVIEWERS]: true
|
||||
}
|
||||
},
|
||||
defaultReviewersAdded: {
|
||||
title: getString('branchProtection.enableDefaultReviewersTitle'),
|
||||
requiredRule: {
|
||||
[RuleFields.DEFAULT_REVIEWERS_ADDED]: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ export interface StringsMap {
|
|||
'branchProtection.addCodeownersToReviewText': string
|
||||
'branchProtection.addCodeownersToReviewTitle': string
|
||||
'branchProtection.allRepoOwners': string
|
||||
'branchProtection.atLeastMinReviewer': string
|
||||
'branchProtection.atLeastMinReviewers': string
|
||||
'branchProtection.autoDeleteText': string
|
||||
'branchProtection.autoDeleteTitle': string
|
||||
'branchProtection.blockBranchCreation': string
|
||||
|
@ -102,6 +104,8 @@ export interface StringsMap {
|
|||
'branchProtection.disableTheRule': string
|
||||
'branchProtection.edit': string
|
||||
'branchProtection.editRule': string
|
||||
'branchProtection.enableDefaultReviewersText': string
|
||||
'branchProtection.enableDefaultReviewersTitle': string
|
||||
'branchProtection.enableTheRule': string
|
||||
'branchProtection.limitMergeStrategies': string
|
||||
'branchProtection.limitMergeStrategiesText': string
|
||||
|
@ -124,6 +128,8 @@ export interface StringsMap {
|
|||
'branchProtection.reqReviewFromCodeOwnerTitle': string
|
||||
'branchProtection.reqStatusChecksText': string
|
||||
'branchProtection.reqStatusChecksTitle': string
|
||||
'branchProtection.requireMinDefaultReviewersContent': string
|
||||
'branchProtection.requireMinDefaultReviewersTitle': string
|
||||
'branchProtection.requireMinReviewersContent': string
|
||||
'branchProtection.requireMinReviewersTitle': string
|
||||
'branchProtection.requirePr': string
|
||||
|
@ -170,24 +176,31 @@ export interface StringsMap {
|
|||
'changesSection.changesApproved': string
|
||||
'changesSection.changesApprovedByXReviewers': string
|
||||
'changesSection.changesWereAppByCodeOwner': string
|
||||
'changesSection.changesWereAppByDefaultReviewers': string
|
||||
'changesSection.changesWereAppByLatestReqRev': string
|
||||
'changesSection.codeOwnerReqChanges': string
|
||||
'changesSection.codeOwnerReqChangesToPr': string
|
||||
'changesSection.defaultReviewersChangesToPr': string
|
||||
'changesSection.defaultReviewersStatus': string
|
||||
'changesSection.latestChangesApprovedByXReviewers': string
|
||||
'changesSection.latestChangesPendingReqRev': string
|
||||
'changesSection.latestChangesWereAppByCodeOwner': string
|
||||
'changesSection.latestChangesWereAppByDefaultReviewers': string
|
||||
'changesSection.latestChangesWereApprovedByReq': string
|
||||
'changesSection.noCodeOwnerReviewsReq': string
|
||||
'changesSection.noReviewsReq': string
|
||||
'changesSection.pendingAppFromCodeOwners': string
|
||||
'changesSection.pendingLatestApprovalCodeOwners': string
|
||||
'changesSection.pendingLatestApprovalDefaultReviewers': string
|
||||
'changesSection.prMergeBlockedMessage': string
|
||||
'changesSection.prMergeBlockedTitle': string
|
||||
'changesSection.pullReqWithoutAnyReviews': string
|
||||
'changesSection.reqChangeFromCodeOwners': string
|
||||
'changesSection.someChangesWereAppByCodeOwner': string
|
||||
'changesSection.waitingOnCodeOwner': string
|
||||
'changesSection.waitingOnDefaultReviewers': string
|
||||
'changesSection.waitingOnLatestCodeOwner': string
|
||||
'changesSection.waitingOnLatestDefaultReviewers': string
|
||||
'changesSection.waitingOnReviewers': string
|
||||
'changesSection.xApprovalsArePending': string
|
||||
characterLimit: string
|
||||
|
@ -309,6 +322,7 @@ export interface StringsMap {
|
|||
dangerDeleteProject: string
|
||||
defaultBranch: string
|
||||
defaultBranchTitle: string
|
||||
defaultReviewers: string
|
||||
delete: string
|
||||
deleteBranch: string
|
||||
deleteBranchConfirm: string
|
||||
|
@ -673,6 +687,7 @@ export interface StringsMap {
|
|||
ok: string
|
||||
on: string
|
||||
onDate: string
|
||||
onLatestChanges: string
|
||||
oneMustBeSelected: string
|
||||
open: string
|
||||
optional: string
|
||||
|
@ -1036,6 +1051,7 @@ export interface StringsMap {
|
|||
selectMergeStrat: string
|
||||
selectRange: string
|
||||
selectRepositoryPlaceholder: string
|
||||
selectReviewers: string
|
||||
selectSpace: string
|
||||
selectSpaceText: string
|
||||
selectStatuses: string
|
||||
|
|
|
@ -1079,6 +1079,12 @@ branchProtection:
|
|||
commitDirectlyBlockText: Some rules don't allow you to commit directly
|
||||
commitDirectlyAlertText: Some rules will be bypassed to commit directly
|
||||
commitDirectlyAlertBtn: Bypass rules and commit directly
|
||||
enableDefaultReviewersTitle: Enable default reviewers
|
||||
enableDefaultReviewersText: Require approval from default reviewer for each pull request
|
||||
requireMinDefaultReviewersTitle: Require a minimum number of default reviewers
|
||||
requireMinDefaultReviewersContent: Require approval on pull requests from a minimum number of default reviewers
|
||||
atLeastMinReviewers: 'Select at least {{count}} default reviewers'
|
||||
atLeastMinReviewer: 'Select at least {{count}} default reviewer'
|
||||
codeOwner:
|
||||
title: Code Owner
|
||||
changesRequested: '{count} {count|1:change,changes} requested'
|
||||
|
@ -1089,6 +1095,7 @@ approved: Approved
|
|||
comingSoon: Coming soon...
|
||||
enterANumber: Enter a number
|
||||
selectUsers: Select Users
|
||||
selectReviewers: Select Reviewers
|
||||
selectUsersAndServiceAcc: Select Users and Service Accounts
|
||||
selectStatuses: Select Statuses
|
||||
featureRoadmap: Feature Roadmap
|
||||
|
@ -1150,8 +1157,11 @@ changesSection:
|
|||
pendingAppFromCodeOwners: Approvals pending from code owners
|
||||
pendingLatestApprovalCodeOwners: Latest changes are pending approval from code owners
|
||||
waitingOnCodeOwner: Changes are pending approval from code owners
|
||||
pendingLatestApprovalDefaultReviewers: Latest changes are pending approval from default reviewers
|
||||
waitingOnDefaultReviewers: Changes are pending approval from default reviewers
|
||||
waitingOnReviewers: Changes are pending approval from required reviewers
|
||||
waitingOnLatestCodeOwner: Waiting on code owner reviews of latest changes
|
||||
waitingOnLatestDefaultReviewers: Waiting on default reviewer's reviews of latest changes
|
||||
approvalPending: Approvals pending
|
||||
changesApproved: Changes approved
|
||||
noReviewsReq: No reviews required
|
||||
|
@ -1161,13 +1171,17 @@ changesSection:
|
|||
changesApprovedByXReviewers: Changes were approved by {length} {length|1:reviewer,reviewers}
|
||||
latestChangesApprovedByXReviewers: Latest changes were approved by {length} {length|1:reviewer,reviewers}
|
||||
latestChangesWereAppByCodeOwner: Latest changes were approved by code owners
|
||||
latestChangesWereAppByDefaultReviewers: Latest changes were approved by default reviewers
|
||||
latestChangesPendingReqRev: Latest changes are pending approval from required reviewers
|
||||
changesWereAppByCodeOwner: Changes were approved by code owners
|
||||
changesWereAppByDefaultReviewers: Changes were approved by default reviewers
|
||||
defaultReviewersStatus: Default reviewers were added to the PR
|
||||
changesWereAppByLatestReqRev: Changes were approved by required reviewers
|
||||
latestChangesWereApprovedByReq: Latest changes were approved by required reviewers
|
||||
someChangesWereAppByCodeOwner: Some changes were approved by code owners
|
||||
xApprovalsArePending: '{approved}/{min} approvals are pending'
|
||||
codeOwnerReqChangesToPr: Code owners requested changes to the pull request
|
||||
defaultReviewersChangesToPr: Default reviewers requested changes to the pull request
|
||||
checkSection:
|
||||
someReqChecksFailed: Some required checks have failed
|
||||
someReqChecksPending: Some required checks are pending
|
||||
|
@ -1203,6 +1217,7 @@ checklist: Check list
|
|||
code: Code
|
||||
approvedBy: Approved By
|
||||
ownersHeading: OWNERS
|
||||
defaultReviewers: DEFAULT REVIEWERS
|
||||
changesRequestedBy: Changes Requested By
|
||||
changesRequested: changesRequested
|
||||
owners: Owners
|
||||
|
@ -1322,3 +1337,4 @@ rebase: Rebase
|
|||
rebaseBranch: Rebase branch
|
||||
updateWithRebase: Update with rebase
|
||||
updatedBranchMessageRebase: Updated branch with rebase
|
||||
onLatestChanges: on latest changes
|
||||
|
|
|
@ -104,7 +104,7 @@ export function CodeOwnersOverview({
|
|||
return codeOwners?.evaluation_entries?.length ? (
|
||||
<Container
|
||||
className={cx(css.main, { [css.codeOwner]: !standalone })}
|
||||
margin={{ top: 'medium', bottom: pullReqMetadata.description ? undefined : 'large' }}
|
||||
margin={{ top: 'medium', bottom: pullReqMetadata?.description ? undefined : 'large' }}
|
||||
style={{ '--border-color': Utils.getRealCSSColor(borderColor) } as React.CSSProperties}>
|
||||
<Match expr={isExpanded}>
|
||||
<Truthy>
|
||||
|
@ -165,7 +165,7 @@ export const CodeOwnerSection: React.FC<CodeOwnerSectionsProps> = ({
|
|||
[
|
||||
{
|
||||
id: 'CODE',
|
||||
width: '45%',
|
||||
width: '40%',
|
||||
sort: true,
|
||||
Header: getString('code'),
|
||||
accessor: 'CODE',
|
||||
|
@ -183,7 +183,7 @@ export const CodeOwnerSection: React.FC<CodeOwnerSectionsProps> = ({
|
|||
},
|
||||
{
|
||||
id: 'Owners',
|
||||
width: '13%',
|
||||
width: '18%',
|
||||
sort: true,
|
||||
Header: getString('ownersHeading'),
|
||||
accessor: 'OWNERS',
|
||||
|
|
|
@ -27,7 +27,8 @@ import type {
|
|||
TypesPullReqReviewer,
|
||||
RepoRepositoryOutput,
|
||||
TypesRuleViolations,
|
||||
TypesBranchExtended
|
||||
TypesBranchExtended,
|
||||
TypesDefaultReviewerApprovalsResponse
|
||||
} from 'services/code'
|
||||
import {
|
||||
PRMergeOption,
|
||||
|
@ -103,6 +104,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
|
|||
const [reqCodeOwnerApproval, setReqCodeOwnerApproval] = useState(false)
|
||||
const [minApproval, setMinApproval] = useState(0)
|
||||
const [reqCodeOwnerLatestApproval, setReqCodeOwnerLatestApproval] = useState(false)
|
||||
const [defaultReviewersInfoSet, setDefaultReviewersInfoSet] = useState<TypesDefaultReviewerApprovalsResponse[]>([])
|
||||
const [minReqLatestApproval, setMinReqLatestApproval] = useState(0)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [resolvedCommentArr, setResolvedCommentArr] = useState<any>()
|
||||
|
@ -234,7 +236,8 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
|
|||
setMinApproval,
|
||||
setReqCodeOwnerLatestApproval,
|
||||
setMinReqLatestApproval,
|
||||
setPRStateLoading
|
||||
setPRStateLoading,
|
||||
setDefaultReviewersInfoSet
|
||||
) // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unchecked, pullReqMetadata?.source_sha, activities])
|
||||
|
||||
|
@ -295,6 +298,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
|
|||
reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval}
|
||||
refetchCodeOwners={refetchCodeOwners}
|
||||
mergeBlockedRule={mergeBlockedRule}
|
||||
defaultReviewersInfoSet={defaultReviewersInfoSet}
|
||||
/>
|
||||
</Render>
|
||||
),
|
||||
|
|
|
@ -30,7 +30,7 @@ import { Render } from 'react-jsx-match'
|
|||
import { isEmpty } from 'lodash-es'
|
||||
import type { IconName } from '@blueprintjs/core'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { CodeOwnerReqDecision, findChangeReqDecisions } from 'utils/Utils'
|
||||
import { CodeOwnerReqDecision, findChangeReqDecisions, getUnifiedDefaultReviewersState } from 'utils/Utils'
|
||||
import { CodeOwnerSection } from 'pages/PullRequest/CodeOwners/CodeOwnersOverview'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type {
|
||||
|
@ -38,10 +38,12 @@ import type {
|
|||
TypesCodeOwnerEvaluationEntry,
|
||||
TypesPullReq,
|
||||
TypesPullReqReviewer,
|
||||
RepoRepositoryOutput
|
||||
RepoRepositoryOutput,
|
||||
TypesDefaultReviewerApprovalsResponse
|
||||
} from 'services/code'
|
||||
import { capitalizeFirstLetter } from 'pages/PullRequest/Checks/ChecksUtils'
|
||||
import { findWaitingDecisions } from 'pages/PullRequest/PullRequestUtils'
|
||||
import { defaultReviewerResponseWithDecision, findWaitingDecisions } from 'pages/PullRequest/PullRequestUtils'
|
||||
import { DefaultReviewersPanel } from 'pages/PullRequest/DefaultReviewers/DefaultReviewersPanel'
|
||||
import greyCircle from '../../../../../icons/greyCircle.svg?url'
|
||||
import emptyStatus from '../../../../../icons/emptyStatus.svg?url'
|
||||
import Success from '../../../../../icons/code-success.svg?url'
|
||||
|
@ -58,6 +60,7 @@ interface ChangesSectionProps {
|
|||
reqCodeOwnerApproval: boolean
|
||||
minApproval: number
|
||||
reviewers: TypesPullReqReviewer[] | null
|
||||
defaultReviewersInfoSet: TypesDefaultReviewerApprovalsResponse[]
|
||||
minReqLatestApproval: number
|
||||
reqCodeOwnerLatestApproval: boolean
|
||||
mergeBlockedRule: boolean
|
||||
|
@ -69,6 +72,7 @@ interface ChangesSectionProps {
|
|||
const ChangesSection = (props: ChangesSectionProps) => {
|
||||
const {
|
||||
reviewers: currReviewers,
|
||||
defaultReviewersInfoSet,
|
||||
minApproval,
|
||||
reqCodeOwnerApproval,
|
||||
repoMetadata,
|
||||
|
@ -139,6 +143,18 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
changeReqEvaluations[0].reviewer?.display_name || changeReqEvaluations[0].reviewer?.uid || ''
|
||||
)
|
||||
: 'Reviewer'
|
||||
const updatedDefaultApprovalRes = reviewers
|
||||
? defaultReviewerResponseWithDecision(defaultReviewersInfoSet, reviewers)
|
||||
: defaultReviewersInfoSet
|
||||
|
||||
const {
|
||||
defReviewerApprovalRequiredByRule,
|
||||
defReviewerLatestApprovalRequiredByRule,
|
||||
defReviewerApprovedLatestChanges,
|
||||
defReviewerApprovedChanges,
|
||||
changesRequestedByDefReviewersArr
|
||||
} = getUnifiedDefaultReviewersState(updatedDefaultApprovalRes)
|
||||
|
||||
const extractInfoForCodeOwnerContent = () => {
|
||||
let statusMessage = ''
|
||||
let statusColor = 'grey' // Default color for no rules required
|
||||
|
@ -151,6 +167,8 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
minApproval > 0 ||
|
||||
reqCodeOwnerLatestApproval ||
|
||||
minReqLatestApproval > 0 ||
|
||||
defReviewerApprovalRequiredByRule ||
|
||||
defReviewerLatestApprovalRequiredByRule ||
|
||||
mergeBlockedRule
|
||||
) {
|
||||
if (mergeBlockedRule) {
|
||||
|
@ -183,6 +201,14 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
statusMessage = getString('changesSection.latestChangesPendingReqRev')
|
||||
statusColor = Color.ORANGE_500
|
||||
statusIcon = 'execution-waiting'
|
||||
} else if (defReviewerLatestApprovalRequiredByRule && !defReviewerApprovedLatestChanges) {
|
||||
title = getString('changesSection.approvalPending')
|
||||
statusMessage = stringSubstitute(getString('changesSection.pendingLatestApprovalDefaultReviewers'), {
|
||||
count: approvedEvaluations?.length || '0',
|
||||
total: minApproval
|
||||
}) as string
|
||||
statusColor = Color.ORANGE_500
|
||||
statusIcon = 'execution-waiting'
|
||||
} else if (approvedEvaluations && approvedEvaluations?.length < minApproval && minApproval > 0) {
|
||||
title = getString('changesSection.approvalPending')
|
||||
statusMessage = stringSubstitute(getString('changesSection.waitingOnReviewers'), {
|
||||
|
@ -190,6 +216,15 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
total: minApproval
|
||||
}) as string
|
||||
|
||||
statusColor = Color.ORANGE_500
|
||||
statusIcon = 'execution-waiting'
|
||||
} else if (defReviewerApprovalRequiredByRule && !defReviewerApprovedChanges) {
|
||||
title = getString('changesSection.approvalPending')
|
||||
statusMessage = stringSubstitute(getString('changesSection.waitingOnDefaultReviewers'), {
|
||||
count: approvedEvaluations?.length || '0',
|
||||
total: minApproval
|
||||
}) as string
|
||||
|
||||
statusColor = Color.ORANGE_500
|
||||
statusIcon = 'execution-waiting'
|
||||
} else if (reqCodeOwnerLatestApproval && latestCodeOwnerApprovalArr?.length > 0) {
|
||||
|
@ -384,6 +419,84 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
|
||||
const renderDefaultReviewersStatus = () => {
|
||||
if (defReviewerLatestApprovalRequiredByRule && !defReviewerApprovedLatestChanges) {
|
||||
return (
|
||||
// Waiting on default reviewers reviews of latest changes
|
||||
<Layout.Horizontal>
|
||||
<Container padding={{ left: 'large' }}>
|
||||
<img alt="emptyStatus" width={16} height={16} src={emptyStatus} />
|
||||
</Container>
|
||||
|
||||
<Text padding={{ left: 'medium' }} className={css.sectionSubheader}>
|
||||
{getString('changesSection.waitingOnLatestDefaultReviewers')}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
if (defReviewerApprovalRequiredByRule && !defReviewerApprovedChanges) {
|
||||
//Changes are pending approval from default reviewers
|
||||
return (
|
||||
<Layout.Horizontal>
|
||||
<Container padding={{ left: 'large' }}>
|
||||
<img alt="emptyStatus" width={16} height={16} src={emptyStatus} />
|
||||
</Container>
|
||||
<Text padding={{ left: 'medium' }} className={css.sectionSubheader}>
|
||||
{getString('changesSection.waitingOnDefaultReviewers')}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
|
||||
if (defReviewerLatestApprovalRequiredByRule && defReviewerApprovedLatestChanges) {
|
||||
// Latest changes were approved by default reviewers
|
||||
return (
|
||||
<Text
|
||||
icon={'tick-circle'}
|
||||
iconProps={{
|
||||
size: 16,
|
||||
color: Color.GREEN_700,
|
||||
padding: { right: 'medium' }
|
||||
}}
|
||||
padding={{ left: 'large' }}
|
||||
className={css.sectionSubheader}>
|
||||
{getString('changesSection.latestChangesWereAppByDefaultReviewers')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
if (defReviewerApprovalRequiredByRule && defReviewerApprovedChanges) {
|
||||
//Changes were approved by default reviewers
|
||||
return (
|
||||
<Text
|
||||
icon={'tick-circle'}
|
||||
iconProps={{
|
||||
size: 16,
|
||||
color: Color.GREEN_700,
|
||||
padding: { right: 'medium' }
|
||||
}}
|
||||
padding={{ left: 'large' }}
|
||||
className={css.sectionSubheader}>
|
||||
{getString('changesSection.changesWereAppByDefaultReviewers')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
icon={'tick-circle'}
|
||||
iconProps={{
|
||||
size: 16,
|
||||
color: Color.GREEN_700,
|
||||
padding: { right: 'medium' }
|
||||
}}
|
||||
padding={{ left: 'large' }}
|
||||
className={css.sectionSubheader}>
|
||||
{getString('changesSection.defaultReviewersStatus')}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const viewBtn =
|
||||
!mergeBlockedRule &&
|
||||
(minApproval > minReqLatestApproval ||
|
||||
|
@ -573,6 +686,56 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
</Layout.Horizontal>
|
||||
</Container>
|
||||
)}
|
||||
{!isEmpty(defaultReviewersInfoSet) &&
|
||||
(defReviewerApprovalRequiredByRule || defReviewerLatestApprovalRequiredByRule) && (
|
||||
<Container className={css.borderContainer} padding={{ left: 'xlarge', right: 'small' }}>
|
||||
<Layout.Horizontal className={css.paddingContainer} flex={{ justifyContent: 'space-between' }}>
|
||||
{changesRequestedByDefReviewersArr && changesRequestedByDefReviewersArr?.length > 0 ? (
|
||||
<Text
|
||||
className={cx(
|
||||
css.sectionSubheader,
|
||||
defReviewerApprovalRequiredByRule || defReviewerLatestApprovalRequiredByRule
|
||||
? css.redIcon
|
||||
: css.greyIcon
|
||||
)}
|
||||
icon={'error-transparent-no-outline'}
|
||||
iconProps={{
|
||||
size: 17,
|
||||
color:
|
||||
defReviewerApprovalRequiredByRule || defReviewerLatestApprovalRequiredByRule
|
||||
? Color.RED_600
|
||||
: '',
|
||||
padding: { right: 'medium' }
|
||||
}}
|
||||
padding={{ left: 'large' }}>
|
||||
{getString('changesSection.defaultReviewersChangesToPr')}
|
||||
</Text>
|
||||
) : (
|
||||
renderDefaultReviewersStatus()
|
||||
)}
|
||||
{(defReviewerApprovalRequiredByRule || defReviewerLatestApprovalRequiredByRule) && (
|
||||
<Container className={css.changeContainerPadding}>
|
||||
<Container className={css.requiredContainer}>
|
||||
<Text className={css.requiredText}>{getString('required')}</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
)}
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
)}
|
||||
{!isEmpty(defaultReviewersInfoSet) && (
|
||||
<Container
|
||||
className={css.codeOwnerContainer}
|
||||
padding={{ top: 'small', bottom: 'small', left: 'xxxlarge', right: 'small' }}>
|
||||
<DefaultReviewersPanel
|
||||
defaultRevApprovalResponse={updatedDefaultApprovalRes.filter(
|
||||
res => res.minimum_required_count || res.minimum_required_count_latest
|
||||
)} //to only consider response with min default reviewers required (>0)
|
||||
pullReqMetadata={pullReqMetadata}
|
||||
repoMetadata={repoMetadata}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
{!isEmpty(codeOwners) && !isEmpty(codeOwners.evaluation_entries) && (
|
||||
<Container className={css.borderContainer} padding={{ left: 'xlarge', right: 'small' }}>
|
||||
<Layout.Horizontal className={css.paddingContainer} flex={{ justifyContent: 'space-between' }}>
|
||||
|
@ -604,19 +767,19 @@ const ChangesSection = (props: ChangesSectionProps) => {
|
|||
</Layout.Horizontal>
|
||||
</Container>
|
||||
)}
|
||||
{codeOwners && !isEmpty(codeOwners?.evaluation_entries) && (
|
||||
<Container
|
||||
className={css.codeOwnerContainer}
|
||||
padding={{ top: 'small', bottom: 'small', left: 'xxxlarge', right: 'small' }}>
|
||||
<CodeOwnerSection
|
||||
data={codeOwners}
|
||||
pullReqMetadata={pullReqMetadata}
|
||||
repoMetadata={repoMetadata}
|
||||
reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</Container>
|
||||
{codeOwners && !isEmpty(codeOwners?.evaluation_entries) && (
|
||||
<Container
|
||||
className={css.codeOwnerContainer}
|
||||
padding={{ top: 'small', bottom: 'small', left: 'xxxlarge', right: 'small' }}>
|
||||
<CodeOwnerSection
|
||||
data={codeOwners}
|
||||
pullReqMetadata={pullReqMetadata}
|
||||
repoMetadata={repoMetadata}
|
||||
reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</Render>
|
||||
</Render>
|
||||
)
|
||||
|
|
|
@ -50,29 +50,27 @@ interface ReviewerAddActivityPayload {
|
|||
}
|
||||
|
||||
const formatListWithAndFragment = (names: string[]): React.ReactNode => {
|
||||
const uniqueNames = [...new Set(names)] // Ensure uniqueness
|
||||
|
||||
switch (uniqueNames.length) {
|
||||
switch (names.length) {
|
||||
case 0:
|
||||
return null
|
||||
case 1:
|
||||
return <strong>{uniqueNames[0]}</strong>
|
||||
return <strong>{names[0]}</strong>
|
||||
case 2:
|
||||
return (
|
||||
<>
|
||||
<strong>{uniqueNames[0]}</strong> and <strong>{uniqueNames[1]}</strong>
|
||||
<strong>{names[0]}</strong> and <strong>{names[1]}</strong>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{uniqueNames.slice(0, -1).map((name, index) => (
|
||||
{names.slice(0, -1).map((name, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<strong>{name}</strong>
|
||||
{index < uniqueNames.length - 2 ? ', ' : ''}
|
||||
{index < names.length - 2 ? ', ' : ''}
|
||||
</React.Fragment>
|
||||
))}{' '}
|
||||
and <strong>{uniqueNames[uniqueNames.length - 1]}</strong>
|
||||
and <strong>{names[names.length - 1]}</strong>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -86,10 +84,18 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
|
|||
const { routes } = useAppContext()
|
||||
const displayNameList = useMemo(() => {
|
||||
const checkList = payload?.metadata?.mentions?.ids ?? []
|
||||
const uniqueList = [...new Set(checkList)]
|
||||
const mentionsMap = payload?.mentions ?? {}
|
||||
return [...new Set(checkList.map(id => mentionsMap[id]?.display_name ?? ''))]
|
||||
return uniqueList.map(id => mentionsMap[id]?.display_name ?? '')
|
||||
}, [payload?.metadata?.mentions?.ids, payload?.mentions])
|
||||
|
||||
const principalNameList = useMemo(() => {
|
||||
const checkList = (payload?.payload as any)?.principal_ids ?? []
|
||||
const uniqueList = [...new Set(checkList)]
|
||||
const mentionsMap = payload?.mentions ?? {}
|
||||
return uniqueList.map(id => mentionsMap[id as number]?.display_name ?? '')
|
||||
}, [(payload?.payload as any)?.principal_ids, payload?.mentions])
|
||||
|
||||
switch (type) {
|
||||
case CommentType.MERGE: {
|
||||
return (
|
||||
|
@ -461,6 +467,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
|
|||
|
||||
case CommentType.REVIEWER_ADD: {
|
||||
const activityMentions = formatListWithAndFragment(displayNameList)
|
||||
const principalMentions = formatListWithAndFragment(principalNameList)
|
||||
|
||||
return (
|
||||
<Container className={css.mergedBox}>
|
||||
|
@ -499,7 +506,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
|
|||
str={getString('prReview.codeowners')}
|
||||
vars={{
|
||||
author: <strong>{payload?.author?.display_name}</strong>,
|
||||
codeowners: activityMentions
|
||||
codeowners: principalMentions
|
||||
}}
|
||||
/>
|
||||
</Case>
|
||||
|
@ -508,7 +515,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
|
|||
str={getString('prReview.defaultReviewers')}
|
||||
vars={{
|
||||
author: <strong>{payload?.author?.display_name}</strong>,
|
||||
reviewers: activityMentions
|
||||
reviewers: principalMentions
|
||||
}}
|
||||
/>
|
||||
</Case>
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* Copyright 2023 Harness, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { Container, Text, TableV2, Layout, Avatar } from '@harnessio/uicore'
|
||||
import { Color } from '@harnessio/design-system'
|
||||
import type { CellProps, Column } from 'react-table'
|
||||
import type { GitInfoProps } from 'utils/GitUtils'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import type { TypesDefaultReviewerApprovalsResponseWithRevDecision } from 'utils/Utils'
|
||||
import { PullReqReviewDecision } from '../PullRequestUtils'
|
||||
import css from '../CodeOwners/CodeOwnersOverview.module.scss'
|
||||
|
||||
interface DefaultReviewersPanelProps extends Pick<GitInfoProps, 'repoMetadata' | 'pullReqMetadata'> {
|
||||
defaultRevApprovalResponse: TypesDefaultReviewerApprovalsResponseWithRevDecision[]
|
||||
}
|
||||
|
||||
export const DefaultReviewersPanel: React.FC<DefaultReviewersPanelProps> = ({
|
||||
defaultRevApprovalResponse,
|
||||
pullReqMetadata
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
id: 'REQUIRED',
|
||||
width: '40%',
|
||||
sort: true,
|
||||
Header: getString('required'),
|
||||
accessor: 'REQUIRED',
|
||||
Cell: ({ row }: CellProps<TypesDefaultReviewerApprovalsResponseWithRevDecision>) => {
|
||||
if (row.original?.minimum_required_count && row.original?.minimum_required_count > 0)
|
||||
return (
|
||||
<Text
|
||||
lineClamp={1}
|
||||
padding={{ left: 'small', right: 'small' }}
|
||||
color={Color.BLACK}
|
||||
flex={{ justifyContent: 'space-between' }}>
|
||||
{row.original.current_count} / {row.original.minimum_required_count}
|
||||
</Text>
|
||||
)
|
||||
else if (row.original?.minimum_required_count_latest && row.original?.minimum_required_count_latest > 0)
|
||||
return (
|
||||
<Layout.Horizontal>
|
||||
<Text
|
||||
lineClamp={1}
|
||||
padding={{ left: 'small', right: 'small' }}
|
||||
color={Color.BLACK}
|
||||
flex={{ justifyContent: 'space-between' }}>
|
||||
{row.original.current_count} / {row.original.minimum_required_count_latest}
|
||||
</Text>
|
||||
<Text color={Color.GREY_350}>({getString('onLatestChanges')})</Text>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
else <Text>{row.original.current_count}</Text>
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'DefaultReviewers',
|
||||
width: '18%',
|
||||
sort: true,
|
||||
Header: getString('defaultReviewers'),
|
||||
accessor: 'DefaultReviewers',
|
||||
Cell: ({ row }: CellProps<TypesDefaultReviewerApprovalsResponseWithRevDecision>) => {
|
||||
return (
|
||||
<Layout.Horizontal
|
||||
key={`keyContainer-${row.original.rule_info?.identifier}`}
|
||||
className={css.ownerContainer}
|
||||
spacing="tiny">
|
||||
{row.original.principals?.map((principal, idx) => {
|
||||
if (idx < 2) {
|
||||
return (
|
||||
<Avatar
|
||||
key={`text-${principal?.display_name}-${idx}-avatar`}
|
||||
hoverCard={true}
|
||||
email={principal?.email || ' '}
|
||||
size="small"
|
||||
name={principal?.display_name || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (idx === 2 && row.original.principals?.length && row.original.principals?.length > 2) {
|
||||
return (
|
||||
<Text
|
||||
key={`text-${principal?.display_name}-${idx}-top`}
|
||||
padding={{ top: 'xsmall' }}
|
||||
tooltipProps={{ isDark: true }}
|
||||
tooltip={
|
||||
<Container width={215} padding={'small'}>
|
||||
<Layout.Horizontal key={`tooltip-${idx}`} className={css.ownerTooltip}>
|
||||
{row.original.principals?.map((entry, entryidx) => (
|
||||
<Text
|
||||
key={`text-${entry?.display_name}-${entryidx}`}
|
||||
lineClamp={1}
|
||||
color={Color.GREY_0}
|
||||
padding={{ right: 'small' }}>
|
||||
{row.original.principals?.length === entryidx + 1
|
||||
? `${entry?.display_name}`
|
||||
: `${entry?.display_name}, `}
|
||||
</Text>
|
||||
))}
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
}
|
||||
flex={{ alignItems: 'center' }}>{`+${row.original.principals?.length - 2}`}</Text>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'changesRequested',
|
||||
Header: getString('changesRequestedBy'),
|
||||
width: '24%',
|
||||
sort: true,
|
||||
accessor: 'ChangesRequested',
|
||||
Cell: ({ row }: CellProps<TypesDefaultReviewerApprovalsResponseWithRevDecision>) => {
|
||||
const changeReqEvaluations = row?.original?.principals?.filter(
|
||||
principal => principal?.review_decision === PullReqReviewDecision.CHANGEREQ
|
||||
)
|
||||
return (
|
||||
<Layout.Horizontal className={css.ownerContainer} spacing="tiny">
|
||||
{changeReqEvaluations?.map((principal, idx: number) => {
|
||||
if (idx < 2) {
|
||||
return (
|
||||
<Avatar
|
||||
key={`approved-${principal?.display_name}-avatar`}
|
||||
hoverCard={true}
|
||||
email={principal?.email || ' '}
|
||||
size="small"
|
||||
name={principal?.display_name || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (idx === 2 && changeReqEvaluations.length && changeReqEvaluations.length > 2) {
|
||||
return (
|
||||
<Text
|
||||
key={`approved-${principal?.display_name}-text`}
|
||||
padding={{ top: 'xsmall' }}
|
||||
tooltipProps={{ isDark: true }}
|
||||
tooltip={
|
||||
<Container width={215} padding={'small'}>
|
||||
<Layout.Horizontal className={css.ownerTooltip}>
|
||||
{changeReqEvaluations?.map(evalPrincipal => (
|
||||
<Text
|
||||
key={`approved-${evalPrincipal?.display_name}`}
|
||||
lineClamp={1}
|
||||
color={Color.GREY_0}
|
||||
padding={{ right: 'small' }}>{`${evalPrincipal?.display_name}, `}</Text>
|
||||
))}
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
}
|
||||
flex={{ alignItems: 'center' }}>{`+${changeReqEvaluations.length - 2}`}</Text>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'approvedBy',
|
||||
Header: getString('approvedBy'),
|
||||
sort: true,
|
||||
width: '15%',
|
||||
accessor: 'APPROVED BY',
|
||||
Cell: ({ row }: CellProps<TypesDefaultReviewerApprovalsResponseWithRevDecision>) => {
|
||||
const approvedEvaluations = row?.original?.principals?.filter(
|
||||
principal =>
|
||||
principal.review_decision === PullReqReviewDecision.APPROVED &&
|
||||
(row.original.minimum_required_count_latest
|
||||
? principal.review_sha === pullReqMetadata?.source_sha
|
||||
: true)
|
||||
)
|
||||
|
||||
return (
|
||||
<Layout.Horizontal className={css.ownerContainer} spacing="tiny">
|
||||
{approvedEvaluations?.map((principal, idx: number) => {
|
||||
if (idx < 2) {
|
||||
return (
|
||||
<Avatar
|
||||
key={`approved-${principal?.display_name}-avatar`}
|
||||
hoverCard={true}
|
||||
email={principal?.email || ' '}
|
||||
size="small"
|
||||
name={principal?.display_name || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (idx === 2 && approvedEvaluations.length && approvedEvaluations.length > 2) {
|
||||
return (
|
||||
<Text
|
||||
key={`approved-${principal?.display_name}-text`}
|
||||
padding={{ top: 'xsmall' }}
|
||||
tooltipProps={{ isDark: true }}
|
||||
tooltip={
|
||||
<Container width={215} padding={'small'}>
|
||||
<Layout.Horizontal className={css.ownerTooltip}>
|
||||
{approvedEvaluations?.map(appPrincipalObj => (
|
||||
<Text
|
||||
key={`approved-${appPrincipalObj?.display_name}`}
|
||||
lineClamp={1}
|
||||
color={Color.GREY_0}
|
||||
padding={{ right: 'small' }}>{`${appPrincipalObj?.display_name}, `}</Text>
|
||||
))}
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
}
|
||||
flex={{ alignItems: 'center' }}>{`+${approvedEvaluations.length - 2}`}</Text>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
}
|
||||
}
|
||||
] as unknown as Column<TypesDefaultReviewerApprovalsResponseWithRevDecision>[], // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
return (
|
||||
<Render when={defaultRevApprovalResponse?.length}>
|
||||
<Container>
|
||||
<Layout.Vertical spacing="small">
|
||||
<TableV2
|
||||
className={css.codeOwnerTable}
|
||||
sortable
|
||||
columns={columns}
|
||||
data={defaultRevApprovalResponse as TypesDefaultReviewerApprovalsResponseWithRevDecision[]}
|
||||
getRowClassName={() => css.row}
|
||||
/>
|
||||
</Layout.Vertical>
|
||||
</Container>
|
||||
</Render>
|
||||
)
|
||||
}
|
|
@ -23,9 +23,12 @@ import type {
|
|||
EnumMergeMethod,
|
||||
EnumPullReqReviewDecision,
|
||||
TypesCodeOwnerEvaluationEntry,
|
||||
TypesDefaultReviewerApprovalsResponse,
|
||||
TypesOwnerEvaluation,
|
||||
TypesPrincipalInfo,
|
||||
TypesPullReq,
|
||||
TypesPullReqActivity
|
||||
TypesPullReqActivity,
|
||||
TypesPullReqReviewer
|
||||
} from 'services/code'
|
||||
|
||||
export interface PRMergeOption extends SelectOption {
|
||||
|
@ -95,7 +98,7 @@ export const findWaitingDecisions = (
|
|||
const hasApprovedDecision = entry?.owner_evaluations?.some(
|
||||
evaluation =>
|
||||
evaluation.review_decision === PullReqReviewDecision.APPROVED &&
|
||||
(reqCodeOwnerLatestApproval ? evaluation.review_sha === pullReqMetadata.source_sha : true)
|
||||
(reqCodeOwnerLatestApproval ? evaluation.review_sha === pullReqMetadata?.source_sha : true)
|
||||
)
|
||||
return !hasApprovedDecision
|
||||
})
|
||||
|
@ -176,3 +179,41 @@ export const getMergeOptions = (getString: UseStringsReturn['getString'], mergea
|
|||
value: 'close'
|
||||
}
|
||||
]
|
||||
|
||||
export const updateReviewDecisionPrincipal = (reviewers: TypesPullReqReviewer[], principals: TypesPrincipalInfo[]) => {
|
||||
const reviewDecisionMap: {
|
||||
[x: number]: { sha: string; review_decision: EnumPullReqReviewDecision } | null
|
||||
} = reviewers?.reduce((acc, rev) => {
|
||||
if (rev.reviewer?.id) {
|
||||
acc[rev.reviewer.id] = {
|
||||
sha: rev.sha ?? '',
|
||||
review_decision: rev.review_decision ?? 'pending'
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {} as { [x: number]: { sha: string; review_decision: EnumPullReqReviewDecision } | null })
|
||||
|
||||
return principals?.map(principal => {
|
||||
if (principal?.id) {
|
||||
return {
|
||||
...principal,
|
||||
review_decision: reviewDecisionMap[principal.id] ? reviewDecisionMap[principal.id]?.review_decision : 'pending',
|
||||
review_sha: reviewDecisionMap[principal.id]?.sha
|
||||
}
|
||||
}
|
||||
return principal
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultReviewerResponseWithDecision = (
|
||||
defaultRevApprovalResponse: TypesDefaultReviewerApprovalsResponse[],
|
||||
reviewers: TypesPullReqReviewer[]
|
||||
) => {
|
||||
return defaultRevApprovalResponse?.map(res => {
|
||||
return {
|
||||
...res,
|
||||
principals:
|
||||
reviewers && res.principals ? updateReviewDecisionPrincipal(reviewers, res.principals) : res.principals
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ export type EnumPullReqCommentStatus = 'active' | 'resolved'
|
|||
|
||||
export type EnumPullReqReviewDecision = 'approved' | 'changereq' | 'pending' | 'reviewed'
|
||||
|
||||
export type EnumPullReqReviewerType = 'assigned' | 'requested' | 'self_assigned'
|
||||
export type EnumPullReqReviewerType = 'assigned' | 'code_owners' | 'default' | 'requested' | 'self_assigned'
|
||||
|
||||
export type EnumPullReqState = 'closed' | 'merged' | 'open'
|
||||
|
||||
|
@ -183,11 +183,11 @@ export type EnumWebhookTrigger =
|
|||
| 'pullreq_comment_created'
|
||||
| 'pullreq_comment_status_updated'
|
||||
| 'pullreq_comment_updated'
|
||||
| 'pullreq_review_submitted'
|
||||
| 'pullreq_created'
|
||||
| 'pullreq_label_assigned'
|
||||
| 'pullreq_merged'
|
||||
| 'pullreq_reopened'
|
||||
| 'pullreq_review_submitted'
|
||||
| 'pullreq_updated'
|
||||
| 'tag_created'
|
||||
| 'tag_deleted'
|
||||
|
@ -344,6 +344,7 @@ export interface OpenapiCommentUpdatePullReqRequest {
|
|||
|
||||
export interface OpenapiCommitFilesRequest {
|
||||
actions?: RepoCommitFileAction[] | null
|
||||
author?: GitIdentity
|
||||
branch?: string
|
||||
bypass_rules?: boolean
|
||||
dry_run_rules?: boolean
|
||||
|
@ -738,6 +739,7 @@ export interface OpenapiWebhookType {
|
|||
latest_execution_result?: EnumWebhookExecutionResult
|
||||
parent_id?: number
|
||||
parent_type?: EnumWebhookParent
|
||||
scope?: number
|
||||
triggers?: EnumWebhookTrigger[] | null
|
||||
updated?: number
|
||||
url?: string
|
||||
|
@ -754,6 +756,7 @@ export interface ProtectionDefApprovals {
|
|||
require_code_owners?: boolean
|
||||
require_latest_commit?: boolean
|
||||
require_minimum_count?: number
|
||||
require_minimum_default_reviewer_count?: number
|
||||
require_no_change_request?: boolean
|
||||
}
|
||||
|
||||
|
@ -1119,6 +1122,14 @@ export interface TypesCreateBranchOutput {
|
|||
sha?: ShaSHA
|
||||
}
|
||||
|
||||
export interface TypesDefaultReviewerApprovalsResponse {
|
||||
current_count?: number
|
||||
minimum_required_count?: number
|
||||
minimum_required_count_latest?: number
|
||||
principals?: TypesPrincipalInfo[] | null
|
||||
rule_info?: TypesRuleInfo
|
||||
}
|
||||
|
||||
export interface TypesDeleteBranchOutput {
|
||||
dry_run_rules?: boolean
|
||||
rule_violations?: TypesRuleViolations[]
|
||||
|
@ -1358,6 +1369,7 @@ export interface TypesMergeResponse {
|
|||
allowed_methods?: EnumMergeMethod[]
|
||||
branch_deleted?: boolean
|
||||
conflict_files?: string[]
|
||||
default_reviewer_aprovals?: TypesDefaultReviewerApprovalsResponse[]
|
||||
dry_run?: boolean
|
||||
dry_run_rules?: boolean
|
||||
mergeable?: boolean
|
||||
|
@ -1676,6 +1688,7 @@ export interface TypesServiceAccount {
|
|||
created?: number
|
||||
display_name?: string
|
||||
email?: string
|
||||
id?: number
|
||||
parent_id?: number
|
||||
parent_type?: EnumParentResourceType
|
||||
uid?: string
|
||||
|
@ -1801,8 +1814,10 @@ export interface TypesUser {
|
|||
|
||||
export interface TypesUserGroupInfo {
|
||||
description?: string
|
||||
id?: number
|
||||
identifier?: string
|
||||
name?: string
|
||||
scope?: number
|
||||
}
|
||||
|
||||
export interface TypesUserGroupOwnerEvaluation {
|
||||
|
@ -3761,7 +3776,7 @@ export interface ListRepoLabelsQueryParams {
|
|||
*/
|
||||
limit?: number
|
||||
/**
|
||||
* The result should inherit labels from parent parent spaces.
|
||||
* The result should inherit entities from parent spaces.
|
||||
*/
|
||||
inherited?: boolean
|
||||
/**
|
||||
|
@ -6636,11 +6651,11 @@ export const useRuleList = ({ repo_ref, ...props }: UseRuleListProps) =>
|
|||
{ base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props }
|
||||
)
|
||||
|
||||
export interface RuleAddPathParams {
|
||||
export interface RepoRuleAddPathParams {
|
||||
repo_ref: string
|
||||
}
|
||||
|
||||
export interface RuleAddRequestBody {
|
||||
export interface RepoRuleAddRequestBody {
|
||||
definition?: OpenapiRuleDefinition
|
||||
description?: string
|
||||
identifier?: string
|
||||
|
@ -6650,14 +6665,14 @@ export interface RuleAddRequestBody {
|
|||
uid?: string
|
||||
}
|
||||
|
||||
export type RuleAddProps = Omit<
|
||||
MutateProps<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>,
|
||||
export type RepoRuleAddProps = Omit<
|
||||
MutateProps<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>,
|
||||
'path' | 'verb'
|
||||
> &
|
||||
RuleAddPathParams
|
||||
RepoRuleAddPathParams
|
||||
|
||||
export const RuleAdd = ({ repo_ref, ...props }: RuleAddProps) => (
|
||||
<Mutate<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>
|
||||
export const RepoRuleAdd = ({ repo_ref, ...props }: RepoRuleAddProps) => (
|
||||
<Mutate<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>
|
||||
verb="POST"
|
||||
path={`/repos/${repo_ref}/rules`}
|
||||
base={getConfig('code/api/v1')}
|
||||
|
@ -6665,31 +6680,31 @@ export const RuleAdd = ({ repo_ref, ...props }: RuleAddProps) => (
|
|||
/>
|
||||
)
|
||||
|
||||
export type UseRuleAddProps = Omit<
|
||||
UseMutateProps<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>,
|
||||
export type UseRepoRuleAddProps = Omit<
|
||||
UseMutateProps<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>,
|
||||
'path' | 'verb'
|
||||
> &
|
||||
RuleAddPathParams
|
||||
RepoRuleAddPathParams
|
||||
|
||||
export const useRuleAdd = ({ repo_ref, ...props }: UseRuleAddProps) =>
|
||||
useMutate<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>(
|
||||
export const useRepoRuleAdd = ({ repo_ref, ...props }: UseRepoRuleAddProps) =>
|
||||
useMutate<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>(
|
||||
'POST',
|
||||
(paramsInPath: RuleAddPathParams) => `/repos/${paramsInPath.repo_ref}/rules`,
|
||||
(paramsInPath: RepoRuleAddPathParams) => `/repos/${paramsInPath.repo_ref}/rules`,
|
||||
{ base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props }
|
||||
)
|
||||
|
||||
export interface RuleDeletePathParams {
|
||||
export interface RepoRuleDeletePathParams {
|
||||
repo_ref: string
|
||||
}
|
||||
|
||||
export type RuleDeleteProps = Omit<
|
||||
MutateProps<void, UsererrorError, void, string, RuleDeletePathParams>,
|
||||
export type RepoRuleDeleteProps = Omit<
|
||||
MutateProps<void, UsererrorError, void, string, RepoRuleDeletePathParams>,
|
||||
'path' | 'verb'
|
||||
> &
|
||||
RuleDeletePathParams
|
||||
RepoRuleDeletePathParams
|
||||
|
||||
export const RuleDelete = ({ repo_ref, ...props }: RuleDeleteProps) => (
|
||||
<Mutate<void, UsererrorError, void, string, RuleDeletePathParams>
|
||||
export const RepoRuleDelete = ({ repo_ref, ...props }: RepoRuleDeleteProps) => (
|
||||
<Mutate<void, UsererrorError, void, string, RepoRuleDeletePathParams>
|
||||
verb="DELETE"
|
||||
path={`/repos/${repo_ref}/rules`}
|
||||
base={getConfig('code/api/v1')}
|
||||
|
@ -6697,50 +6712,50 @@ export const RuleDelete = ({ repo_ref, ...props }: RuleDeleteProps) => (
|
|||
/>
|
||||
)
|
||||
|
||||
export type UseRuleDeleteProps = Omit<
|
||||
UseMutateProps<void, UsererrorError, void, string, RuleDeletePathParams>,
|
||||
export type UseRepoRuleDeleteProps = Omit<
|
||||
UseMutateProps<void, UsererrorError, void, string, RepoRuleDeletePathParams>,
|
||||
'path' | 'verb'
|
||||
> &
|
||||
RuleDeletePathParams
|
||||
RepoRuleDeletePathParams
|
||||
|
||||
export const useRuleDelete = ({ repo_ref, ...props }: UseRuleDeleteProps) =>
|
||||
useMutate<void, UsererrorError, void, string, RuleDeletePathParams>(
|
||||
export const useRepoRuleDelete = ({ repo_ref, ...props }: UseRepoRuleDeleteProps) =>
|
||||
useMutate<void, UsererrorError, void, string, RepoRuleDeletePathParams>(
|
||||
'DELETE',
|
||||
(paramsInPath: RuleDeletePathParams) => `/repos/${paramsInPath.repo_ref}/rules`,
|
||||
(paramsInPath: RepoRuleDeletePathParams) => `/repos/${paramsInPath.repo_ref}/rules`,
|
||||
{ base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props }
|
||||
)
|
||||
|
||||
export interface RuleGetPathParams {
|
||||
export interface RepoRuleGetPathParams {
|
||||
repo_ref: string
|
||||
rule_identifier: string
|
||||
}
|
||||
|
||||
export type RuleGetProps = Omit<GetProps<OpenapiRule, UsererrorError, void, RuleGetPathParams>, 'path'> &
|
||||
RuleGetPathParams
|
||||
export type RepoRuleGetProps = Omit<GetProps<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>, 'path'> &
|
||||
RepoRuleGetPathParams
|
||||
|
||||
export const RuleGet = ({ repo_ref, rule_identifier, ...props }: RuleGetProps) => (
|
||||
<Get<OpenapiRule, UsererrorError, void, RuleGetPathParams>
|
||||
export const RepoRuleGet = ({ repo_ref, rule_identifier, ...props }: RepoRuleGetProps) => (
|
||||
<Get<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>
|
||||
path={`/repos/${repo_ref}/rules/${rule_identifier}`}
|
||||
base={getConfig('code/api/v1')}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export type UseRuleGetProps = Omit<UseGetProps<OpenapiRule, UsererrorError, void, RuleGetPathParams>, 'path'> &
|
||||
RuleGetPathParams
|
||||
export type UseRepoRuleGetProps = Omit<UseGetProps<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>, 'path'> &
|
||||
RepoRuleGetPathParams
|
||||
|
||||
export const useRuleGet = ({ repo_ref, rule_identifier, ...props }: UseRuleGetProps) =>
|
||||
useGet<OpenapiRule, UsererrorError, void, RuleGetPathParams>(
|
||||
(paramsInPath: RuleGetPathParams) => `/repos/${paramsInPath.repo_ref}/rules/${paramsInPath.rule_identifier}`,
|
||||
export const useRepoRuleGet = ({ repo_ref, rule_identifier, ...props }: UseRepoRuleGetProps) =>
|
||||
useGet<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>(
|
||||
(paramsInPath: RepoRuleGetPathParams) => `/repos/${paramsInPath.repo_ref}/rules/${paramsInPath.rule_identifier}`,
|
||||
{ base: getConfig('code/api/v1'), pathParams: { repo_ref, rule_identifier }, ...props }
|
||||
)
|
||||
|
||||
export interface RuleUpdatePathParams {
|
||||
export interface RepoRuleUpdatePathParams {
|
||||
repo_ref: string
|
||||
rule_identifier: string
|
||||
}
|
||||
|
||||
export interface RuleUpdateRequestBody {
|
||||
export interface RepoRuleUpdateRequestBody {
|
||||
definition?: OpenapiRuleDefinition
|
||||
description?: string | null
|
||||
identifier?: string | null
|
||||
|
@ -6750,14 +6765,14 @@ export interface RuleUpdateRequestBody {
|
|||
uid?: string | null
|
||||
}
|
||||
|
||||
export type RuleUpdateProps = Omit<
|
||||
MutateProps<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>,
|
||||
export type RepoRuleUpdateProps = Omit<
|
||||
MutateProps<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>,
|
||||
'path' | 'verb'
|
||||
> &
|
||||
RuleUpdatePathParams
|
||||
RepoRuleUpdatePathParams
|
||||
|
||||
export const RuleUpdate = ({ repo_ref, rule_identifier, ...props }: RuleUpdateProps) => (
|
||||
<Mutate<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>
|
||||
export const RepoRuleUpdate = ({ repo_ref, rule_identifier, ...props }: RepoRuleUpdateProps) => (
|
||||
<Mutate<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>
|
||||
verb="PATCH"
|
||||
path={`/repos/${repo_ref}/rules/${rule_identifier}`}
|
||||
base={getConfig('code/api/v1')}
|
||||
|
@ -6765,16 +6780,16 @@ export const RuleUpdate = ({ repo_ref, rule_identifier, ...props }: RuleUpdatePr
|
|||
/>
|
||||
)
|
||||
|
||||
export type UseRuleUpdateProps = Omit<
|
||||
UseMutateProps<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>,
|
||||
export type UseRepoRuleUpdateProps = Omit<
|
||||
UseMutateProps<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>,
|
||||
'path' | 'verb'
|
||||
> &
|
||||
RuleUpdatePathParams
|
||||
RepoRuleUpdatePathParams
|
||||
|
||||
export const useRuleUpdate = ({ repo_ref, rule_identifier, ...props }: UseRuleUpdateProps) =>
|
||||
useMutate<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>(
|
||||
export const useRepoRuleUpdate = ({ repo_ref, rule_identifier, ...props }: UseRepoRuleUpdateProps) =>
|
||||
useMutate<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>(
|
||||
'PATCH',
|
||||
(paramsInPath: RuleUpdatePathParams) => `/repos/${paramsInPath.repo_ref}/rules/${paramsInPath.rule_identifier}`,
|
||||
(paramsInPath: RepoRuleUpdatePathParams) => `/repos/${paramsInPath.repo_ref}/rules/${paramsInPath.rule_identifier}`,
|
||||
{ base: getConfig('code/api/v1'), pathParams: { repo_ref, rule_identifier }, ...props }
|
||||
)
|
||||
|
||||
|
|
|
@ -12287,6 +12287,8 @@ components:
|
|||
- deleted
|
||||
- starting
|
||||
- stopping
|
||||
- cleaning
|
||||
- cleaned
|
||||
type: string
|
||||
EnumGitspaceOwner:
|
||||
enum:
|
||||
|
@ -12416,6 +12418,8 @@ components:
|
|||
EnumPullReqReviewerType:
|
||||
enum:
|
||||
- assigned
|
||||
- code_owners
|
||||
- default
|
||||
- requested
|
||||
- self_assigned
|
||||
type: string
|
||||
|
@ -12468,11 +12472,15 @@ components:
|
|||
type: string
|
||||
EnumWebhookParent:
|
||||
enum:
|
||||
- registry
|
||||
- repo
|
||||
- space
|
||||
type: string
|
||||
EnumWebhookTrigger:
|
||||
enum:
|
||||
- artifact_created
|
||||
- artifact_deleted
|
||||
- artifact_updated
|
||||
- branch_created
|
||||
- branch_deleted
|
||||
- branch_updated
|
||||
|
@ -14439,8 +14447,13 @@ components:
|
|||
properties:
|
||||
created:
|
||||
type: integer
|
||||
deleted:
|
||||
nullable: true
|
||||
type: integer
|
||||
identifier:
|
||||
type: string
|
||||
is_deleted:
|
||||
type: boolean
|
||||
metadata:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
|
@ -15229,6 +15242,8 @@ components:
|
|||
type: string
|
||||
email:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
parent_id:
|
||||
type: integer
|
||||
parent_type:
|
||||
|
@ -15469,6 +15484,8 @@ components:
|
|||
type: string
|
||||
email:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
uid:
|
||||
type: string
|
||||
updated:
|
||||
|
|
|
@ -28,7 +28,8 @@ import type {
|
|||
TypesCommit,
|
||||
TypesPullReq,
|
||||
RepoRepositoryOutput,
|
||||
TypesRuleViolations
|
||||
TypesRuleViolations,
|
||||
TypesDefaultReviewerApprovalsResponse
|
||||
} from 'services/code'
|
||||
import { getConfig } from 'services/config'
|
||||
import { PullRequestSection, getErrorMessage } from './Utils'
|
||||
|
@ -173,7 +174,8 @@ export enum GitRefType {
|
|||
|
||||
export enum PrincipalUserType {
|
||||
USER = 'user',
|
||||
SERVICE = 'service'
|
||||
SERVICE = 'service',
|
||||
SERVICE_ACCOUNT = 'serviceaccount'
|
||||
}
|
||||
|
||||
export enum SettingTypeMode {
|
||||
|
@ -540,9 +542,10 @@ export const dryMerge = (
|
|||
setMinApproval?: (value: React.SetStateAction<number>) => void,
|
||||
setReqCodeOwnerLatestApproval?: (value: React.SetStateAction<boolean>) => void,
|
||||
setMinReqLatestApproval?: (value: React.SetStateAction<number>) => void,
|
||||
setPRStateLoading?: (value: React.SetStateAction<boolean>) => void
|
||||
setPRStateLoading?: (value: React.SetStateAction<boolean>) => void,
|
||||
setDefaultReviewersInfoSet?: React.Dispatch<React.SetStateAction<TypesDefaultReviewerApprovalsResponse[]>>
|
||||
) => {
|
||||
if (isMounted.current && !isClosed && pullReqMetadata.state !== PullRequestState.MERGED) {
|
||||
if (isMounted.current && !isClosed && pullReqMetadata?.state !== PullRequestState.MERGED) {
|
||||
// Use an internal flag to prevent flickering during the loading state of buttons
|
||||
internalFlags.current.dryRun = true
|
||||
mergePR({ bypass_rules: true, dry_run: true, source_sha: pullReqMetadata?.source_sha })
|
||||
|
@ -558,6 +561,7 @@ export const dryMerge = (
|
|||
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
|
||||
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
|
||||
setConflictingFiles?.(res.conflict_files)
|
||||
setDefaultReviewersInfoSet?.(res.default_reviewer_aprovals)
|
||||
} else {
|
||||
setRuleViolation(false)
|
||||
setAllowedStrats(res.allowed_methods)
|
||||
|
@ -568,6 +572,7 @@ export const dryMerge = (
|
|||
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
|
||||
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
|
||||
setConflictingFiles?.(res.conflict_files)
|
||||
setDefaultReviewersInfoSet?.(res.default_reviewer_aprovals)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
|
@ -582,6 +587,7 @@ export const dryMerge = (
|
|||
setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest)
|
||||
setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest)
|
||||
setConflictingFiles?.(err.conflict_files)
|
||||
setDefaultReviewersInfoSet?.(err.default_reviewer_aprovals)
|
||||
} else if (
|
||||
err.status === 400 &&
|
||||
[oldCommitRefetchRequired, prMergedRefetchRequired].includes(getErrorMessage(err) || '')
|
||||
|
|
|
@ -30,9 +30,11 @@ import type {
|
|||
TypesLabel,
|
||||
TypesLabelValue,
|
||||
TypesPrincipalInfo,
|
||||
EnumMembershipRole
|
||||
EnumMembershipRole,
|
||||
TypesDefaultReviewerApprovalsResponse
|
||||
} from 'services/code'
|
||||
import type { StringKeys } from 'framework/strings'
|
||||
import { PullReqReviewDecision } from 'pages/PullRequest/PullRequestUtils'
|
||||
|
||||
export enum ACCESS_MODES {
|
||||
VIEW,
|
||||
|
@ -361,8 +363,11 @@ export const rulesFormInitialPayload: RulesFormPayload = {
|
|||
targetList: [] as string[][],
|
||||
allRepoOwners: false,
|
||||
bypassList: [] as string[],
|
||||
defaultReviewersList: [] as string[],
|
||||
requireMinReviewers: false,
|
||||
requireMinDefaultReviewers: false,
|
||||
minReviewers: '',
|
||||
minDefaultReviewers: '',
|
||||
requireCodeOwner: false,
|
||||
requireNewChanges: false,
|
||||
reqResOfChanges: false,
|
||||
|
@ -380,7 +385,9 @@ export const rulesFormInitialPayload: RulesFormPayload = {
|
|||
blockForcePush: false,
|
||||
requirePr: false,
|
||||
bypassSet: false,
|
||||
targetSet: false
|
||||
targetSet: false,
|
||||
defaultReviewersSet: false,
|
||||
defaultReviewersEnabled: false
|
||||
}
|
||||
|
||||
export type RulesFormPayload = {
|
||||
|
@ -392,8 +399,11 @@ export type RulesFormPayload = {
|
|||
targetList: string[][]
|
||||
allRepoOwners?: boolean
|
||||
bypassList?: string[]
|
||||
defaultReviewersList?: string[]
|
||||
requireMinReviewers: boolean
|
||||
requireMinDefaultReviewers: boolean
|
||||
minReviewers?: string | number
|
||||
minDefaultReviewers?: string | number
|
||||
autoAddCodeOwner?: boolean
|
||||
requireCodeOwner?: boolean
|
||||
requireNewChanges?: boolean
|
||||
|
@ -414,6 +424,8 @@ export type RulesFormPayload = {
|
|||
requirePr?: boolean
|
||||
bypassSet: boolean
|
||||
targetSet: boolean
|
||||
defaultReviewersSet: boolean
|
||||
defaultReviewersEnabled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1002,8 +1014,10 @@ export enum RuleFields {
|
|||
APPROVALS_REQUIRE_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count',
|
||||
APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners',
|
||||
APPROVALS_REQUIRE_NO_CHANGE_REQUEST = 'pullreq.approvals.require_no_change_request',
|
||||
APPROVALS_REQUIRE_MINIMUM_DEFAULT_REVIEWERS = 'pullreq.approvals.require_minimum_default_reviewer_count',
|
||||
APPROVALS_REQUIRE_LATEST_COMMIT = 'pullreq.approvals.require_latest_commit',
|
||||
AUTO_ADD_CODE_OWNERS = 'pullreq.reviewers.request_code_owners',
|
||||
DEFAULT_REVIEWERS_ADDED = 'pullreq.reviewers.default_reviewer_ids',
|
||||
COMMENTS_REQUIRE_RESOLVE_ALL = 'pullreq.comments.require_resolve_all',
|
||||
STATUS_CHECKS_ALL_MUST_SUCCEED = 'pullreq.status_checks.all_must_succeed',
|
||||
STATUS_CHECKS_REQUIRE_IDENTIFIERS = 'pullreq.status_checks.require_identifiers',
|
||||
|
@ -1034,10 +1048,12 @@ export type BranchProtectionRulesMapType = Record<string, BranchProtectionRule>
|
|||
export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
|
||||
const ruleFieldsMap: RuleFieldsMap = {
|
||||
[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: false,
|
||||
[RuleFields.AUTO_ADD_CODE_OWNERS]: false,
|
||||
[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: false,
|
||||
[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: false,
|
||||
[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT]: false,
|
||||
[RuleFields.APPROVALS_REQUIRE_MINIMUM_DEFAULT_REVIEWERS]: false,
|
||||
[RuleFields.AUTO_ADD_CODE_OWNERS]: false,
|
||||
[RuleFields.DEFAULT_REVIEWERS_ADDED]: false,
|
||||
[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL]: false,
|
||||
[RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: false,
|
||||
[RuleFields.STATUS_CHECKS_REQUIRE_IDENTIFIERS]: false,
|
||||
|
@ -1058,6 +1074,8 @@ export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
|
|||
typeof ruleDefinition.pullreq.approvals.require_minimum_count === 'number'
|
||||
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST] =
|
||||
!!ruleDefinition.pullreq.approvals.require_no_change_request
|
||||
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_MINIMUM_DEFAULT_REVIEWERS] =
|
||||
!!ruleDefinition.pullreq.approvals.require_minimum_default_reviewer_count
|
||||
}
|
||||
|
||||
if (ruleDefinition.pullreq.comments) {
|
||||
|
@ -1080,6 +1098,9 @@ export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
|
|||
|
||||
if (ruleDefinition.pullreq.reviewers) {
|
||||
ruleFieldsMap[RuleFields.AUTO_ADD_CODE_OWNERS] = !!ruleDefinition.pullreq.reviewers.request_code_owners
|
||||
ruleFieldsMap[RuleFields.DEFAULT_REVIEWERS_ADDED] =
|
||||
Array.isArray(ruleDefinition.pullreq.reviewers.default_reviewer_ids) &&
|
||||
ruleDefinition.pullreq.reviewers.default_reviewer_ids.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1122,3 +1143,43 @@ export const formatListWithAnd = (list: string[]): string => {
|
|||
return `${list.slice(0, -1).join(', ')} and ${list[list.length - 1]}`
|
||||
} else return ''
|
||||
}
|
||||
|
||||
export interface TypesPrincipalInfoWithReviewDecision extends TypesPrincipalInfo {
|
||||
review_decision?: PullReqReviewDecision
|
||||
review_sha?: string
|
||||
}
|
||||
|
||||
export interface TypesDefaultReviewerApprovalsResponseWithRevDecision extends TypesDefaultReviewerApprovalsResponse {
|
||||
principals?: TypesPrincipalInfoWithReviewDecision[] | null // Override the 'principals' field
|
||||
}
|
||||
export const getUnifiedDefaultReviewersState = (info: TypesDefaultReviewerApprovalsResponseWithRevDecision[]) => {
|
||||
const defaultReviewState = {
|
||||
defReviewerApprovalRequiredByRule: false,
|
||||
defReviewerLatestApprovalRequiredByRule: false,
|
||||
defReviewerApprovedLatestChanges: true,
|
||||
defReviewerApprovedChanges: true,
|
||||
changesRequestedByDefReviewersArr: [] as TypesPrincipalInfoWithReviewDecision[]
|
||||
}
|
||||
|
||||
info?.forEach(item => {
|
||||
if (item?.minimum_required_count !== undefined && item.minimum_required_count > 0) {
|
||||
defaultReviewState.defReviewerApprovalRequiredByRule = true
|
||||
if (item.current_count !== undefined && item.current_count < item.minimum_required_count) {
|
||||
defaultReviewState.defReviewerApprovedChanges = false
|
||||
}
|
||||
}
|
||||
if (item?.minimum_required_count_latest !== undefined && item.minimum_required_count_latest > 0) {
|
||||
defaultReviewState.defReviewerLatestApprovalRequiredByRule = true
|
||||
if (item.current_count !== undefined && item.current_count < item.minimum_required_count_latest) {
|
||||
defaultReviewState.defReviewerApprovedLatestChanges = false
|
||||
}
|
||||
}
|
||||
|
||||
item?.principals?.forEach(principal => {
|
||||
if (principal?.review_decision === PullReqReviewDecision.CHANGEREQ)
|
||||
defaultReviewState.changesRequestedByDefReviewersArr.push(principal)
|
||||
})
|
||||
})
|
||||
|
||||
return defaultReviewState
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue