feat: [CODE-3324] add default reviewers support ()

* 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 support
main
Ritik Kapoor 2025-03-18 21:55:55 +00:00 committed by Harness
parent 1cfdf10e08
commit 8e925410fb
19 changed files with 1016 additions and 101 deletions

View File

@ -81,6 +81,10 @@
} }
} }
.codeCloseBtn {
cursor: pointer !important;
}
.targetContainer { .targetContainer {
:global(.bp3-form-group) { :global(.bp3-form-group) {
margin-bottom: unset !important; margin-bottom: unset !important;
@ -187,3 +191,14 @@
flex-wrap: wrap; flex-wrap: wrap;
max-width: calc(100% - 100px) !important; 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;
}

View File

@ -22,6 +22,8 @@ export declare const checkboxLabel: string
export declare const checkboxText: string export declare const checkboxText: string
export declare const checkContainer: string export declare const checkContainer: string
export declare const codeClose: string export declare const codeClose: string
export declare const codeCloseBtn: string
export declare const defaultReviewerContainer: string
export declare const dividerContainer: string export declare const dividerContainer: string
export declare const generalContainer: string export declare const generalContainer: string
export declare const greyButton: string export declare const greyButton: string
@ -35,6 +37,7 @@ export declare const minText: string
export declare const noData: string export declare const noData: string
export declare const paddingTop: string export declare const paddingTop: string
export declare const popover: string export declare const popover: string
export declare const reviewerBlock: string
export declare const row: string export declare const row: string
export declare const statusWidthContainer: string export declare const statusWidthContainer: string
export declare const table: string export declare const table: string

View File

@ -35,7 +35,14 @@ import { Menu, PopoverPosition } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons' import { Icon } from '@harnessio/icons'
import { useHistory } from 'react-router-dom' import { useHistory } from 'react-router-dom'
import { useGet, useMutate } from 'restful-react' 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 { useStrings } from 'framework/strings'
import { import {
LabelsPageScope, 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 [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 = () => const getUpdateChecksPath = () =>
currentRule?.scope === 0 && repoMetadata currentRule?.scope === 0 && repoMetadata
? `/repos/${repoMetadata?.path}/+/checks/recent` ? `/repos/${repoMetadata?.path}/+/checks/recent`
@ -173,6 +189,20 @@ const BranchProtectionForm = (props: {
[principals] [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) => { const handleSubmit = async (operation: Promise<OpenapiRule>, successMessage: string, resetForm: () => void) => {
try { try {
await operation await operation
@ -204,7 +234,10 @@ const BranchProtectionForm = (props: {
const initialValues = useMemo((): RulesFormPayload => { const initialValues = useMemo((): RulesFormPayload => {
if (editMode && rule) { if (editMode && rule) {
const minReviewerCheck = 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( const isMergePresent = (rule.definition as ProtectionBranch)?.pullreq?.merge?.strategies_allowed?.includes(
MergeStrategy.MERGE MergeStrategy.MERGE
) )
@ -224,12 +257,19 @@ const BranchProtectionForm = (props: {
const includeArr = includeList?.map((arr: string) => ['include', arr]) const includeArr = includeList?.map((arr: string) => ['include', arr])
const excludeArr = excludeList?.map((arr: string) => ['exclude', arr]) const excludeArr = excludeList?.map((arr: string) => ['exclude', arr])
const finalArray = [...includeArr, ...excludeArr] const finalArray = [...includeArr, ...excludeArr]
const usersArray = transformDataToArray(rule.users) const usersArray = transformDataToArray(bypassListUsers || [])
const bypassList = const bypassList =
userArrayState.length > 0 userArrayState.length > 0
? userArrayState ? userArrayState
: usersArray?.map(user => `${user.id} ${user.display_name} (${user.email})`) : 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 { return {
name: rule?.identifier, name: rule?.identifier,
desc: rule.description, desc: rule.description,
@ -239,10 +279,16 @@ const BranchProtectionForm = (props: {
targetList: finalArray, targetList: finalArray,
allRepoOwners: (rule.definition as ProtectionBranch)?.bypass?.repo_owners, allRepoOwners: (rule.definition as ProtectionBranch)?.bypass?.repo_owners,
bypassList: bypassList, bypassList: bypassList,
defaultReviewersEnabled: (rule.definition as any)?.pullreq?.reviewers?.default_reviewer_ids?.length > 0,
defaultReviewersList: defaultReviewersList,
requireMinReviewers: minReviewerCheck, requireMinReviewers: minReviewerCheck,
requireMinDefaultReviewers: minDefaultReviewerCheck,
minReviewers: minReviewerCheck minReviewers: minReviewerCheck
? (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count ? (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, autoAddCodeOwner: (rule.definition as ProtectionBranch)?.pullreq?.reviewers?.request_code_owners,
requireCodeOwner: (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_code_owners, requireCodeOwner: (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_code_owners,
requireNewChanges: (rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_latest_commit, 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)?.lifecycle?.update_forbidden &&
!(rule.definition as ProtectionBranch)?.pullreq?.merge?.block, !(rule.definition as ProtectionBranch)?.pullreq?.merge?.block,
targetSet: false, targetSet: false,
bypassSet: false bypassSet: false,
defaultReviewersSet: false
} }
} }
@ -280,6 +327,14 @@ const BranchProtectionForm = (props: {
getEditPermissionRequestFromScope(space, currentRule?.scope ?? 0, repoMetadata), getEditPermissionRequestFromScope(space, currentRule?.scope ?? 0, repoMetadata),
[space, currentRule?.scope, repoMetadata] [space, currentRule?.scope, repoMetadata]
) )
const defaultReviewerProps = {
setSearchTerm,
userPrincipalOptions,
settingSectionMode,
setDefaultReviewersState
}
return ( return (
<Formik<RulesFormPayload> <Formik<RulesFormPayload>
formName="branchProtectionRulesNewEditForm" formName="branchProtectionRulesNewEditForm"
@ -287,7 +342,34 @@ const BranchProtectionForm = (props: {
enableReinitialize enableReinitialize
validationSchema={yup.object().shape({ validationSchema={yup.object().shape({
name: yup.string().trim().required().matches(REGEX_VALID_REPO_NAME, getString('validation.nameLogic')), 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 }) => { onSubmit={async (formData, { resetForm }) => {
const stratArray = [ const stratArray = [
@ -302,6 +384,7 @@ const BranchProtectionForm = (props: {
formData?.targetList?.filter(([type]) => type === 'exclude').map(([, value]) => value) ?? [] formData?.targetList?.filter(([type]) => type === 'exclude').map(([, value]) => value) ?? []
const bypassList = formData?.bypassList?.map(item => parseInt(item.split(' ')[0])) const bypassList = formData?.bypassList?.map(item => parseInt(item.split(' ')[0]))
const defaultReviewersList = formData?.defaultReviewersList?.map(item => parseInt(item.split(' ')[0]))
const payload: OpenapiRule = { const payload: OpenapiRule = {
identifier: formData.name, identifier: formData.name,
type: 'branch', type: 'branch',
@ -321,11 +404,13 @@ const BranchProtectionForm = (props: {
approvals: { approvals: {
require_code_owners: formData.requireCodeOwner, require_code_owners: formData.requireCodeOwner,
require_minimum_count: parseInt(formData.minReviewers as string), require_minimum_count: parseInt(formData.minReviewers as string),
require_minimum_default_reviewer_count: parseInt(formData.minDefaultReviewers as string),
require_latest_commit: formData.requireNewChanges, require_latest_commit: formData.requireNewChanges,
require_no_change_request: formData.reqResOfChanges require_no_change_request: formData.reqResOfChanges
}, },
reviewers: { reviewers: {
request_code_owners: formData.autoAddCodeOwner request_code_owners: formData.autoAddCodeOwner,
default_reviewer_ids: defaultReviewersList
}, },
comments: { comments: {
require_resolve_all: formData.requireCommentResolution require_resolve_all: formData.requireCommentResolution
@ -356,6 +441,9 @@ const BranchProtectionForm = (props: {
if (!formData.requireMinReviewers) { if (!formData.requireMinReviewers) {
delete (payload?.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count 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) { if (editMode) {
handleSubmit(updateRule(payload), getString('branchProtection.ruleUpdated'), resetForm) handleSubmit(updateRule(payload), getString('branchProtection.ruleUpdated'), resetForm)
} else { } else {
@ -549,6 +637,7 @@ const BranchProtectionForm = (props: {
statusChecks={statusChecks} statusChecks={statusChecks}
limitMergeStrats={limitMergeStrats} limitMergeStrats={limitMergeStrats}
setSearchStatusTerm={setSearchStatusTerm} setSearchStatusTerm={setSearchStatusTerm}
defaultReviewerProps={defaultReviewerProps}
/> />
<Container padding={{ top: 'large' }}> <Container padding={{ top: 'large' }}>
<Layout.Horizontal spacing="small"> <Layout.Horizontal spacing="small">

View File

@ -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

View File

@ -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

View File

@ -22,6 +22,8 @@ import type { FormikProps } from 'formik'
import { Classes, Popover, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core' import { Classes, Popover, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import type { RulesFormPayload } from 'utils/Utils' import type { RulesFormPayload } from 'utils/Utils'
import type { SettingTypeMode } from 'utils/GitUtils'
import DefaultReviewersSection from './DefaultReviewersSection'
import css from '../BranchProtectionForm.module.scss' import css from '../BranchProtectionForm.module.scss'
const ProtectionRulesForm = (props: { const ProtectionRulesForm = (props: {
@ -29,9 +31,15 @@ const ProtectionRulesForm = (props: {
minReviewers: boolean minReviewers: boolean
statusOptions: SelectOption[] statusOptions: SelectOption[]
statusChecks: string[] statusChecks: string[]
limitMergeStrats: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any limitMergeStrats: boolean
setSearchStatusTerm: React.Dispatch<React.SetStateAction<string>> setSearchStatusTerm: React.Dispatch<React.SetStateAction<string>>
formik: FormikProps<RulesFormPayload> formik: FormikProps<RulesFormPayload>
defaultReviewerProps: {
setSearchTerm: React.Dispatch<React.SetStateAction<string>>
userPrincipalOptions: SelectOption[]
settingSectionMode: SettingTypeMode
setDefaultReviewersState: React.Dispatch<React.SetStateAction<string[]>>
}
}) => { }) => {
const { const {
statusChecks, statusChecks,
@ -40,7 +48,8 @@ const ProtectionRulesForm = (props: {
requireStatusChecks, requireStatusChecks,
statusOptions, statusOptions,
limitMergeStrats, limitMergeStrats,
formik formik,
defaultReviewerProps
} = props } = props
const { getString } = useStrings() const { getString } = useStrings()
const setFieldValue = formik.setFieldValue const setFieldValue = formik.setFieldValue
@ -143,6 +152,7 @@ const ProtectionRulesForm = (props: {
</Popover> </Popover>
<hr className={css.dividerContainer} /> <hr className={css.dividerContainer} />
<DefaultReviewersSection formik={formik} defaultReviewerProps={defaultReviewerProps} />
<FormInput.CheckBox <FormInput.CheckBox
className={css.checkboxLabel} className={css.checkboxLabel}
label={getString('branchProtection.requireMinReviewersTitle')} label={getString('branchProtection.requireMinReviewersTitle')}

View File

@ -219,6 +219,18 @@ const BranchProtectionListing = (props: {
requiredRule: { requiredRule: {
[RuleFields.AUTO_ADD_CODE_OWNERS]: true [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
}
} }
} }

View File

@ -69,6 +69,8 @@ export interface StringsMap {
'branchProtection.addCodeownersToReviewText': string 'branchProtection.addCodeownersToReviewText': string
'branchProtection.addCodeownersToReviewTitle': string 'branchProtection.addCodeownersToReviewTitle': string
'branchProtection.allRepoOwners': string 'branchProtection.allRepoOwners': string
'branchProtection.atLeastMinReviewer': string
'branchProtection.atLeastMinReviewers': string
'branchProtection.autoDeleteText': string 'branchProtection.autoDeleteText': string
'branchProtection.autoDeleteTitle': string 'branchProtection.autoDeleteTitle': string
'branchProtection.blockBranchCreation': string 'branchProtection.blockBranchCreation': string
@ -102,6 +104,8 @@ export interface StringsMap {
'branchProtection.disableTheRule': string 'branchProtection.disableTheRule': string
'branchProtection.edit': string 'branchProtection.edit': string
'branchProtection.editRule': string 'branchProtection.editRule': string
'branchProtection.enableDefaultReviewersText': string
'branchProtection.enableDefaultReviewersTitle': string
'branchProtection.enableTheRule': string 'branchProtection.enableTheRule': string
'branchProtection.limitMergeStrategies': string 'branchProtection.limitMergeStrategies': string
'branchProtection.limitMergeStrategiesText': string 'branchProtection.limitMergeStrategiesText': string
@ -124,6 +128,8 @@ export interface StringsMap {
'branchProtection.reqReviewFromCodeOwnerTitle': string 'branchProtection.reqReviewFromCodeOwnerTitle': string
'branchProtection.reqStatusChecksText': string 'branchProtection.reqStatusChecksText': string
'branchProtection.reqStatusChecksTitle': string 'branchProtection.reqStatusChecksTitle': string
'branchProtection.requireMinDefaultReviewersContent': string
'branchProtection.requireMinDefaultReviewersTitle': string
'branchProtection.requireMinReviewersContent': string 'branchProtection.requireMinReviewersContent': string
'branchProtection.requireMinReviewersTitle': string 'branchProtection.requireMinReviewersTitle': string
'branchProtection.requirePr': string 'branchProtection.requirePr': string
@ -170,24 +176,31 @@ export interface StringsMap {
'changesSection.changesApproved': string 'changesSection.changesApproved': string
'changesSection.changesApprovedByXReviewers': string 'changesSection.changesApprovedByXReviewers': string
'changesSection.changesWereAppByCodeOwner': string 'changesSection.changesWereAppByCodeOwner': string
'changesSection.changesWereAppByDefaultReviewers': string
'changesSection.changesWereAppByLatestReqRev': string 'changesSection.changesWereAppByLatestReqRev': string
'changesSection.codeOwnerReqChanges': string 'changesSection.codeOwnerReqChanges': string
'changesSection.codeOwnerReqChangesToPr': string 'changesSection.codeOwnerReqChangesToPr': string
'changesSection.defaultReviewersChangesToPr': string
'changesSection.defaultReviewersStatus': string
'changesSection.latestChangesApprovedByXReviewers': string 'changesSection.latestChangesApprovedByXReviewers': string
'changesSection.latestChangesPendingReqRev': string 'changesSection.latestChangesPendingReqRev': string
'changesSection.latestChangesWereAppByCodeOwner': string 'changesSection.latestChangesWereAppByCodeOwner': string
'changesSection.latestChangesWereAppByDefaultReviewers': string
'changesSection.latestChangesWereApprovedByReq': string 'changesSection.latestChangesWereApprovedByReq': string
'changesSection.noCodeOwnerReviewsReq': string 'changesSection.noCodeOwnerReviewsReq': string
'changesSection.noReviewsReq': string 'changesSection.noReviewsReq': string
'changesSection.pendingAppFromCodeOwners': string 'changesSection.pendingAppFromCodeOwners': string
'changesSection.pendingLatestApprovalCodeOwners': string 'changesSection.pendingLatestApprovalCodeOwners': string
'changesSection.pendingLatestApprovalDefaultReviewers': string
'changesSection.prMergeBlockedMessage': string 'changesSection.prMergeBlockedMessage': string
'changesSection.prMergeBlockedTitle': string 'changesSection.prMergeBlockedTitle': string
'changesSection.pullReqWithoutAnyReviews': string 'changesSection.pullReqWithoutAnyReviews': string
'changesSection.reqChangeFromCodeOwners': string 'changesSection.reqChangeFromCodeOwners': string
'changesSection.someChangesWereAppByCodeOwner': string 'changesSection.someChangesWereAppByCodeOwner': string
'changesSection.waitingOnCodeOwner': string 'changesSection.waitingOnCodeOwner': string
'changesSection.waitingOnDefaultReviewers': string
'changesSection.waitingOnLatestCodeOwner': string 'changesSection.waitingOnLatestCodeOwner': string
'changesSection.waitingOnLatestDefaultReviewers': string
'changesSection.waitingOnReviewers': string 'changesSection.waitingOnReviewers': string
'changesSection.xApprovalsArePending': string 'changesSection.xApprovalsArePending': string
characterLimit: string characterLimit: string
@ -309,6 +322,7 @@ export interface StringsMap {
dangerDeleteProject: string dangerDeleteProject: string
defaultBranch: string defaultBranch: string
defaultBranchTitle: string defaultBranchTitle: string
defaultReviewers: string
delete: string delete: string
deleteBranch: string deleteBranch: string
deleteBranchConfirm: string deleteBranchConfirm: string
@ -673,6 +687,7 @@ export interface StringsMap {
ok: string ok: string
on: string on: string
onDate: string onDate: string
onLatestChanges: string
oneMustBeSelected: string oneMustBeSelected: string
open: string open: string
optional: string optional: string
@ -1036,6 +1051,7 @@ export interface StringsMap {
selectMergeStrat: string selectMergeStrat: string
selectRange: string selectRange: string
selectRepositoryPlaceholder: string selectRepositoryPlaceholder: string
selectReviewers: string
selectSpace: string selectSpace: string
selectSpaceText: string selectSpaceText: string
selectStatuses: string selectStatuses: string

View File

@ -1079,6 +1079,12 @@ branchProtection:
commitDirectlyBlockText: Some rules don't allow you to commit directly commitDirectlyBlockText: Some rules don't allow you to commit directly
commitDirectlyAlertText: Some rules will be bypassed to commit directly commitDirectlyAlertText: Some rules will be bypassed to commit directly
commitDirectlyAlertBtn: Bypass rules and 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: codeOwner:
title: Code Owner title: Code Owner
changesRequested: '{count} {count|1:change,changes} requested' changesRequested: '{count} {count|1:change,changes} requested'
@ -1089,6 +1095,7 @@ approved: Approved
comingSoon: Coming soon... comingSoon: Coming soon...
enterANumber: Enter a number enterANumber: Enter a number
selectUsers: Select Users selectUsers: Select Users
selectReviewers: Select Reviewers
selectUsersAndServiceAcc: Select Users and Service Accounts selectUsersAndServiceAcc: Select Users and Service Accounts
selectStatuses: Select Statuses selectStatuses: Select Statuses
featureRoadmap: Feature Roadmap featureRoadmap: Feature Roadmap
@ -1150,8 +1157,11 @@ changesSection:
pendingAppFromCodeOwners: Approvals pending from code owners pendingAppFromCodeOwners: Approvals pending from code owners
pendingLatestApprovalCodeOwners: Latest changes are pending approval from code owners pendingLatestApprovalCodeOwners: Latest changes are pending approval from code owners
waitingOnCodeOwner: 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 waitingOnReviewers: Changes are pending approval from required reviewers
waitingOnLatestCodeOwner: Waiting on code owner reviews of latest changes waitingOnLatestCodeOwner: Waiting on code owner reviews of latest changes
waitingOnLatestDefaultReviewers: Waiting on default reviewer's reviews of latest changes
approvalPending: Approvals pending approvalPending: Approvals pending
changesApproved: Changes approved changesApproved: Changes approved
noReviewsReq: No reviews required noReviewsReq: No reviews required
@ -1161,13 +1171,17 @@ changesSection:
changesApprovedByXReviewers: Changes were approved by {length} {length|1:reviewer,reviewers} changesApprovedByXReviewers: Changes were approved by {length} {length|1:reviewer,reviewers}
latestChangesApprovedByXReviewers: Latest 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 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 latestChangesPendingReqRev: Latest changes are pending approval from required reviewers
changesWereAppByCodeOwner: Changes were approved by code owners 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 changesWereAppByLatestReqRev: Changes were approved by required reviewers
latestChangesWereApprovedByReq: Latest changes were approved by required reviewers latestChangesWereApprovedByReq: Latest changes were approved by required reviewers
someChangesWereAppByCodeOwner: Some changes were approved by code owners someChangesWereAppByCodeOwner: Some changes were approved by code owners
xApprovalsArePending: '{approved}/{min} approvals are pending' xApprovalsArePending: '{approved}/{min} approvals are pending'
codeOwnerReqChangesToPr: Code owners requested changes to the pull request codeOwnerReqChangesToPr: Code owners requested changes to the pull request
defaultReviewersChangesToPr: Default reviewers requested changes to the pull request
checkSection: checkSection:
someReqChecksFailed: Some required checks have failed someReqChecksFailed: Some required checks have failed
someReqChecksPending: Some required checks are pending someReqChecksPending: Some required checks are pending
@ -1203,6 +1217,7 @@ checklist: Check list
code: Code code: Code
approvedBy: Approved By approvedBy: Approved By
ownersHeading: OWNERS ownersHeading: OWNERS
defaultReviewers: DEFAULT REVIEWERS
changesRequestedBy: Changes Requested By changesRequestedBy: Changes Requested By
changesRequested: changesRequested changesRequested: changesRequested
owners: Owners owners: Owners
@ -1322,3 +1337,4 @@ rebase: Rebase
rebaseBranch: Rebase branch rebaseBranch: Rebase branch
updateWithRebase: Update with rebase updateWithRebase: Update with rebase
updatedBranchMessageRebase: Updated branch with rebase updatedBranchMessageRebase: Updated branch with rebase
onLatestChanges: on latest changes

View File

@ -104,7 +104,7 @@ export function CodeOwnersOverview({
return codeOwners?.evaluation_entries?.length ? ( return codeOwners?.evaluation_entries?.length ? (
<Container <Container
className={cx(css.main, { [css.codeOwner]: !standalone })} 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}> style={{ '--border-color': Utils.getRealCSSColor(borderColor) } as React.CSSProperties}>
<Match expr={isExpanded}> <Match expr={isExpanded}>
<Truthy> <Truthy>
@ -165,7 +165,7 @@ export const CodeOwnerSection: React.FC<CodeOwnerSectionsProps> = ({
[ [
{ {
id: 'CODE', id: 'CODE',
width: '45%', width: '40%',
sort: true, sort: true,
Header: getString('code'), Header: getString('code'),
accessor: 'CODE', accessor: 'CODE',
@ -183,7 +183,7 @@ export const CodeOwnerSection: React.FC<CodeOwnerSectionsProps> = ({
}, },
{ {
id: 'Owners', id: 'Owners',
width: '13%', width: '18%',
sort: true, sort: true,
Header: getString('ownersHeading'), Header: getString('ownersHeading'),
accessor: 'OWNERS', accessor: 'OWNERS',

View File

@ -27,7 +27,8 @@ import type {
TypesPullReqReviewer, TypesPullReqReviewer,
RepoRepositoryOutput, RepoRepositoryOutput,
TypesRuleViolations, TypesRuleViolations,
TypesBranchExtended TypesBranchExtended,
TypesDefaultReviewerApprovalsResponse
} from 'services/code' } from 'services/code'
import { import {
PRMergeOption, PRMergeOption,
@ -103,6 +104,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
const [reqCodeOwnerApproval, setReqCodeOwnerApproval] = useState(false) const [reqCodeOwnerApproval, setReqCodeOwnerApproval] = useState(false)
const [minApproval, setMinApproval] = useState(0) const [minApproval, setMinApproval] = useState(0)
const [reqCodeOwnerLatestApproval, setReqCodeOwnerLatestApproval] = useState(false) const [reqCodeOwnerLatestApproval, setReqCodeOwnerLatestApproval] = useState(false)
const [defaultReviewersInfoSet, setDefaultReviewersInfoSet] = useState<TypesDefaultReviewerApprovalsResponse[]>([])
const [minReqLatestApproval, setMinReqLatestApproval] = useState(0) const [minReqLatestApproval, setMinReqLatestApproval] = useState(0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [resolvedCommentArr, setResolvedCommentArr] = useState<any>() const [resolvedCommentArr, setResolvedCommentArr] = useState<any>()
@ -234,7 +236,8 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
setMinApproval, setMinApproval,
setReqCodeOwnerLatestApproval, setReqCodeOwnerLatestApproval,
setMinReqLatestApproval, setMinReqLatestApproval,
setPRStateLoading setPRStateLoading,
setDefaultReviewersInfoSet
) // eslint-disable-next-line react-hooks/exhaustive-deps ) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [unchecked, pullReqMetadata?.source_sha, activities]) }, [unchecked, pullReqMetadata?.source_sha, activities])
@ -295,6 +298,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval} reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval}
refetchCodeOwners={refetchCodeOwners} refetchCodeOwners={refetchCodeOwners}
mergeBlockedRule={mergeBlockedRule} mergeBlockedRule={mergeBlockedRule}
defaultReviewersInfoSet={defaultReviewersInfoSet}
/> />
</Render> </Render>
), ),

View File

@ -30,7 +30,7 @@ import { Render } from 'react-jsx-match'
import { isEmpty } from 'lodash-es' import { isEmpty } from 'lodash-es'
import type { IconName } from '@blueprintjs/core' import type { IconName } from '@blueprintjs/core'
import { Icon } from '@harnessio/icons' 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 { CodeOwnerSection } from 'pages/PullRequest/CodeOwners/CodeOwnersOverview'
import { useStrings } from 'framework/strings' import { useStrings } from 'framework/strings'
import type { import type {
@ -38,10 +38,12 @@ import type {
TypesCodeOwnerEvaluationEntry, TypesCodeOwnerEvaluationEntry,
TypesPullReq, TypesPullReq,
TypesPullReqReviewer, TypesPullReqReviewer,
RepoRepositoryOutput RepoRepositoryOutput,
TypesDefaultReviewerApprovalsResponse
} from 'services/code' } from 'services/code'
import { capitalizeFirstLetter } from 'pages/PullRequest/Checks/ChecksUtils' 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 greyCircle from '../../../../../icons/greyCircle.svg?url'
import emptyStatus from '../../../../../icons/emptyStatus.svg?url' import emptyStatus from '../../../../../icons/emptyStatus.svg?url'
import Success from '../../../../../icons/code-success.svg?url' import Success from '../../../../../icons/code-success.svg?url'
@ -58,6 +60,7 @@ interface ChangesSectionProps {
reqCodeOwnerApproval: boolean reqCodeOwnerApproval: boolean
minApproval: number minApproval: number
reviewers: TypesPullReqReviewer[] | null reviewers: TypesPullReqReviewer[] | null
defaultReviewersInfoSet: TypesDefaultReviewerApprovalsResponse[]
minReqLatestApproval: number minReqLatestApproval: number
reqCodeOwnerLatestApproval: boolean reqCodeOwnerLatestApproval: boolean
mergeBlockedRule: boolean mergeBlockedRule: boolean
@ -69,6 +72,7 @@ interface ChangesSectionProps {
const ChangesSection = (props: ChangesSectionProps) => { const ChangesSection = (props: ChangesSectionProps) => {
const { const {
reviewers: currReviewers, reviewers: currReviewers,
defaultReviewersInfoSet,
minApproval, minApproval,
reqCodeOwnerApproval, reqCodeOwnerApproval,
repoMetadata, repoMetadata,
@ -139,6 +143,18 @@ const ChangesSection = (props: ChangesSectionProps) => {
changeReqEvaluations[0].reviewer?.display_name || changeReqEvaluations[0].reviewer?.uid || '' changeReqEvaluations[0].reviewer?.display_name || changeReqEvaluations[0].reviewer?.uid || ''
) )
: 'Reviewer' : 'Reviewer'
const updatedDefaultApprovalRes = reviewers
? defaultReviewerResponseWithDecision(defaultReviewersInfoSet, reviewers)
: defaultReviewersInfoSet
const {
defReviewerApprovalRequiredByRule,
defReviewerLatestApprovalRequiredByRule,
defReviewerApprovedLatestChanges,
defReviewerApprovedChanges,
changesRequestedByDefReviewersArr
} = getUnifiedDefaultReviewersState(updatedDefaultApprovalRes)
const extractInfoForCodeOwnerContent = () => { const extractInfoForCodeOwnerContent = () => {
let statusMessage = '' let statusMessage = ''
let statusColor = 'grey' // Default color for no rules required let statusColor = 'grey' // Default color for no rules required
@ -151,6 +167,8 @@ const ChangesSection = (props: ChangesSectionProps) => {
minApproval > 0 || minApproval > 0 ||
reqCodeOwnerLatestApproval || reqCodeOwnerLatestApproval ||
minReqLatestApproval > 0 || minReqLatestApproval > 0 ||
defReviewerApprovalRequiredByRule ||
defReviewerLatestApprovalRequiredByRule ||
mergeBlockedRule mergeBlockedRule
) { ) {
if (mergeBlockedRule) { if (mergeBlockedRule) {
@ -183,6 +201,14 @@ const ChangesSection = (props: ChangesSectionProps) => {
statusMessage = getString('changesSection.latestChangesPendingReqRev') statusMessage = getString('changesSection.latestChangesPendingReqRev')
statusColor = Color.ORANGE_500 statusColor = Color.ORANGE_500
statusIcon = 'execution-waiting' 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) { } else if (approvedEvaluations && approvedEvaluations?.length < minApproval && minApproval > 0) {
title = getString('changesSection.approvalPending') title = getString('changesSection.approvalPending')
statusMessage = stringSubstitute(getString('changesSection.waitingOnReviewers'), { statusMessage = stringSubstitute(getString('changesSection.waitingOnReviewers'), {
@ -190,6 +216,15 @@ const ChangesSection = (props: ChangesSectionProps) => {
total: minApproval total: minApproval
}) as string }) 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 statusColor = Color.ORANGE_500
statusIcon = 'execution-waiting' statusIcon = 'execution-waiting'
} else if (reqCodeOwnerLatestApproval && latestCodeOwnerApprovalArr?.length > 0) { } else if (reqCodeOwnerLatestApproval && latestCodeOwnerApprovalArr?.length > 0) {
@ -384,6 +419,84 @@ const ChangesSection = (props: ChangesSectionProps) => {
</Layout.Horizontal> </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 = const viewBtn =
!mergeBlockedRule && !mergeBlockedRule &&
(minApproval > minReqLatestApproval || (minApproval > minReqLatestApproval ||
@ -573,6 +686,56 @@ const ChangesSection = (props: ChangesSectionProps) => {
</Layout.Horizontal> </Layout.Horizontal>
</Container> </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) && ( {!isEmpty(codeOwners) && !isEmpty(codeOwners.evaluation_entries) && (
<Container className={css.borderContainer} padding={{ left: 'xlarge', right: 'small' }}> <Container className={css.borderContainer} padding={{ left: 'xlarge', right: 'small' }}>
<Layout.Horizontal className={css.paddingContainer} flex={{ justifyContent: 'space-between' }}> <Layout.Horizontal className={css.paddingContainer} flex={{ justifyContent: 'space-between' }}>
@ -604,7 +767,6 @@ const ChangesSection = (props: ChangesSectionProps) => {
</Layout.Horizontal> </Layout.Horizontal>
</Container> </Container>
)} )}
</Container>
{codeOwners && !isEmpty(codeOwners?.evaluation_entries) && ( {codeOwners && !isEmpty(codeOwners?.evaluation_entries) && (
<Container <Container
className={css.codeOwnerContainer} className={css.codeOwnerContainer}
@ -617,6 +779,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
/> />
</Container> </Container>
)} )}
</Container>
</Render> </Render>
</Render> </Render>
) )

View File

@ -50,29 +50,27 @@ interface ReviewerAddActivityPayload {
} }
const formatListWithAndFragment = (names: string[]): React.ReactNode => { const formatListWithAndFragment = (names: string[]): React.ReactNode => {
const uniqueNames = [...new Set(names)] // Ensure uniqueness switch (names.length) {
switch (uniqueNames.length) {
case 0: case 0:
return null return null
case 1: case 1:
return <strong>{uniqueNames[0]}</strong> return <strong>{names[0]}</strong>
case 2: case 2:
return ( return (
<> <>
<strong>{uniqueNames[0]}</strong> and <strong>{uniqueNames[1]}</strong> <strong>{names[0]}</strong> and <strong>{names[1]}</strong>
</> </>
) )
default: default:
return ( return (
<> <>
{uniqueNames.slice(0, -1).map((name, index) => ( {names.slice(0, -1).map((name, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<strong>{name}</strong> <strong>{name}</strong>
{index < uniqueNames.length - 2 ? ', ' : ''} {index < names.length - 2 ? ', ' : ''}
</React.Fragment> </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 { routes } = useAppContext()
const displayNameList = useMemo(() => { const displayNameList = useMemo(() => {
const checkList = payload?.metadata?.mentions?.ids ?? [] const checkList = payload?.metadata?.mentions?.ids ?? []
const uniqueList = [...new Set(checkList)]
const mentionsMap = payload?.mentions ?? {} 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]) }, [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) { switch (type) {
case CommentType.MERGE: { case CommentType.MERGE: {
return ( return (
@ -461,6 +467,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
case CommentType.REVIEWER_ADD: { case CommentType.REVIEWER_ADD: {
const activityMentions = formatListWithAndFragment(displayNameList) const activityMentions = formatListWithAndFragment(displayNameList)
const principalMentions = formatListWithAndFragment(principalNameList)
return ( return (
<Container className={css.mergedBox}> <Container className={css.mergedBox}>
@ -499,7 +506,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
str={getString('prReview.codeowners')} str={getString('prReview.codeowners')}
vars={{ vars={{
author: <strong>{payload?.author?.display_name}</strong>, author: <strong>{payload?.author?.display_name}</strong>,
codeowners: activityMentions codeowners: principalMentions
}} }}
/> />
</Case> </Case>
@ -508,7 +515,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
str={getString('prReview.defaultReviewers')} str={getString('prReview.defaultReviewers')}
vars={{ vars={{
author: <strong>{payload?.author?.display_name}</strong>, author: <strong>{payload?.author?.display_name}</strong>,
reviewers: activityMentions reviewers: principalMentions
}} }}
/> />
</Case> </Case>

View File

@ -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>
)
}

View File

@ -23,9 +23,12 @@ import type {
EnumMergeMethod, EnumMergeMethod,
EnumPullReqReviewDecision, EnumPullReqReviewDecision,
TypesCodeOwnerEvaluationEntry, TypesCodeOwnerEvaluationEntry,
TypesDefaultReviewerApprovalsResponse,
TypesOwnerEvaluation, TypesOwnerEvaluation,
TypesPrincipalInfo,
TypesPullReq, TypesPullReq,
TypesPullReqActivity TypesPullReqActivity,
TypesPullReqReviewer
} from 'services/code' } from 'services/code'
export interface PRMergeOption extends SelectOption { export interface PRMergeOption extends SelectOption {
@ -95,7 +98,7 @@ export const findWaitingDecisions = (
const hasApprovedDecision = entry?.owner_evaluations?.some( const hasApprovedDecision = entry?.owner_evaluations?.some(
evaluation => evaluation =>
evaluation.review_decision === PullReqReviewDecision.APPROVED && evaluation.review_decision === PullReqReviewDecision.APPROVED &&
(reqCodeOwnerLatestApproval ? evaluation.review_sha === pullReqMetadata.source_sha : true) (reqCodeOwnerLatestApproval ? evaluation.review_sha === pullReqMetadata?.source_sha : true)
) )
return !hasApprovedDecision return !hasApprovedDecision
}) })
@ -176,3 +179,41 @@ export const getMergeOptions = (getString: UseStringsReturn['getString'], mergea
value: 'close' 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
}
})
}

View File

@ -145,7 +145,7 @@ export type EnumPullReqCommentStatus = 'active' | 'resolved'
export type EnumPullReqReviewDecision = 'approved' | 'changereq' | 'pending' | 'reviewed' 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' export type EnumPullReqState = 'closed' | 'merged' | 'open'
@ -183,11 +183,11 @@ export type EnumWebhookTrigger =
| 'pullreq_comment_created' | 'pullreq_comment_created'
| 'pullreq_comment_status_updated' | 'pullreq_comment_status_updated'
| 'pullreq_comment_updated' | 'pullreq_comment_updated'
| 'pullreq_review_submitted'
| 'pullreq_created' | 'pullreq_created'
| 'pullreq_label_assigned' | 'pullreq_label_assigned'
| 'pullreq_merged' | 'pullreq_merged'
| 'pullreq_reopened' | 'pullreq_reopened'
| 'pullreq_review_submitted'
| 'pullreq_updated' | 'pullreq_updated'
| 'tag_created' | 'tag_created'
| 'tag_deleted' | 'tag_deleted'
@ -344,6 +344,7 @@ export interface OpenapiCommentUpdatePullReqRequest {
export interface OpenapiCommitFilesRequest { export interface OpenapiCommitFilesRequest {
actions?: RepoCommitFileAction[] | null actions?: RepoCommitFileAction[] | null
author?: GitIdentity
branch?: string branch?: string
bypass_rules?: boolean bypass_rules?: boolean
dry_run_rules?: boolean dry_run_rules?: boolean
@ -738,6 +739,7 @@ export interface OpenapiWebhookType {
latest_execution_result?: EnumWebhookExecutionResult latest_execution_result?: EnumWebhookExecutionResult
parent_id?: number parent_id?: number
parent_type?: EnumWebhookParent parent_type?: EnumWebhookParent
scope?: number
triggers?: EnumWebhookTrigger[] | null triggers?: EnumWebhookTrigger[] | null
updated?: number updated?: number
url?: string url?: string
@ -754,6 +756,7 @@ export interface ProtectionDefApprovals {
require_code_owners?: boolean require_code_owners?: boolean
require_latest_commit?: boolean require_latest_commit?: boolean
require_minimum_count?: number require_minimum_count?: number
require_minimum_default_reviewer_count?: number
require_no_change_request?: boolean require_no_change_request?: boolean
} }
@ -1119,6 +1122,14 @@ export interface TypesCreateBranchOutput {
sha?: ShaSHA 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 { export interface TypesDeleteBranchOutput {
dry_run_rules?: boolean dry_run_rules?: boolean
rule_violations?: TypesRuleViolations[] rule_violations?: TypesRuleViolations[]
@ -1358,6 +1369,7 @@ export interface TypesMergeResponse {
allowed_methods?: EnumMergeMethod[] allowed_methods?: EnumMergeMethod[]
branch_deleted?: boolean branch_deleted?: boolean
conflict_files?: string[] conflict_files?: string[]
default_reviewer_aprovals?: TypesDefaultReviewerApprovalsResponse[]
dry_run?: boolean dry_run?: boolean
dry_run_rules?: boolean dry_run_rules?: boolean
mergeable?: boolean mergeable?: boolean
@ -1676,6 +1688,7 @@ export interface TypesServiceAccount {
created?: number created?: number
display_name?: string display_name?: string
email?: string email?: string
id?: number
parent_id?: number parent_id?: number
parent_type?: EnumParentResourceType parent_type?: EnumParentResourceType
uid?: string uid?: string
@ -1801,8 +1814,10 @@ export interface TypesUser {
export interface TypesUserGroupInfo { export interface TypesUserGroupInfo {
description?: string description?: string
id?: number
identifier?: string identifier?: string
name?: string name?: string
scope?: number
} }
export interface TypesUserGroupOwnerEvaluation { export interface TypesUserGroupOwnerEvaluation {
@ -3761,7 +3776,7 @@ export interface ListRepoLabelsQueryParams {
*/ */
limit?: number limit?: number
/** /**
* The result should inherit labels from parent parent spaces. * The result should inherit entities from parent spaces.
*/ */
inherited?: boolean inherited?: boolean
/** /**
@ -6636,11 +6651,11 @@ export const useRuleList = ({ repo_ref, ...props }: UseRuleListProps) =>
{ base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props } { base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props }
) )
export interface RuleAddPathParams { export interface RepoRuleAddPathParams {
repo_ref: string repo_ref: string
} }
export interface RuleAddRequestBody { export interface RepoRuleAddRequestBody {
definition?: OpenapiRuleDefinition definition?: OpenapiRuleDefinition
description?: string description?: string
identifier?: string identifier?: string
@ -6650,14 +6665,14 @@ export interface RuleAddRequestBody {
uid?: string uid?: string
} }
export type RuleAddProps = Omit< export type RepoRuleAddProps = Omit<
MutateProps<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>, MutateProps<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>,
'path' | 'verb' 'path' | 'verb'
> & > &
RuleAddPathParams RepoRuleAddPathParams
export const RuleAdd = ({ repo_ref, ...props }: RuleAddProps) => ( export const RepoRuleAdd = ({ repo_ref, ...props }: RepoRuleAddProps) => (
<Mutate<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams> <Mutate<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>
verb="POST" verb="POST"
path={`/repos/${repo_ref}/rules`} path={`/repos/${repo_ref}/rules`}
base={getConfig('code/api/v1')} base={getConfig('code/api/v1')}
@ -6665,31 +6680,31 @@ export const RuleAdd = ({ repo_ref, ...props }: RuleAddProps) => (
/> />
) )
export type UseRuleAddProps = Omit< export type UseRepoRuleAddProps = Omit<
UseMutateProps<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>, UseMutateProps<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>,
'path' | 'verb' 'path' | 'verb'
> & > &
RuleAddPathParams RepoRuleAddPathParams
export const useRuleAdd = ({ repo_ref, ...props }: UseRuleAddProps) => export const useRepoRuleAdd = ({ repo_ref, ...props }: UseRepoRuleAddProps) =>
useMutate<OpenapiRule, UsererrorError, void, RuleAddRequestBody, RuleAddPathParams>( useMutate<OpenapiRule, UsererrorError, void, RepoRuleAddRequestBody, RepoRuleAddPathParams>(
'POST', 'POST',
(paramsInPath: RuleAddPathParams) => `/repos/${paramsInPath.repo_ref}/rules`, (paramsInPath: RepoRuleAddPathParams) => `/repos/${paramsInPath.repo_ref}/rules`,
{ base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props } { base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props }
) )
export interface RuleDeletePathParams { export interface RepoRuleDeletePathParams {
repo_ref: string repo_ref: string
} }
export type RuleDeleteProps = Omit< export type RepoRuleDeleteProps = Omit<
MutateProps<void, UsererrorError, void, string, RuleDeletePathParams>, MutateProps<void, UsererrorError, void, string, RepoRuleDeletePathParams>,
'path' | 'verb' 'path' | 'verb'
> & > &
RuleDeletePathParams RepoRuleDeletePathParams
export const RuleDelete = ({ repo_ref, ...props }: RuleDeleteProps) => ( export const RepoRuleDelete = ({ repo_ref, ...props }: RepoRuleDeleteProps) => (
<Mutate<void, UsererrorError, void, string, RuleDeletePathParams> <Mutate<void, UsererrorError, void, string, RepoRuleDeletePathParams>
verb="DELETE" verb="DELETE"
path={`/repos/${repo_ref}/rules`} path={`/repos/${repo_ref}/rules`}
base={getConfig('code/api/v1')} base={getConfig('code/api/v1')}
@ -6697,50 +6712,50 @@ export const RuleDelete = ({ repo_ref, ...props }: RuleDeleteProps) => (
/> />
) )
export type UseRuleDeleteProps = Omit< export type UseRepoRuleDeleteProps = Omit<
UseMutateProps<void, UsererrorError, void, string, RuleDeletePathParams>, UseMutateProps<void, UsererrorError, void, string, RepoRuleDeletePathParams>,
'path' | 'verb' 'path' | 'verb'
> & > &
RuleDeletePathParams RepoRuleDeletePathParams
export const useRuleDelete = ({ repo_ref, ...props }: UseRuleDeleteProps) => export const useRepoRuleDelete = ({ repo_ref, ...props }: UseRepoRuleDeleteProps) =>
useMutate<void, UsererrorError, void, string, RuleDeletePathParams>( useMutate<void, UsererrorError, void, string, RepoRuleDeletePathParams>(
'DELETE', 'DELETE',
(paramsInPath: RuleDeletePathParams) => `/repos/${paramsInPath.repo_ref}/rules`, (paramsInPath: RepoRuleDeletePathParams) => `/repos/${paramsInPath.repo_ref}/rules`,
{ base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props } { base: getConfig('code/api/v1'), pathParams: { repo_ref }, ...props }
) )
export interface RuleGetPathParams { export interface RepoRuleGetPathParams {
repo_ref: string repo_ref: string
rule_identifier: string rule_identifier: string
} }
export type RuleGetProps = Omit<GetProps<OpenapiRule, UsererrorError, void, RuleGetPathParams>, 'path'> & export type RepoRuleGetProps = Omit<GetProps<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>, 'path'> &
RuleGetPathParams RepoRuleGetPathParams
export const RuleGet = ({ repo_ref, rule_identifier, ...props }: RuleGetProps) => ( export const RepoRuleGet = ({ repo_ref, rule_identifier, ...props }: RepoRuleGetProps) => (
<Get<OpenapiRule, UsererrorError, void, RuleGetPathParams> <Get<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>
path={`/repos/${repo_ref}/rules/${rule_identifier}`} path={`/repos/${repo_ref}/rules/${rule_identifier}`}
base={getConfig('code/api/v1')} base={getConfig('code/api/v1')}
{...props} {...props}
/> />
) )
export type UseRuleGetProps = Omit<UseGetProps<OpenapiRule, UsererrorError, void, RuleGetPathParams>, 'path'> & export type UseRepoRuleGetProps = Omit<UseGetProps<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>, 'path'> &
RuleGetPathParams RepoRuleGetPathParams
export const useRuleGet = ({ repo_ref, rule_identifier, ...props }: UseRuleGetProps) => export const useRepoRuleGet = ({ repo_ref, rule_identifier, ...props }: UseRepoRuleGetProps) =>
useGet<OpenapiRule, UsererrorError, void, RuleGetPathParams>( useGet<OpenapiRule, UsererrorError, void, RepoRuleGetPathParams>(
(paramsInPath: RuleGetPathParams) => `/repos/${paramsInPath.repo_ref}/rules/${paramsInPath.rule_identifier}`, (paramsInPath: RepoRuleGetPathParams) => `/repos/${paramsInPath.repo_ref}/rules/${paramsInPath.rule_identifier}`,
{ base: getConfig('code/api/v1'), pathParams: { repo_ref, rule_identifier }, ...props } { base: getConfig('code/api/v1'), pathParams: { repo_ref, rule_identifier }, ...props }
) )
export interface RuleUpdatePathParams { export interface RepoRuleUpdatePathParams {
repo_ref: string repo_ref: string
rule_identifier: string rule_identifier: string
} }
export interface RuleUpdateRequestBody { export interface RepoRuleUpdateRequestBody {
definition?: OpenapiRuleDefinition definition?: OpenapiRuleDefinition
description?: string | null description?: string | null
identifier?: string | null identifier?: string | null
@ -6750,14 +6765,14 @@ export interface RuleUpdateRequestBody {
uid?: string | null uid?: string | null
} }
export type RuleUpdateProps = Omit< export type RepoRuleUpdateProps = Omit<
MutateProps<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>, MutateProps<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>,
'path' | 'verb' 'path' | 'verb'
> & > &
RuleUpdatePathParams RepoRuleUpdatePathParams
export const RuleUpdate = ({ repo_ref, rule_identifier, ...props }: RuleUpdateProps) => ( export const RepoRuleUpdate = ({ repo_ref, rule_identifier, ...props }: RepoRuleUpdateProps) => (
<Mutate<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams> <Mutate<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>
verb="PATCH" verb="PATCH"
path={`/repos/${repo_ref}/rules/${rule_identifier}`} path={`/repos/${repo_ref}/rules/${rule_identifier}`}
base={getConfig('code/api/v1')} base={getConfig('code/api/v1')}
@ -6765,16 +6780,16 @@ export const RuleUpdate = ({ repo_ref, rule_identifier, ...props }: RuleUpdatePr
/> />
) )
export type UseRuleUpdateProps = Omit< export type UseRepoRuleUpdateProps = Omit<
UseMutateProps<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>, UseMutateProps<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>,
'path' | 'verb' 'path' | 'verb'
> & > &
RuleUpdatePathParams RepoRuleUpdatePathParams
export const useRuleUpdate = ({ repo_ref, rule_identifier, ...props }: UseRuleUpdateProps) => export const useRepoRuleUpdate = ({ repo_ref, rule_identifier, ...props }: UseRepoRuleUpdateProps) =>
useMutate<OpenapiRule, UsererrorError, void, RuleUpdateRequestBody, RuleUpdatePathParams>( useMutate<OpenapiRule, UsererrorError, void, RepoRuleUpdateRequestBody, RepoRuleUpdatePathParams>(
'PATCH', '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 } { base: getConfig('code/api/v1'), pathParams: { repo_ref, rule_identifier }, ...props }
) )

View File

@ -12287,6 +12287,8 @@ components:
- deleted - deleted
- starting - starting
- stopping - stopping
- cleaning
- cleaned
type: string type: string
EnumGitspaceOwner: EnumGitspaceOwner:
enum: enum:
@ -12416,6 +12418,8 @@ components:
EnumPullReqReviewerType: EnumPullReqReviewerType:
enum: enum:
- assigned - assigned
- code_owners
- default
- requested - requested
- self_assigned - self_assigned
type: string type: string
@ -12468,11 +12472,15 @@ components:
type: string type: string
EnumWebhookParent: EnumWebhookParent:
enum: enum:
- registry
- repo - repo
- space - space
type: string type: string
EnumWebhookTrigger: EnumWebhookTrigger:
enum: enum:
- artifact_created
- artifact_deleted
- artifact_updated
- branch_created - branch_created
- branch_deleted - branch_deleted
- branch_updated - branch_updated
@ -14439,8 +14447,13 @@ components:
properties: properties:
created: created:
type: integer type: integer
deleted:
nullable: true
type: integer
identifier: identifier:
type: string type: string
is_deleted:
type: boolean
metadata: metadata:
additionalProperties: {} additionalProperties: {}
nullable: true nullable: true
@ -15229,6 +15242,8 @@ components:
type: string type: string
email: email:
type: string type: string
id:
type: integer
parent_id: parent_id:
type: integer type: integer
parent_type: parent_type:
@ -15469,6 +15484,8 @@ components:
type: string type: string
email: email:
type: string type: string
id:
type: integer
uid: uid:
type: string type: string
updated: updated:

View File

@ -28,7 +28,8 @@ import type {
TypesCommit, TypesCommit,
TypesPullReq, TypesPullReq,
RepoRepositoryOutput, RepoRepositoryOutput,
TypesRuleViolations TypesRuleViolations,
TypesDefaultReviewerApprovalsResponse
} from 'services/code' } from 'services/code'
import { getConfig } from 'services/config' import { getConfig } from 'services/config'
import { PullRequestSection, getErrorMessage } from './Utils' import { PullRequestSection, getErrorMessage } from './Utils'
@ -173,7 +174,8 @@ export enum GitRefType {
export enum PrincipalUserType { export enum PrincipalUserType {
USER = 'user', USER = 'user',
SERVICE = 'service' SERVICE = 'service',
SERVICE_ACCOUNT = 'serviceaccount'
} }
export enum SettingTypeMode { export enum SettingTypeMode {
@ -540,9 +542,10 @@ export const dryMerge = (
setMinApproval?: (value: React.SetStateAction<number>) => void, setMinApproval?: (value: React.SetStateAction<number>) => void,
setReqCodeOwnerLatestApproval?: (value: React.SetStateAction<boolean>) => void, setReqCodeOwnerLatestApproval?: (value: React.SetStateAction<boolean>) => void,
setMinReqLatestApproval?: (value: React.SetStateAction<number>) => 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 // Use an internal flag to prevent flickering during the loading state of buttons
internalFlags.current.dryRun = true internalFlags.current.dryRun = true
mergePR({ bypass_rules: true, dry_run: true, source_sha: pullReqMetadata?.source_sha }) 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) setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest) setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
setConflictingFiles?.(res.conflict_files) setConflictingFiles?.(res.conflict_files)
setDefaultReviewersInfoSet?.(res.default_reviewer_aprovals)
} else { } else {
setRuleViolation(false) setRuleViolation(false)
setAllowedStrats(res.allowed_methods) setAllowedStrats(res.allowed_methods)
@ -568,6 +572,7 @@ export const dryMerge = (
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest) setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest) setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
setConflictingFiles?.(res.conflict_files) setConflictingFiles?.(res.conflict_files)
setDefaultReviewersInfoSet?.(res.default_reviewer_aprovals)
} }
}) })
.catch(err => { .catch(err => {
@ -582,6 +587,7 @@ export const dryMerge = (
setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest) setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest) setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest)
setConflictingFiles?.(err.conflict_files) setConflictingFiles?.(err.conflict_files)
setDefaultReviewersInfoSet?.(err.default_reviewer_aprovals)
} else if ( } else if (
err.status === 400 && err.status === 400 &&
[oldCommitRefetchRequired, prMergedRefetchRequired].includes(getErrorMessage(err) || '') [oldCommitRefetchRequired, prMergedRefetchRequired].includes(getErrorMessage(err) || '')

View File

@ -30,9 +30,11 @@ import type {
TypesLabel, TypesLabel,
TypesLabelValue, TypesLabelValue,
TypesPrincipalInfo, TypesPrincipalInfo,
EnumMembershipRole EnumMembershipRole,
TypesDefaultReviewerApprovalsResponse
} from 'services/code' } from 'services/code'
import type { StringKeys } from 'framework/strings' import type { StringKeys } from 'framework/strings'
import { PullReqReviewDecision } from 'pages/PullRequest/PullRequestUtils'
export enum ACCESS_MODES { export enum ACCESS_MODES {
VIEW, VIEW,
@ -361,8 +363,11 @@ export const rulesFormInitialPayload: RulesFormPayload = {
targetList: [] as string[][], targetList: [] as string[][],
allRepoOwners: false, allRepoOwners: false,
bypassList: [] as string[], bypassList: [] as string[],
defaultReviewersList: [] as string[],
requireMinReviewers: false, requireMinReviewers: false,
requireMinDefaultReviewers: false,
minReviewers: '', minReviewers: '',
minDefaultReviewers: '',
requireCodeOwner: false, requireCodeOwner: false,
requireNewChanges: false, requireNewChanges: false,
reqResOfChanges: false, reqResOfChanges: false,
@ -380,7 +385,9 @@ export const rulesFormInitialPayload: RulesFormPayload = {
blockForcePush: false, blockForcePush: false,
requirePr: false, requirePr: false,
bypassSet: false, bypassSet: false,
targetSet: false targetSet: false,
defaultReviewersSet: false,
defaultReviewersEnabled: false
} }
export type RulesFormPayload = { export type RulesFormPayload = {
@ -392,8 +399,11 @@ export type RulesFormPayload = {
targetList: string[][] targetList: string[][]
allRepoOwners?: boolean allRepoOwners?: boolean
bypassList?: string[] bypassList?: string[]
defaultReviewersList?: string[]
requireMinReviewers: boolean requireMinReviewers: boolean
requireMinDefaultReviewers: boolean
minReviewers?: string | number minReviewers?: string | number
minDefaultReviewers?: string | number
autoAddCodeOwner?: boolean autoAddCodeOwner?: boolean
requireCodeOwner?: boolean requireCodeOwner?: boolean
requireNewChanges?: boolean requireNewChanges?: boolean
@ -414,6 +424,8 @@ export type RulesFormPayload = {
requirePr?: boolean requirePr?: boolean
bypassSet: boolean bypassSet: boolean
targetSet: 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_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count',
APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners', APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners',
APPROVALS_REQUIRE_NO_CHANGE_REQUEST = 'pullreq.approvals.require_no_change_request', 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', APPROVALS_REQUIRE_LATEST_COMMIT = 'pullreq.approvals.require_latest_commit',
AUTO_ADD_CODE_OWNERS = 'pullreq.reviewers.request_code_owners', 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', COMMENTS_REQUIRE_RESOLVE_ALL = 'pullreq.comments.require_resolve_all',
STATUS_CHECKS_ALL_MUST_SUCCEED = 'pullreq.status_checks.all_must_succeed', STATUS_CHECKS_ALL_MUST_SUCCEED = 'pullreq.status_checks.all_must_succeed',
STATUS_CHECKS_REQUIRE_IDENTIFIERS = 'pullreq.status_checks.require_identifiers', 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 { export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
const ruleFieldsMap: RuleFieldsMap = { const ruleFieldsMap: RuleFieldsMap = {
[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: false, [RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: false,
[RuleFields.AUTO_ADD_CODE_OWNERS]: false,
[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: false, [RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: false,
[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: false, [RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: false,
[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT]: 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.COMMENTS_REQUIRE_RESOLVE_ALL]: false,
[RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: false, [RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: false,
[RuleFields.STATUS_CHECKS_REQUIRE_IDENTIFIERS]: 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' typeof ruleDefinition.pullreq.approvals.require_minimum_count === 'number'
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST] = ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST] =
!!ruleDefinition.pullreq.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) { if (ruleDefinition.pullreq.comments) {
@ -1080,6 +1098,9 @@ export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
if (ruleDefinition.pullreq.reviewers) { if (ruleDefinition.pullreq.reviewers) {
ruleFieldsMap[RuleFields.AUTO_ADD_CODE_OWNERS] = !!ruleDefinition.pullreq.reviewers.request_code_owners 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]}` return `${list.slice(0, -1).join(', ')} and ${list[list.length - 1]}`
} else return '' } 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
}