Merge pull request #17288 from tylerhekman-procore/master

plugin-github-pull-requests-board: Filter archived repos
This commit is contained in:
Ben Lambert
2023-04-18 10:49:38 +02:00
committed by GitHub
11 changed files with 583 additions and 5 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-github-pull-requests-board': patch
---
Add a new "Archived" Filter Options to the Github Pull Requests Dashboard.
When toggling this option on, the dashboard will display PRs from archived repositories.
These PRs will not be displayed in the default filter.
@@ -36,6 +36,7 @@ export const useGetPullRequestDetails = () => {
owner {
login
}
isArchived
}
title
url
@@ -25,6 +25,8 @@ type Props = {
authorName: string;
authorAvatar?: string;
repositoryName: string;
isDraft: boolean;
repositoryIsArchived: boolean;
};
const Card: FunctionComponent<Props> = (props: PropsWithChildren<Props>) => {
@@ -36,6 +38,8 @@ const Card: FunctionComponent<Props> = (props: PropsWithChildren<Props>) => {
authorName,
authorAvatar,
repositoryName,
isDraft,
repositoryIsArchived,
children,
} = props;
@@ -51,6 +55,8 @@ const Card: FunctionComponent<Props> = (props: PropsWithChildren<Props>) => {
authorName={authorName}
authorAvatar={authorAvatar}
repositoryName={repositoryName}
isDraft={isDraft}
repositoryIsArchived={repositoryIsArchived}
/>
{children}
</Box>
@@ -14,9 +14,11 @@
* limitations under the License.
*/
import React, { FunctionComponent } from 'react';
import { Typography, Box } from '@material-ui/core';
import { Typography, Box, Tooltip } from '@material-ui/core';
import { getElapsedTime } from '../../utils/functions';
import { UserHeader } from '../UserHeader';
import { DraftPrIcon } from '../icons/DraftPr';
import UnarchiveIcon from '@material-ui/icons/Unarchive';
type Props = {
title: string;
@@ -25,6 +27,8 @@ type Props = {
authorName: string;
authorAvatar?: string;
repositoryName: string;
isDraft: boolean;
repositoryIsArchived: boolean;
};
const CardHeader: FunctionComponent<Props> = (props: Props) => {
@@ -35,6 +39,8 @@ const CardHeader: FunctionComponent<Props> = (props: Props) => {
authorName,
authorAvatar,
repositoryName,
isDraft,
repositoryIsArchived,
} = props;
return (
@@ -45,6 +51,22 @@ const CardHeader: FunctionComponent<Props> = (props: Props) => {
</Typography>
<UserHeader name={authorName} avatar={authorAvatar} />
</Box>
<Box display="flex" justifyContent="left">
{isDraft && (
<Tooltip title="Draft PR">
<Box display="flex" justifyContent="center" alignItems="center">
<DraftPrIcon />
</Box>
</Tooltip>
)}
{repositoryIsArchived && (
<Tooltip title="Repository is archived">
<Box display="flex" justifyContent="center" alignItems="center">
<UnarchiveIcon />
</Box>
</Tooltip>
)}
</Box>
<Typography component="h3">
<b>{title}</b>
</Typography>
@@ -0,0 +1,257 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { EntityTeamPullRequestsCard } from '../EntityTeamPullRequestsCard';
import { PullRequestsColumn } from '../../utils/types';
import { render } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
jest.mock('../../hooks/useUserRepositoriesAndTeam', () => {
return {
useUserRepositoriesAndTeam: () => {
return {
loading: false,
repositories: ['team-login/team-repo'],
teamMembers: ['team-member'],
teamMembersOrganization: 'test-org',
};
},
};
});
jest.mock('../../hooks/usePullRequestsByTeam', () => {
const buildPullRequest = (
prTitle: string,
authorLogin: string,
repoName: string,
isDraft: boolean,
isArchived: boolean,
) => {
return {
id: 'id',
title: prTitle,
url: 'url',
lastEditedAt: 'last-edited-at',
latestReviews: {
nodes: [],
},
mergeable: true,
state: 'state',
reviewDecision: null,
createdAt: 'created-at',
repository: {
name: repoName,
owner: {
login: 'team-login',
},
isArchived: isArchived,
},
isDraft: isDraft,
author: {
login: authorLogin,
avatarUrl: 'avatar-url',
id: 'id',
email: 'email',
name: 'name',
},
};
};
const pullRequests: PullRequestsColumn[] = [
{
title: 'column',
content: [
buildPullRequest(
'non-team-non-draft-non-archive',
'non-team-member',
'team-repo',
false,
false,
),
buildPullRequest(
'non-team-non-draft-is-archive',
'non-team-member',
'team-repo',
false,
true,
),
buildPullRequest(
'non-team-is-draft-non-archive',
'non-team-member',
'team-repo',
true,
false,
),
buildPullRequest(
'non-team-is-draft-is-archive',
'non-team-member',
'team-repo',
true,
true,
),
buildPullRequest(
'is-team-non-draft-non-archive',
'team-member',
'non-team-repo',
false,
false,
),
buildPullRequest(
'is-team-non-draft-is-archive',
'team-member',
'non-team-repo',
false,
true,
),
buildPullRequest(
'is-team-is-draft-non-archive',
'team-member',
'non-team-repo',
true,
false,
),
buildPullRequest(
'is-team-is-draft-is-archive',
'team-member',
'non-team-repo',
true,
true,
),
],
},
];
return {
usePullRequestsByTeam: () => {
return {
loading: false,
pullRequests: pullRequests,
refreshPullRequest: () => {},
};
},
};
});
describe('EntityTeamPullRequestsCard', () => {
describe('non-team PRs', () => {
describe('non-draft PRs', () => {
it('should show non-team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, queryAllByTitle } = await render(
<EntityTeamPullRequestsCard />,
);
expect(getByText('non-team-non-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
it('should show non-team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('non-team-non-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
});
describe('draft PRs', () => {
it('should show draft non-team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
expect(getByText('non-team-is-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
it('should show draft non-team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('non-team-is-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
});
});
describe('team PRs', () => {
describe('non-draft PRs', () => {
it('should show team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
expect(getByText('is-team-non-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
it('should show team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('is-team-non-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
});
describe('draft PRs', () => {
it('should show draft team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
expect(getByText('is-team-is-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
it('should show draft team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsCard />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('is-team-is-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
});
});
});
@@ -29,6 +29,7 @@ import { PRCardFormating } from '../../utils/types';
import { shouldDisplayCard } from '../../utils/functions';
import { DraftPrIcon } from '../icons/DraftPr';
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
import UnarchiveIcon from '@material-ui/icons/Unarchive';
/** @public */
export interface EntityTeamPullRequestsCardProps {
@@ -71,6 +72,11 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
value: 'draft',
ariaLabel: 'Show draft PRs',
},
{
icon: <UnarchiveIcon />,
value: 'archivedRepo',
ariaLabel: 'Show archived repos',
},
{
icon: <FullscreenIcon />,
value: 'fullscreen',
@@ -127,6 +133,7 @@ const EntityTeamPullRequestsCard = (props: EntityTeamPullRequestsCardProps) => {
url={url}
reviews={latestReviews.nodes}
repositoryName={repository.name}
repositoryIsArchived={repository.isArchived}
isDraft={isDraft}
/>
),
@@ -0,0 +1,257 @@
/*
* Copyright 2023 The Backstage Authors
*
* 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 { EntityTeamPullRequestsContent } from '../EntityTeamPullRequestsContent';
import { PullRequestsColumn } from '../../utils/types';
import { render } from '@testing-library/react';
import { fireEvent } from '@testing-library/react';
jest.mock('../../hooks/useUserRepositoriesAndTeam', () => {
return {
useUserRepositoriesAndTeam: () => {
return {
loading: false,
repositories: ['team-login/team-repo'],
teamMembers: ['team-member'],
teamMembersOrganization: 'test-org',
};
},
};
});
jest.mock('../../hooks/usePullRequestsByTeam', () => {
const buildPullRequest = (
prTitle: string,
authorLogin: string,
repoName: string,
isDraft: boolean,
isArchived: boolean,
) => {
return {
id: 'id',
title: prTitle,
url: 'url',
lastEditedAt: 'last-edited-at',
latestReviews: {
nodes: [],
},
mergeable: true,
state: 'state',
reviewDecision: null,
createdAt: 'created-at',
repository: {
name: repoName,
owner: {
login: 'team-login',
},
isArchived: isArchived,
},
isDraft: isDraft,
author: {
login: authorLogin,
avatarUrl: 'avatar-url',
id: 'id',
email: 'email',
name: 'name',
},
};
};
const pullRequests: PullRequestsColumn[] = [
{
title: 'column',
content: [
buildPullRequest(
'non-team-non-draft-non-archive',
'non-team-member',
'team-repo',
false,
false,
),
buildPullRequest(
'non-team-non-draft-is-archive',
'non-team-member',
'team-repo',
false,
true,
),
buildPullRequest(
'non-team-is-draft-non-archive',
'non-team-member',
'team-repo',
true,
false,
),
buildPullRequest(
'non-team-is-draft-is-archive',
'non-team-member',
'team-repo',
true,
true,
),
buildPullRequest(
'is-team-non-draft-non-archive',
'team-member',
'non-team-repo',
false,
false,
),
buildPullRequest(
'is-team-non-draft-is-archive',
'team-member',
'non-team-repo',
false,
true,
),
buildPullRequest(
'is-team-is-draft-non-archive',
'team-member',
'non-team-repo',
true,
false,
),
buildPullRequest(
'is-team-is-draft-is-archive',
'team-member',
'non-team-repo',
true,
true,
),
],
},
];
return {
usePullRequestsByTeam: () => {
return {
loading: false,
pullRequests: pullRequests,
refreshPullRequest: () => {},
};
},
};
});
describe('EntityTeamPullRequestsContent', () => {
describe('non-team PRs', () => {
describe('non-draft PRs', () => {
it('should show non-team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, queryAllByTitle } = await render(
<EntityTeamPullRequestsContent />,
);
expect(getByText('non-team-non-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
it('should show non-team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('non-team-non-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
});
describe('draft PRs', () => {
it('should show draft non-team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
expect(getByText('non-team-is-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
it('should show draft non-team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('non-team-is-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
});
});
describe('team PRs', () => {
describe('non-draft PRs', () => {
it('should show team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
expect(getByText('is-team-non-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
it('should show team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('is-team-non-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(0);
});
});
describe('draft PRs', () => {
it('should show draft team PRs for un-archived repos when archived option is not checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
expect(getByText('is-team-is-draft-non-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(0);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
it('should show draft team PRs for archived repos when archived option is checked', async () => {
const { getByText, getAllByText, getByTitle, queryAllByTitle } =
await render(<EntityTeamPullRequestsContent />);
const teamToggle = getByTitle('Show PRs from your team');
fireEvent.click(teamToggle);
const draftToggle = getByTitle('Show draft PRs');
fireEvent.click(draftToggle);
const archiveToggle = getByTitle('Show archived repos');
fireEvent.click(archiveToggle);
expect(getByText('is-team-is-draft-is-archive')).toBeInTheDocument();
expect(getAllByText('non-team-repo')).toHaveLength(1);
expect(queryAllByTitle('Repository is archived')).toHaveLength(1);
expect(queryAllByTitle('Draft PR')).toHaveLength(1);
});
});
});
});
@@ -27,6 +27,7 @@ import { PRCardFormating } from '../../utils/types';
import { shouldDisplayCard } from '../../utils/functions';
import { DraftPrIcon } from '../icons/DraftPr';
import { useUserRepositoriesAndTeam } from '../../hooks/useUserRepositoriesAndTeam';
import UnarchiveIcon from '@material-ui/icons/Unarchive';
/** @public */
export interface EntityTeamPullRequestsContentProps {
@@ -71,6 +72,11 @@ const EntityTeamPullRequestsContent = (
value: 'draft',
ariaLabel: 'Show draft PRs',
},
{
icon: <UnarchiveIcon />,
value: 'archivedRepo',
ariaLabel: 'Show archived repos',
},
]}
/>
</InfoCardHeader>
@@ -119,6 +125,7 @@ const EntityTeamPullRequestsContent = (
url={url}
reviews={latestReviews.nodes}
repositoryName={repository.name}
repositoryIsArchived={repository.isArchived}
isDraft={isDraft}
/>
),
@@ -31,6 +31,7 @@ type Props = {
url: string;
reviews: Reviews;
repositoryName: string;
repositoryIsArchived: boolean;
isDraft: boolean;
};
@@ -43,6 +44,7 @@ const PullRequestCard: FunctionComponent<Props> = (props: Props) => {
url,
reviews,
repositoryName,
repositoryIsArchived,
isDraft,
} = props;
@@ -50,17 +52,17 @@ const PullRequestCard: FunctionComponent<Props> = (props: Props) => {
const commentsReviews = getCommentedReviews(reviews);
const changeRequests = getChangeRequests(reviews);
const cardTitle = isDraft ? `🔧 DRAFT - ${title}` : title;
return (
<Card
title={cardTitle}
title={title}
createdAt={createdAt}
updatedAt={updatedAt}
authorName={author.login}
authorAvatar={author.avatarUrl}
repositoryName={repositoryName}
prUrl={url}
isDraft={isDraft}
repositoryIsArchived={repositoryIsArchived}
>
{!!approvedReviews.length && (
<UserHeaderList
@@ -137,6 +137,11 @@ export const shouldDisplayCard = (
return false;
}
// hide PRs from archived repositories unless "archivedRepo" filter is toggled
if (infoCardFormat.includes('archivedRepo') !== repository.isArchived) {
return false;
}
// when "team" filter is toggled on, only shows PR from team members
if (infoCardFormat.includes('team')) {
return teamMembers.includes(author.login);
@@ -73,6 +73,7 @@ export type Repository = {
owner: {
login: string;
};
isArchived: boolean;
};
export type PullRequest = {
@@ -99,7 +100,12 @@ export type PullRequestsColumn = {
content: PullRequests;
};
export type PRCardFormating = 'compacted' | 'fullscreen' | 'draft' | 'team';
export type PRCardFormating =
| 'compacted'
| 'fullscreen'
| 'draft'
| 'team'
| 'archivedRepo';
export type ReviewDecision = 'IN_PROGRESS' | 'APPROVED' | 'REVIEW_REQUIRED';