feat: [CODE-3214] added support for Pull Requests on Account/Org/Project in Harness code (#3521)

* fix: [CODE-3194] addressed comments
* fix: [CODE-3194] addressed comments
* fix: [CODE-3194] extract PR Author Filter to a common component
* fix: [CODE-3194] comments
* fix: [CODE-3194] comments
* fix: [CODE-3194] lint
*

fix: [CODE-3214] lint check
*

feat: [CODE-3214] added support for Account/Org/Project in Harness code
try-new-ui
Ritik Kapoor 2025-03-10 12:33:35 +00:00 committed by Harness
parent 75ec960ef2
commit efa98ba52e
21 changed files with 2606 additions and 526 deletions

View File

@ -52,6 +52,7 @@ module.exports = {
'./Webhooks': './src/pages/Webhooks/Webhooks.tsx',
'./WebhookNew': './src/pages/WebhookNew/WebhookNew.tsx',
'./Search': './src/pages/Search/CodeSearchPage.tsx',
'./SpacePullRequests': '/src/pages/ManageSpace/SpacePullRequests/SpacePullRequests.tsx',
'./Labels': './src/pages/ManageSpace/ManageRepositories/ManageRepositories.tsx',
'./WebhookDetails': './src/pages/WebhookDetails/WebhookDetails.tsx',
'./NewRepoModalButton': './src/components/NewRepoModalButton/NewRepoModalButton.tsx',

View File

@ -39,7 +39,8 @@ export const defaultCurrentUser: Required<TypesUser> = {
updated: 0,
display_name: '',
email: '',
uid: ''
uid: '',
id: 0
}
const AppContext = React.createContext<AppContextProps>({

View File

@ -40,6 +40,7 @@ import {
LIST_FETCHING_LIMIT,
LabelFilterObj,
LabelFilterType,
ScopeLevel,
getErrorMessage,
getScopeData
} from 'utils/Utils'
@ -53,8 +54,9 @@ interface LabelFilterProps {
setLabelFilterOption: React.Dispatch<React.SetStateAction<LabelFilterObj[] | undefined>>
onPullRequestLabelFilterChanged: (labelFilter: LabelFilterObj[]) => void
bearerToken: string
repoMetadata: RepoRepositoryOutput
spaceRef: string
filterScope: ScopeLevel
repoMetadata?: RepoRepositoryOutput
}
enum utilFilterType {
@ -76,6 +78,7 @@ export const LabelFilter = (props: LabelFilterProps) => {
onPullRequestLabelFilterChanged,
bearerToken,
repoMetadata,
filterScope,
spaceRef
} = props
const { showError } = useToaster()
@ -90,22 +93,36 @@ export const LabelFilter = (props: LabelFilterProps) => {
const [labelItems, setLabelItems] = useState<SelectOption[]>()
const { getString } = useStrings()
const [accountIdentifier, orgIdentifier, projectIdentifier] = spaceRef?.split('/') || []
const getLabelsOnRepoScope = () =>
getUsingFetch(getConfig('code/api/v1'), `/repos/${repoMetadata?.path}/+/labels`, bearerToken, {
queryParams: {
page: 1,
limit: LIST_FETCHING_LIMIT,
inherited: true,
query: labelQuery?.trim(),
accountIdentifier: routingId
}
})
const getLabelsOnSpaceScope = () =>
getUsingFetch(getConfig('code/api/v1'), `/labels`, bearerToken, {
queryParams: {
accountIdentifier: accountIdentifier ?? routingId,
orgIdentifier,
projectIdentifier,
page: 1,
limit: LIST_FETCHING_LIMIT,
inherited: true,
query: labelQuery?.trim()
}
})
const getDropdownLabels = async (currentFilterOption?: LabelFilterObj[]) => {
try {
const fetchedLabels: TypesLabel[] = await getUsingFetch(
getConfig('code/api/v1'),
`/repos/${repoMetadata?.path}/+/labels`,
bearerToken,
{
queryParams: {
page: 1,
limit: LIST_FETCHING_LIMIT,
inherited: true,
query: labelQuery?.trim(),
accountIdentifier: routingId
}
}
)
const fetchedLabels: TypesLabel[] =
filterScope === ScopeLevel.SPACE ? await getLabelsOnSpaceScope() : await getLabelsOnRepoScope()
const updateLabelsList = mapToSelectOptions(fetchedLabels)
const labelForTop = mapToSelectOptions(currentFilterOption?.map(({ labelObj }) => labelObj))
const mergedArray = [...labelForTop, ...updateLabelsList]

View File

@ -23,7 +23,7 @@ import { useStrings } from 'framework/strings'
import type { TypesPullReq } from 'services/code'
import css from './PullRequestStateLabel.module.scss'
export const PullRequestStateLabel: React.FC<{ data: TypesPullReq; iconSize?: number; iconOnly?: boolean }> = ({
export const PullRequestStateLabel: React.FC<{ data?: TypesPullReq; iconSize?: number; iconOnly?: boolean }> = ({
data,
iconSize = 20,
iconOnly = false
@ -51,7 +51,7 @@ export const PullRequestStateLabel: React.FC<{ data: TypesPullReq; iconSize?: nu
css: css.open
}
}
const map = data.is_draft ? maps.draft : maps[data.state || 'unknown']
const map = data?.is_draft ? maps.draft : maps[data?.state || 'unknown']
return (
<Text
@ -60,7 +60,7 @@ export const PullRequestStateLabel: React.FC<{ data: TypesPullReq; iconSize?: nu
icon={map.icon as IconName}
iconProps={{ size: iconOnly ? iconSize : 12 }}>
{!iconOnly && (
<StringSubstitute str={getString('pr.state')} vars={{ state: data.is_draft ? 'draft' : data.state }} />
<StringSubstitute str={getString('pr.state')} vars={{ state: data?.is_draft ? 'draft' : data?.state }} />
)}
</Text>
)

View File

@ -106,7 +106,7 @@ interface PrevNextPaginationProps {
skipLayout?: boolean
}
function PrevNextPagination({ onPrev, onNext, skipLayout }: PrevNextPaginationProps) {
export function PrevNextPagination({ onPrev, onNext, skipLayout }: PrevNextPaginationProps) {
const { getString } = useStrings()
return (

View File

@ -441,6 +441,7 @@ export interface StringsMap {
fileTooLarge: string
files: string
filesChanged: string
filterByAuthor: string
findATag: string
findAUser: string
findBranch: string
@ -522,6 +523,7 @@ export interface StringsMap {
'importSpace.title': string
in: string
inactiveBranches: string
includeSubspacePR: string
invalidResponse: string
isRequired: string
italic: string
@ -779,6 +781,7 @@ export interface StringsMap {
'pr.branchHasNoConflicts': string
'pr.cantBeMerged': string
'pr.cantMerge': string
'pr.changesRequested': string
'pr.checkingToMerge': string
'pr.checks': string
'pr.checksFailure': string
@ -826,6 +829,7 @@ export interface StringsMap {
'pr.metaLine': string
'pr.modalTitle': string
'pr.moreComments': string
'pr.myPRs': string
'pr.openForReview': string
'pr.outdated': string
'pr.prBranchDeleteInfo': string
@ -847,6 +851,7 @@ export interface StringsMap {
'pr.requestSubmitted': string
'pr.requestedChanges': string
'pr.reviewChanges': string
'pr.reviewRequested': string
'pr.reviewSubmitted': string
'pr.showDiff': string
'pr.showLink': string
@ -876,6 +881,7 @@ export interface StringsMap {
prHasNoConflicts: string
prMustSelectSourceAndTargetBranches: string
'prReview.assigned': string
'prReview.filterByReviews': string
'prReview.removed': string
'prReview.requested': string
'prReview.selfAssigned': string
@ -1023,7 +1029,6 @@ export interface StringsMap {
'securitySettings.vulnerabilityScanning': string
'securitySettings.vulnerabilityScanningDesc': string
seeNMoreMatches: string
selectAuthor: string
selectBranchPlaceHolder: string
selectLanguagePlaceholder: string
selectMergeStrat: string

View File

@ -364,6 +364,9 @@ pr:
checks: Checks
checksFailure: '{failedCount}/{total} checks failed'
addDescription: Add Description
reviewRequested: Review Requested
changesRequested: Changes Requested
myPRs: My Pull Requests
prState:
draftHeading: This pull request is still a work in progress
draftDesc: Draft pull requests cannot be merged.
@ -383,6 +386,7 @@ prReview:
selfAssigned: '{reviewer} self-requested a review'
removed: '{author} removed the request for review from {reviewer}'
selfRemoved: '{author} removed their request for review'
filterByReviews: Filter by Reviews
webhookListingContent: 'create,delete,deployment ...'
general: 'General'
webhooks: 'Webhooks'
@ -551,7 +555,8 @@ zoomIn: Zoom In
zoomOut: Zoom Out
checks: Checks
blameCommitLine: '{author} committed {timestamp}'
selectAuthor: Select Author
filterByAuthor: Filter by Author
includeSubspacePR: Include sub-space PRs
tooltipRepoEdit: You are not authorized to {PERMS}
missingPerms: 'You are missing the following permission:'
createRepoPerms: 'Create / Edit Repository'

View File

@ -26,7 +26,7 @@ import LabelsListing from 'pages/Labels/LabelsListing'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { CODEProps } from 'RouteDefinitions'
import { useGetCurrentPageScope } from 'hooks/useGetCurrentPageScope'
import css from './ManageRepositories.module.scss'
import css from '../ManageSpace.module.scss'
export default function ManageRepositories() {
const { settingSection } = useParams<CODEProps>()

View File

@ -0,0 +1,69 @@
/*
* 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.
*/
.main {
min-height: var(--page-height);
background-color: var(--primary-bg) !important;
.table {
.row {
height: fit-content;
display: flex;
justify-content: center;
padding: 0;
.title {
font-weight: 600;
display: flex;
.convoIcon {
padding-top: 1px !important;
}
}
.convo {
white-space: nowrap;
}
.prLabels {
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 10px;
}
.prwrap {
width: 80% !important;
flex-wrap: wrap;
}
}
.rowLink:hover {
&,
*:not(a) {
text-decoration: none !important;
}
}
}
.titleRow {
padding-left: var(--spacing-small);
align-items: center;
padding: var(--spacing-medium);
}
}
.state {
font-weight: 600;
}

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const convo: string
export declare const convoIcon: string
export declare const main: string
export declare const prLabels: string
export declare const prwrap: string
export declare const row: string
export declare const rowLink: string
export declare const state: string
export declare const table: string
export declare const title: string
export declare const titleRow: string

View File

@ -0,0 +1,98 @@
/*
* 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, { useState, useEffect } from 'react'
import cx from 'classnames'
import { Container, Tabs, Page } from '@harnessio/uicore'
import { omit } from 'lodash-es'
import { useStrings } from 'framework/strings'
import { PullRequestFilterOption, PullRequestReviewFilterOption, SpacePRTabs } from 'utils/GitUtils'
import { PageBrowserProps, ScopeLevelEnum } from 'utils/Utils'
import { useQueryParams } from 'hooks/useQueryParams'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { SpacePullRequestsListing } from 'pages/PullRequests/SpacePullRequestsListing'
import css from '../ManageSpace.module.scss'
export default function SpacePullRequests() {
const browserParams = useQueryParams<PageBrowserProps>()
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
const [activeTab, setActiveTab] = useState<string>(browserParams.tab || SpacePRTabs.CREATED)
const [includeSubspaces, setIncludeSubspaces] = useState<ScopeLevelEnum>(
browserParams?.subspace || ScopeLevelEnum.CURRENT
)
const { getString } = useStrings()
useEffect(() => {
const params = {
...browserParams,
tab: browserParams.tab ?? SpacePRTabs.CREATED,
...(!browserParams.state && { state: PullRequestFilterOption.OPEN })
}
updateQueryParams(params, undefined, true)
}, [browserParams])
const tabListArray = [
{
id: SpacePRTabs.CREATED,
title: getString('pr.myPRs'),
panel: (
<SpacePullRequestsListing
activeTab={SpacePRTabs.CREATED}
includeSubspaces={includeSubspaces}
setIncludeSubspaces={setIncludeSubspaces}
/>
)
},
{
id: SpacePRTabs.REVIEW_REQUESTED,
title: getString('pr.reviewRequested'),
panel: (
<SpacePullRequestsListing
activeTab={SpacePRTabs.REVIEW_REQUESTED}
includeSubspaces={includeSubspaces}
setIncludeSubspaces={setIncludeSubspaces}
/>
)
}
]
return (
<Container className={css.main}>
<Page.Header title={getString('pullRequests')} />
<Container className={cx(css.main, css.tabsContainer)}>
<Tabs
id="SettingsTabs"
large={false}
defaultSelectedTabId={activeTab}
animate={false}
onChange={(id: string) => {
setActiveTab(id)
const params = {
...browserParams,
tab: id
}
if (id === SpacePRTabs.CREATED) {
replaceQueryParams(omit(params, 'review'), undefined, true)
} else {
const updatedParams = { ...params, review: PullRequestReviewFilterOption.PENDING }
replaceQueryParams(updatedParams, undefined, true)
}
}}
tabList={tabListArray}></Tabs>
</Container>
</Container>
)
}

View File

@ -0,0 +1,179 @@
/*
* 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, { useState } from 'react'
import { Layout, DropDown, SelectOption, Text } from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import { sortBy } from 'lodash-es'
import { getConfig, getUsingFetch } from 'services/config'
import { useStrings } from 'framework/strings'
import type { TypesPrincipalInfo, TypesUser } from 'services/code'
import { useAppContext } from 'AppContext'
import css from './PullRequestsContentHeader.module.scss'
interface PullRequestsContentHeaderProps {
bearerToken: string
activePullRequestAuthorFilterOption?: string
activePullRequestAuthorObj?: TypesPrincipalInfo | null
onPullRequestAuthorFilterChanged: (authorFilter: string) => void
}
export function PRAuthorFilter({
onPullRequestAuthorFilterChanged,
activePullRequestAuthorFilterOption,
activePullRequestAuthorObj,
bearerToken
}: PullRequestsContentHeaderProps) {
const { getString } = useStrings()
const [authorFilterOption, setAuthorFilterOption] = useState(activePullRequestAuthorFilterOption)
const [query, setQuery] = useState<string>('')
const [loadingAuthors, setLoadingAuthors] = useState<boolean>(false)
const { currentUser, routingId } = useAppContext()
const moveCurrentUserToTop = async (
authorsList: TypesPrincipalInfo[],
user: Required<TypesUser>,
userQuery: string
): Promise<TypesPrincipalInfo[]> => {
const sortedList = sortBy(authorsList, item => item.display_name?.toLowerCase())
const updateList = (index: number, list: TypesPrincipalInfo[]) => {
if (index !== -1) {
const currentUserObj = list[index]
list.splice(index, 1)
list.unshift(currentUserObj)
}
}
if (userQuery) return sortedList
const targetIndex = sortedList.findIndex(obj => obj.uid === user.uid)
if (targetIndex !== -1) {
updateList(targetIndex, sortedList)
} else {
if (user) {
const newAuthorsList = await getUsingFetch(getConfig('code/api/v1'), `/principals`, bearerToken, {
queryParams: {
query: user?.display_name?.trim(),
type: 'user',
accountIdentifier: routingId
}
})
const mergedList = [...new Set(authorsList?.concat(newAuthorsList))]
const newSortedList = sortBy(mergedList, item => item.display_name?.toLowerCase())
const newIndex = newSortedList.findIndex(obj => obj.uid === user.uid)
updateList(newIndex, newSortedList)
return newSortedList
}
}
return sortedList
}
const getAuthorsPromise = async (): Promise<SelectOption[]> => {
setLoadingAuthors(true)
try {
const fetchedAuthors: TypesPrincipalInfo[] = await getUsingFetch(
getConfig('code/api/v1'),
`/principals`,
bearerToken,
{
queryParams: {
query: query?.trim(),
type: 'user',
accountIdentifier: routingId
}
}
)
const authors = [...fetchedAuthors, ...(activePullRequestAuthorObj ? [activePullRequestAuthorObj] : [])]
const authorsList = await moveCurrentUserToTop(authors, currentUser, query)
const updatedAuthorsList = Array.isArray(authorsList)
? ([
...(authorsList || []).map(item => ({
label: JSON.stringify({ displayName: item?.display_name, email: item?.email }),
value: String(item?.id)
}))
] as SelectOption[])
: ([] as SelectOption[])
setLoadingAuthors(false)
return updatedAuthorsList
} catch (error) {
setLoadingAuthors(false)
throw error
}
}
return (
<DropDown
value={authorFilterOption}
items={() => getAuthorsPromise()}
disabled={loadingAuthors}
onChange={({ value, label }) => {
setAuthorFilterOption(label as string)
onPullRequestAuthorFilterChanged(value as string)
}}
popoverClassName={css.branchDropdown}
icon="nav-user-profile"
iconProps={{ size: 16 }}
placeholder={getString('filterByAuthor')}
addClearBtn={true}
resetOnClose
resetOnSelect
resetOnQuery
query={query}
onQueryChange={newQuery => {
setQuery(newQuery)
}}
itemRenderer={(item, { handleClick }) => {
const itemObj = JSON.parse(item.label)
return (
<Layout.Horizontal
padding={{ top: 'small', right: 'small', bottom: 'small', left: 'small' }}
font={{ variation: FontVariation.BODY }}
className={css.authorDropdownItem}
onClick={handleClick}>
<Text color={Color.GREY_900} className={css.authorName} tooltipProps={{ isDark: true }}>
<span>{itemObj.displayName}</span>
</Text>
<Text color={Color.GREY_400} font={{ variation: FontVariation.BODY }} lineClamp={1} tooltip={itemObj.email}>
({itemObj.email})
</Text>
</Layout.Horizontal>
)
}}
getCustomLabel={item => {
const itemObj = JSON.parse(item.label)
return (
<Layout.Horizontal spacing="small">
<Text
color={Color.GREY_900}
font={{ variation: FontVariation.BODY }}
tooltip={
<Text
padding={{ top: 'medium', right: 'medium', bottom: 'medium', left: 'medium' }}
color={Color.GREY_0}>
{itemObj.email}
</Text>
}
tooltipProps={{ isDark: true }}>
{itemObj.displayName}
</Text>
</Layout.Horizontal>
)
}}
/>
)
}

View File

@ -16,28 +16,17 @@
import { useHistory } from 'react-router-dom'
import React, { useEffect, useMemo, useState } from 'react'
import {
Container,
Layout,
FlexExpander,
DropDown,
ButtonVariation,
Button,
SelectOption,
Text
} from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import { sortBy } from 'lodash-es'
import { getConfig, getUsingFetch } from 'services/config'
import { Container, Layout, FlexExpander, DropDown, ButtonVariation, Button } from '@harnessio/uicore'
import { useStrings } from 'framework/strings'
import { CodeIcon, GitInfoProps, makeDiffRefs, PullRequestFilterOption } from 'utils/GitUtils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { TypesPrincipalInfo, TypesUser } from 'services/code'
import type { TypesPrincipalInfo } from 'services/code'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { LabelFilterObj, PageBrowserProps, permissionProps } from 'utils/Utils'
import { LabelFilterObj, PageBrowserProps, ScopeLevel, permissionProps } from 'utils/Utils'
import { useQueryParams } from 'hooks/useQueryParams'
import { LabelFilter } from 'components/Label/LabelFilter/LabelFilter'
import { PRAuthorFilter } from './PRAuthorFilter'
import css from './PullRequestsContentHeader.module.scss'
interface PullRequestsContentHeaderProps extends Pick<GitInfoProps, 'repoMetadata'> {
@ -68,13 +57,10 @@ export function PullRequestsContentHeader({
const { getString } = useStrings()
const browserParams = useQueryParams<PageBrowserProps>()
const [filterOption, setFilterOption] = useState(activePullRequestFilterOption)
const [authorFilterOption, setAuthorFilterOption] = useState(activePullRequestAuthorFilterOption)
const [labelFilterOption, setLabelFilterOption] = useState(activePullRequestLabelFilterOption)
const [searchTerm, setSearchTerm] = useState('')
const [query, setQuery] = useState<string>('')
const [loadingAuthors, setLoadingAuthors] = useState<boolean>(false)
const space = useGetSpaceParam()
const { hooks, currentUser, standalone, routingId, routes } = useAppContext()
const { hooks, standalone, routes } = useAppContext()
const permPushResult = hooks?.usePermissionTranslate?.(
{
resource: {
@ -99,85 +85,12 @@ export function PullRequestsContentHeader({
{ label: getString('open'), value: PullRequestFilterOption.OPEN },
{ label: getString('merged'), value: PullRequestFilterOption.MERGED },
{ label: getString('closed'), value: PullRequestFilterOption.CLOSED },
// { label: getString('draft'), value: PullRequestFilterOption.DRAFT },
// { label: getString('yours'), value: PullRequestFilterOption.YOURS },
{ label: getString('all'), value: PullRequestFilterOption.ALL }
],
[getString]
)
const bearerToken = hooks?.useGetToken?.() || ''
const moveCurrentUserToTop = async (
authorsList: TypesPrincipalInfo[],
user: Required<TypesUser>,
userQuery: string
): Promise<TypesPrincipalInfo[]> => {
const sortedList = sortBy(authorsList, item => item.display_name?.toLowerCase())
const updateList = (index: number, list: TypesPrincipalInfo[]) => {
if (index !== -1) {
const currentUserObj = list[index]
list.splice(index, 1)
list.unshift(currentUserObj)
}
}
if (userQuery) return sortedList
const targetIndex = sortedList.findIndex(obj => obj.uid === user.uid)
if (targetIndex !== -1) {
updateList(targetIndex, sortedList)
} else {
if (user) {
const newAuthorsList = await getUsingFetch(getConfig('code/api/v1'), `/principals`, bearerToken, {
queryParams: {
query: user?.display_name?.trim(),
type: 'user',
accountIdentifier: routingId
}
})
const mergedList = [...new Set(authorsList?.concat(newAuthorsList))]
const newSortedList = sortBy(mergedList, item => item.display_name?.toLowerCase())
const newIndex = newSortedList.findIndex(obj => obj.uid === user.uid)
updateList(newIndex, newSortedList)
return newSortedList
}
}
return sortedList
}
const getAuthorsPromise = async (): Promise<SelectOption[]> => {
setLoadingAuthors(true)
try {
const fetchedAuthors: TypesPrincipalInfo[] = await getUsingFetch(
getConfig('code/api/v1'),
`/principals`,
bearerToken,
{
queryParams: {
query: query?.trim(),
type: 'user',
accountIdentifier: routingId
}
}
)
const authors = [...fetchedAuthors, ...(activePullRequestAuthorObj ? [activePullRequestAuthorObj] : [])]
const authorsList = await moveCurrentUserToTop(authors, currentUser, query)
const updatedAuthorsList = Array.isArray(authorsList)
? ([
...(authorsList || []).map(item => ({
label: JSON.stringify({ displayName: item?.display_name, email: item?.email }),
value: String(item?.id)
}))
] as SelectOption[])
: ([] as SelectOption[])
setLoadingAuthors(false)
return updatedAuthorsList
} catch (error) {
setLoadingAuthors(false)
throw error
}
}
return (
<Container className={css.main} padding="xlarge">
@ -200,70 +113,16 @@ export function PullRequestsContentHeader({
bearerToken={bearerToken}
repoMetadata={repoMetadata}
spaceRef={space}
filterScope={ScopeLevel.REPOSITORY}
/>
<DropDown
value={authorFilterOption}
items={() => getAuthorsPromise()}
disabled={loadingAuthors}
onChange={({ value, label }) => {
setAuthorFilterOption(label as string)
onPullRequestAuthorFilterChanged(value as string)
}}
popoverClassName={css.branchDropdown}
icon="nav-user-profile"
iconProps={{ size: 16 }}
placeholder={getString('selectAuthor')}
addClearBtn={true}
resetOnClose
resetOnSelect
resetOnQuery
query={query}
onQueryChange={newQuery => {
setQuery(newQuery)
}}
itemRenderer={(item, { handleClick }) => {
const itemObj = JSON.parse(item.label)
return (
<Layout.Horizontal
padding={{ top: 'small', right: 'small', bottom: 'small', left: 'small' }}
font={{ variation: FontVariation.BODY }}
className={css.authorDropdownItem}
onClick={handleClick}>
<Text color={Color.GREY_900} className={css.authorName} tooltipProps={{ isDark: true }}>
<span>{itemObj.displayName}</span>
</Text>
<Text
color={Color.GREY_400}
font={{ variation: FontVariation.BODY }}
lineClamp={1}
tooltip={itemObj.email}>
({itemObj.email})
</Text>
</Layout.Horizontal>
)
}}
getCustomLabel={item => {
const itemObj = JSON.parse(item.label)
return (
<Layout.Horizontal spacing="small">
<Text
color={Color.GREY_900}
font={{ variation: FontVariation.BODY }}
tooltip={
<Text
padding={{ top: 'medium', right: 'medium', bottom: 'medium', left: 'medium' }}
color={Color.GREY_0}>
{itemObj.email}
</Text>
}
tooltipProps={{ isDark: true }}>
{itemObj.displayName}
</Text>
</Layout.Horizontal>
)
}}
<PRAuthorFilter
onPullRequestAuthorFilterChanged={onPullRequestAuthorFilterChanged}
activePullRequestAuthorFilterOption={activePullRequestAuthorFilterOption}
activePullRequestAuthorObj={activePullRequestAuthorObj}
bearerToken={bearerToken}
/>
<DropDown
value={filterOption}
items={items}

View File

@ -0,0 +1,203 @@
/*
* 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, { useEffect, useMemo, useState } from 'react'
import { Container, Layout, FlexExpander, DropDown, SelectOption } from '@harnessio/uicore'
import { Render } from 'react-jsx-match'
import { useStrings } from 'framework/strings'
import { PullRequestFilterOption, PullRequestReviewFilterOption, SpacePRTabs } from 'utils/GitUtils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { TypesPrincipalInfo } from 'services/code'
import { useAppContext } from 'AppContext'
import { SearchInputWithSpinner } from 'components/SearchInputWithSpinner/SearchInputWithSpinner'
import { ScopeLevelEnum, type LabelFilterObj, type PageBrowserProps, ScopeLevel } from 'utils/Utils'
import { useQueryParams } from 'hooks/useQueryParams'
import { LabelFilter } from 'components/Label/LabelFilter/LabelFilter'
import { PRAuthorFilter } from './PRAuthorFilter'
import css from './PullRequestsContentHeader.module.scss'
interface SpacePullRequestsContentHeaderProps {
activeTab: SpacePRTabs
loading?: boolean
activePullRequestFilterOption?: string
activePullRequestReviewFilterOption?: string
activePullRequestAuthorFilterOption?: string
activePullRequestAuthorObj?: TypesPrincipalInfo | null
activePullRequestLabelFilterOption?: LabelFilterObj[]
activePullRequestIncludeSubSpaceOption?: ScopeLevelEnum
onPullRequestFilterChanged: React.Dispatch<React.SetStateAction<string>>
onPullRequestReviewFilterChanged: React.Dispatch<React.SetStateAction<string>>
onPullRequestAuthorFilterChanged: (authorFilter: string) => void
onPullRequestLabelFilterChanged: (labelFilter: LabelFilterObj[]) => void
onSearchTermChanged: (searchTerm: string) => void
onPullRequestIncludeSubSpaceOptionChanged: React.Dispatch<React.SetStateAction<ScopeLevelEnum>>
}
export function SpacePullRequestsContentHeader({
activeTab,
loading,
onPullRequestFilterChanged,
onPullRequestReviewFilterChanged,
onPullRequestAuthorFilterChanged,
onPullRequestLabelFilterChanged,
onPullRequestIncludeSubSpaceOptionChanged,
onSearchTermChanged,
activePullRequestFilterOption = PullRequestFilterOption.OPEN,
activePullRequestReviewFilterOption,
activePullRequestAuthorFilterOption,
activePullRequestLabelFilterOption,
activePullRequestAuthorObj,
activePullRequestIncludeSubSpaceOption
}: SpacePullRequestsContentHeaderProps) {
const { getString } = useStrings()
const browserParams = useQueryParams<PageBrowserProps>()
const [filterOption, setFilterOption] = useState(activePullRequestFilterOption)
const [reviewFilterOption, setReviewFilterOption] = useState(activePullRequestReviewFilterOption)
const [labelFilterOption, setLabelFilterOption] = useState(activePullRequestLabelFilterOption)
const [searchTerm, setSearchTerm] = useState('')
const space = useGetSpaceParam()
const { hooks } = useAppContext()
const [accountIdentifier, orgIdentifier, projectIdentifier] = space?.split('/') || []
useEffect(() => {
setLabelFilterOption(activePullRequestLabelFilterOption)
}, [activePullRequestLabelFilterOption])
useEffect(() => {
setFilterOption(browserParams?.state as string)
}, [browserParams, activeTab])
useEffect(() => {
activeTab === SpacePRTabs.REVIEW_REQUESTED && setReviewFilterOption(activePullRequestReviewFilterOption)
}, [activePullRequestReviewFilterOption, activeTab])
const items = useMemo(
() => [
{ label: getString('open'), value: PullRequestFilterOption.OPEN },
{ label: getString('merged'), value: PullRequestFilterOption.MERGED },
{ label: getString('closed'), value: PullRequestFilterOption.CLOSED },
{ label: getString('all'), value: PullRequestFilterOption.ALL }
],
[getString]
)
const reviewItems = useMemo(
() => [
{ label: getString('pending'), value: PullRequestReviewFilterOption.PENDING },
{ label: getString('approved'), value: PullRequestReviewFilterOption.APPROVED },
{ label: getString('pr.changesRequested'), value: PullRequestReviewFilterOption.CHANGES_REQUESTED }
],
[getString]
)
const bearerToken = hooks?.useGetToken?.() || ''
const scopeOption = [
accountIdentifier && !orgIdentifier
? {
label: getString('searchScope.allScopes'),
value: ScopeLevelEnum.ALL
}
: null,
accountIdentifier && !orgIdentifier
? { label: getString('searchScope.accOnly'), value: ScopeLevelEnum.CURRENT }
: null,
orgIdentifier ? { label: getString('searchScope.orgAndProj'), value: ScopeLevelEnum.ALL } : null,
orgIdentifier ? { label: getString('searchScope.orgOnly'), value: ScopeLevelEnum.CURRENT } : null
].filter(Boolean) as SelectOption[]
const currentScopeLabel =
activePullRequestIncludeSubSpaceOption === ScopeLevelEnum.ALL
? {
label:
accountIdentifier && !orgIdentifier
? getString('searchScope.allScopes')
: getString('searchScope.orgAndProj'),
value: ScopeLevelEnum.ALL
}
: {
label:
accountIdentifier && !orgIdentifier ? getString('searchScope.accOnly') : getString('searchScope.orgOnly'),
value: ScopeLevelEnum.CURRENT
}
const [scopeLabel, setScopeLabel] = useState<SelectOption>(currentScopeLabel ? currentScopeLabel : scopeOption[0])
return (
<Container className={css.main} padding="xlarge">
<Layout.Horizontal spacing="medium">
<SearchInputWithSpinner
loading={loading}
spinnerPosition="right"
query={searchTerm}
setQuery={value => {
setSearchTerm(value)
onSearchTermChanged(value)
}}
/>
<Render when={!projectIdentifier}>
<DropDown
placeholder={scopeLabel.label}
value={scopeLabel}
items={scopeOption}
onChange={e => {
onPullRequestIncludeSubSpaceOptionChanged(e.value as ScopeLevelEnum)
setScopeLabel(e)
}}
/>
</Render>
<FlexExpander />
<LabelFilter
labelFilterOption={labelFilterOption}
setLabelFilterOption={setLabelFilterOption}
onPullRequestLabelFilterChanged={onPullRequestLabelFilterChanged}
bearerToken={bearerToken}
filterScope={ScopeLevel.SPACE}
spaceRef={space}
/>
<Render when={activeTab === SpacePRTabs.REVIEW_REQUESTED}>
<DropDown
value={reviewFilterOption}
items={reviewItems}
onChange={({ value }) => {
setReviewFilterOption(value as string)
onPullRequestReviewFilterChanged(value as string)
}}
popoverClassName={css.branchDropdown}
/>
</Render>
<Render when={activeTab !== SpacePRTabs.CREATED}>
<PRAuthorFilter
onPullRequestAuthorFilterChanged={onPullRequestAuthorFilterChanged}
activePullRequestAuthorFilterOption={activePullRequestAuthorFilterOption}
activePullRequestAuthorObj={activePullRequestAuthorObj}
bearerToken={bearerToken}
/>
</Render>
<DropDown
value={filterOption}
items={items}
onChange={({ value }) => {
setFilterOption(value as string)
onPullRequestFilterChanged(value as string)
}}
popoverClassName={css.branchDropdown}
/>
</Layout.Horizontal>
</Container>
)
}

View File

@ -0,0 +1,537 @@
/*
* 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, { useEffect, useMemo, useState } from 'react'
import {
Container,
PageBody,
Text,
TableV2,
Layout,
StringSubstitute,
FlexExpander,
Utils,
stringSubstitute
} from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { Color, FontVariation } from '@harnessio/design-system'
import { Link } from 'react-router-dom'
import { useGet } from 'restful-react'
import type { CellProps, Column } from 'react-table'
import { Case, Match, Render, Truthy } from 'react-jsx-match'
import { defaultTo, isEmpty, noop } from 'lodash-es'
import { PullRequestFilterOption, PullRequestReviewFilterOption, PullRequestState, SpacePRTabs } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { useStrings } from 'framework/strings'
import {
voidFn,
getErrorMessage,
LIST_FETCHING_LIMIT,
PageBrowserProps,
ColorName,
LabelFilterObj,
LabelFilterType,
ScopeLevelEnum,
PageAction
} from 'utils/Utils'
import { usePageIndex } from 'hooks/usePageIndex'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { useQueryParams } from 'hooks/useQueryParams'
import type { TypesLabelPullReqAssignmentInfo, TypesPrincipalInfo, TypesPullReqRepo } from 'services/code'
import { PrevNextPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
import { PullRequestStateLabel } from 'components/PullRequestStateLabel/PullRequestStateLabel'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { Label } from 'components/Label/Label'
import { getConfig } from 'services/config'
import { SpacePullRequestsContentHeader } from './PullRequestsContentHeader/SpacePullRequestsContentHeader'
import css from './PullRequests.module.scss'
interface SpacePullRequestsProps {
activeTab: SpacePRTabs
includeSubspaces: ScopeLevelEnum
setIncludeSubspaces: (sub: ScopeLevelEnum) => void
}
export function SpacePullRequestsListing({ activeTab, includeSubspaces, setIncludeSubspaces }: SpacePullRequestsProps) {
const { getString } = useStrings()
const { routes, routingId, currentUser } = useAppContext()
const [searchTerm, setSearchTerm] = useState<string | undefined>()
const browserParams = useQueryParams<PageBrowserProps>()
const [filter, setFilter] = useState(browserParams?.state || (PullRequestFilterOption.OPEN as string))
const [reviewFilter, setReviewFilter] = useState(
browserParams?.review || (PullRequestReviewFilterOption.PENDING as string)
)
const [authorFilter, setAuthorFilter] = useState<string>(browserParams?.author ?? '')
const [labelFilter, setLabelFilter] = useState<LabelFilterObj[]>([])
const [pageAction, setPageAction] = useState<{ action: PageAction; timestamp: number }>({
action: PageAction.NEXT,
timestamp: 1
})
const space = useGetSpaceParam()
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
const pageInit = browserParams.page ? parseInt(browserParams.page) : 1
const [page, setPage] = usePageIndex(pageInit)
const { data: principal, refetch: refetchPrincipal } = useGet<TypesPrincipalInfo>({
base: getConfig('code/api/v1'),
path: `/principals/${browserParams.author}`,
queryParams: {
accountIdentifier: routingId
},
lazy: true
})
useEffect(() => {
const params = {
...browserParams,
...(Boolean(authorFilter) && { author: authorFilter }),
// ...(page >= 1 && { page: page.toString() }),
...(filter && { state: filter }),
...(reviewFilter && { review: reviewFilter }),
subspace: includeSubspaces.toString()
}
updateQueryParams(params, undefined, true)
if (page <= 1) {
const updateParams = { ...params }
delete updateParams.page
replaceQueryParams(updateParams, undefined, true)
}
if (!authorFilter && browserParams.author) {
const paramList = { ...params }
delete paramList.author
replaceQueryParams(paramList, undefined, true)
}
if (activeTab === SpacePRTabs.CREATED || !reviewFilter) {
const paramList = { ...params }
delete paramList.review
replaceQueryParams(paramList, undefined, true)
}
if (browserParams.author) {
refetchPrincipal()
}
}, [page, filter, reviewFilter, authorFilter, activeTab, includeSubspaces]) // eslint-disable-line react-hooks/exhaustive-deps
const [accountIdentifier, orgIdentifier, projectIdentifier] = space?.split('/') || []
const {
data,
error: prError,
loading: prLoading,
refetch: refetchPrs
} = useGet<TypesPullReqRepo[]>({
//add type
path: `/api/v1/pullreq`,
queryParams: {
accountIdentifier,
orgIdentifier,
projectIdentifier,
limit: String(LIST_FETCHING_LIMIT),
exclude_description: true,
page,
sort: filter == PullRequestFilterOption.MERGED ? 'merged' : 'number',
order: 'desc',
query: searchTerm,
include_subspaces: includeSubspaces === ScopeLevelEnum.ALL,
state: browserParams.state ? browserParams.state : filter == PullRequestFilterOption.ALL ? '' : filter,
...(activeTab === SpacePRTabs.REVIEW_REQUESTED && {
reviewer_id: Number(currentUser.id),
review_decision: reviewFilter
}),
...(activeTab === SpacePRTabs.CREATED && { created_by: Number(currentUser.id) }),
...(authorFilter && activeTab === SpacePRTabs.REVIEW_REQUESTED && { created_by: Number(authorFilter) }),
...(labelFilter.filter(({ type, valueId }) => type === 'label' || valueId === -1).length && {
label_id: labelFilter
.filter(({ type, valueId }) => type === 'label' || valueId === -1)
.map(({ labelId }) => labelId)
}),
...(labelFilter.filter(({ type }) => type === 'value').length && {
value_id: labelFilter
.filter(({ type, valueId }) => type === 'value' && valueId !== -1)
.map(({ valueId }) => valueId)
}),
...(page > 1
? pageAction?.action === PageAction.NEXT
? { updated_lt: pageAction.timestamp }
: { updated_gt: pageAction.timestamp }
: {})
},
queryParamStringifyOptions: {
arrayFormat: 'repeat'
},
debounce: 500,
lazy: !currentUser
})
const handleLabelClick = (labelFilterArr: LabelFilterObj[], clickedLabel: TypesLabelPullReqAssignmentInfo) => {
// if not present - add :
const isLabelAlreadyAdded = labelFilterArr.map(({ labelId }) => labelId).includes(clickedLabel.id || -1)
const updatedLabelsList = [...labelFilterArr]
if (!isLabelAlreadyAdded && clickedLabel?.id) {
if (clickedLabel.value && clickedLabel.value_id) {
updatedLabelsList.push({
labelId: clickedLabel.id,
type: LabelFilterType.VALUE,
valueId: clickedLabel.value_id,
labelObj: clickedLabel,
valueObj: {
id: clickedLabel.value_id,
color: clickedLabel.value_color,
label_id: clickedLabel.id,
value: clickedLabel.value
}
})
} else if (clickedLabel.value_count && !clickedLabel.value_id) {
updatedLabelsList.push({
labelId: clickedLabel.id,
type: LabelFilterType.VALUE,
valueId: -1,
labelObj: clickedLabel,
valueObj: {
id: -1,
color: clickedLabel.value_color,
label_id: clickedLabel.id,
value: getString('labels.anyValueOption')
}
})
} else {
updatedLabelsList.push({
labelId: clickedLabel.id,
type: LabelFilterType.LABEL,
valueId: undefined,
labelObj: clickedLabel,
valueObj: undefined
})
}
setLabelFilter(updatedLabelsList)
}
// if 'any value' label present - replace :
const replacedAnyValueIfPresent = updatedLabelsList.map(filterObj => {
if (
filterObj.valueId === -1 &&
filterObj.labelId === clickedLabel.id &&
clickedLabel.value_id &&
clickedLabel.value
) {
return {
...filterObj,
valueId: clickedLabel.value_id,
valueObj: {
id: clickedLabel.value_id,
color: clickedLabel.value_color,
label_id: clickedLabel.id,
value: clickedLabel.value
}
}
}
return filterObj
})
const isUpdated = !updatedLabelsList.every((obj, index) => obj === replacedAnyValueIfPresent[index])
if (isUpdated) {
setLabelFilter(replacedAnyValueIfPresent)
}
}
const columns: Column<TypesPullReqRepo>[] = useMemo(
() => [
{
id: 'title',
width: '100%',
Cell: ({ row }: CellProps<TypesPullReqRepo>) => {
//add type
const { pull_request, repository } = row.original
return (
<Link
className={css.rowLink}
to={routes.toCODEPullRequest({
repoPath: row?.original?.repository?.path as string,
pullRequestId: String(pull_request?.number)
})}>
<Layout.Horizontal className={css.titleRow} spacing="medium">
<PullRequestStateLabel iconSize={22} data={pull_request} iconOnly />
<Container padding={{ left: 'small' }}>
<Layout.Vertical spacing="small">
<Container>
<Layout.Horizontal flex={{ alignItems: 'center' }} className={css.prLabels}>
<Layout.Horizontal spacing={'xsmall'}>
<Text
icon="code-repo"
font={{ variation: FontVariation.SMALL_SEMI }}
color={Color.GREY_600}
border={{ right: true }}
padding={{ right: 'small' }}>
{repository?.identifier}
</Text>
<Text padding={{ left: 'xsmall' }} color={Color.GREY_800} className={css.title} lineClamp={1}>
{pull_request?.title}
</Text>
<Container className={css.convo}>
<Icon
className={css.convoIcon}
padding={{ left: 'small', right: 'small' }}
name="code-chat"
size={15}
/>
<Text font={{ variation: FontVariation.SMALL }} color={Color.GREY_500} tag="span">
{pull_request?.stats?.conversations}
</Text>
</Container>
</Layout.Horizontal>
<Render when={row.original && pull_request?.labels && pull_request.labels.length !== 0}>
{row.original?.pull_request?.labels?.map((label, index: number) => (
<Label
key={index}
name={label.key as string}
label_color={label.color as ColorName}
label_value={{
name: label.value as string,
color: label.value_color as ColorName
}}
scope={label.scope}
onClick={() => {
handleLabelClick(labelFilter, label)
}}
/>
))}
</Render>
</Layout.Horizontal>
</Container>
<Container>
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }}>
<Text color={Color.GREY_500} font={{ size: 'small' }}>
<StringSubstitute
str={getString('pr.statusLine')}
vars={{
state: pull_request?.state,
number: <Text inline>{pull_request?.number}</Text>,
time: (
<strong>
<TimePopoverWithLocal
time={defaultTo(
(pull_request?.state == PullRequestState.MERGED
? pull_request?.merged
: pull_request?.created) as number,
0
)}
inline={false}
font={{ variation: FontVariation.SMALL_BOLD }}
color={Color.GREY_500}
tag="span"
/>
</strong>
),
user: (
<strong>
{pull_request?.author?.display_name || pull_request?.author?.email || ''}
</strong>
)
}}
/>
</Text>
<PipeSeparator height={10} />
<Container>
<Layout.Horizontal
spacing="xsmall"
style={{ alignItems: 'center' }}
onClick={Utils.stopEvent}>
<GitRefLink
text={pull_request?.target_branch as string}
url={routes.toCODERepository({
repoPath: repository?.path as string,
gitRef: pull_request?.target_branch
})}
showCopy={false}
/>
<Text color={Color.GREY_500}></Text>
<GitRefLink
text={pull_request?.source_branch as string}
url={routes.toCODERepository({
repoPath: repository?.path as string,
gitRef: pull_request?.source_branch
})}
showCopy={false}
/>
</Layout.Horizontal>
</Container>
</Layout.Horizontal>
</Container>
</Layout.Vertical>
</Container>
<FlexExpander />
</Layout.Horizontal>
</Link>
)
}
}
],
[getString] // eslint-disable-line react-hooks/exhaustive-deps
)
return (
<Container className={css.main}>
<PageBody error={getErrorMessage(prError)} retryOnError={voidFn(refetchPrs)}>
<LoadingSpinner visible={prLoading && !searchTerm} withBorder={!searchTerm} />
<Render when={data}>
<Layout.Vertical>
<SpacePullRequestsContentHeader
activeTab={activeTab}
loading={prLoading && searchTerm !== undefined}
activePullRequestFilterOption={filter}
activePullRequestReviewFilterOption={reviewFilter}
onPullRequestFilterChanged={_filter => {
setFilter(_filter)
setPage(1)
}}
onPullRequestReviewFilterChanged={_reviewFilter => {
setReviewFilter(_reviewFilter)
setPage(1)
}}
onSearchTermChanged={value => {
setSearchTerm(value)
setPage(1)
}}
activePullRequestAuthorObj={principal}
activePullRequestAuthorFilterOption={authorFilter}
activePullRequestLabelFilterOption={labelFilter}
activePullRequestIncludeSubSpaceOption={includeSubspaces}
onPullRequestAuthorFilterChanged={_authorFilter => {
setAuthorFilter(_authorFilter)
setPage(1)
}}
onPullRequestLabelFilterChanged={_labelFilter => {
setLabelFilter(_labelFilter)
setPage(1)
}}
onPullRequestIncludeSubSpaceOptionChanged={_includeSubspaces => {
setIncludeSubspaces(_includeSubspaces as ScopeLevelEnum)
setPage(1)
}}
/>
<Container padding="xlarge">
<Container padding={{ top: 'medium', bottom: 'large' }}>
<Layout.Horizontal
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
style={{ flexWrap: 'wrap', gap: '5px' }}>
<Render when={!isEmpty(labelFilter) || !prLoading}>
<Text color={Color.GREY_400}>
{isEmpty(data)
? !isEmpty(labelFilter) && getString('labels.noResults')
: (stringSubstitute(getString('labels.prCount'), {
count: data?.length
}) as string)}
</Text>
</Render>
{labelFilter &&
labelFilter?.length !== 0 &&
labelFilter?.map((label, index) => (
<Label
key={index}
name={label.labelObj.key as string}
label_color={label.labelObj.color as ColorName}
label_value={{
name: label.valueObj?.value as string,
color: label.valueObj?.color as ColorName
}}
scope={label.labelObj.scope}
removeLabelBtn={true}
handleRemoveClick={() => {
if (label.type === 'value') {
const updateFilterObjArr = labelFilter.filter(filterObj => {
if (!(filterObj.labelId === label.labelId && filterObj.type === 'value')) {
return filterObj
}
})
setLabelFilter(updateFilterObjArr)
setPage(1)
} else if (label.type === 'label') {
const updateFilterObjArr = labelFilter.filter(filterObj => {
if (!(filterObj.labelId === label.labelId && filterObj.type === 'label')) {
return filterObj
}
})
setLabelFilter(updateFilterObjArr)
setPage(1)
}
}}
disableRemoveBtnTooltip={true}
/>
))}
</Layout.Horizontal>
</Container>
<Match expr={data?.length && !prLoading}>
<Truthy>
<>
<TableV2<any> //add type
className={css.table}
hideHeaders
columns={columns}
data={data || []}
getRowClassName={() => css.row}
onRowClick={noop}
/>
<PrevNextPagination
onPrev={
page > 1 && data
? () => {
setPage(pre => pre - 1)
setPageAction({ action: PageAction.PREV, timestamp: data[0].pull_request?.updated ?? 0 })
}
: false
}
onNext={
data && data?.length === LIST_FETCHING_LIMIT
? () => {
setPage(pre => pre + 1)
setPageAction({
action: PageAction.NEXT,
timestamp: data.slice(-1)[0]?.pull_request?.updated ?? 0
})
}
: false
}
/>
</>
</Truthy>
<Case val={0}>
<NoResultCard
forSearch={!!searchTerm}
forFilter={!isEmpty(labelFilter) || !isEmpty(authorFilter) || !isEmpty(filter)}
emptyFilterMessage={getString('pullRequestNotFoundforFilter')}
message={getString('pullRequestEmpty')}
buttonText={getString('newPullRequest')}
/>
</Case>
</Match>
</Container>
</Layout.Vertical>
</Render>
</PageBody>
</Container>
)
}

View File

@ -1586,6 +1586,17 @@ export interface TypesRepository {
updated?: number
}
export interface TypesPullReqLabelAssignInput {
label_id?: number
value?: string
value_id?: number | null
}
export interface TypesPullReqRepo {
pull_request?: TypesPullReq
repository?: TypesRepository
}
export interface TypesRepositoryPullReqSummary {
closed_count?: number
merged_count?: number
@ -1783,6 +1794,7 @@ export interface TypesUser {
created?: number
display_name?: string
email?: string
id?: number
uid?: string
updated?: number
}

File diff suppressed because it is too large Load Diff

View File

@ -133,6 +133,11 @@ export enum SettingsTab {
labels = 'labels'
}
export enum SpacePRTabs {
CREATED = 'created',
REVIEW_REQUESTED = 'review_requested'
}
export enum SpaceSettingsTab {
general = '/',
labels = 'labels'
@ -235,6 +240,12 @@ export const PullRequestFilterOption = {
ALL: 'all'
}
export const PullRequestReviewFilterOption = {
PENDING: 'pending',
APPROVED: 'approved',
CHANGES_REQUESTED: 'changereq'
}
export enum MergeStrategy {
MERGE = 'merge',
SQUASH = 'squash',

View File

@ -132,6 +132,8 @@ export interface PageBrowserProps {
page?: string
state?: string
tab?: string
review?: string
subspace?: ScopeLevelEnum
}
export const extractInfoFromRuleViolationArr = (ruleViolationArr: TypesRuleViolations[]) => {
@ -429,6 +431,11 @@ export const ButtonRoleProps = {
style: { cursor: 'pointer ' }
}
export enum ScopeLevel {
REPOSITORY = 'repos',
SPACE = 'space'
}
export enum orderSortDate {
ASC = 'asc',
DESC = 'desc'
@ -680,6 +687,11 @@ export enum ScopeLevelEnum {
CURRENT = 'current'
}
export enum PageAction {
NEXT = 'next',
PREV = 'previous'
}
export enum PullRequestCheckType {
EMPTY = '',
RAW = 'raw',