From 0187f17d12874b0b4183c30deaec96af85c2b2ec Mon Sep 17 00:00:00 2001 From: Derek Worthen Date: Mon, 8 Apr 2024 09:53:50 -0700 Subject: [PATCH] Add pull request management --- .changeset/dirty-taxis-develop.md | 5 + src/electron/services/CommunityService.ts | 16 +- src/electron/services/GitHubService.ts | 20 +- .../community/manage/ManageCommunity.tsx | 3 + .../community/manage/issues/IssuesPanel.tsx | 2 +- .../manage/pullRequests/PullRequestsPanel.tsx | 250 ++++++++++++++++++ src/renderer/src/pages/layout/DataLoader.tsx | 11 +- .../src/store/communityManageStore.ts | 43 ++- src/renderer/src/store/types.ts | 9 +- src/shared/services/CommunityService.ts | 5 +- 10 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 .changeset/dirty-taxis-develop.md create mode 100644 src/renderer/src/pages/dashboard/community/manage/pullRequests/PullRequestsPanel.tsx diff --git a/.changeset/dirty-taxis-develop.md b/.changeset/dirty-taxis-develop.md new file mode 100644 index 0000000..6e7ac25 --- /dev/null +++ b/.changeset/dirty-taxis-develop.md @@ -0,0 +1,5 @@ +--- +'gov4git-desktop-app': patch +--- + +Add pull requests management diff --git a/src/electron/services/CommunityService.ts b/src/electron/services/CommunityService.ts index 7928094..664bfff 100644 --- a/src/electron/services/CommunityService.ts +++ b/src/electron/services/CommunityService.ts @@ -267,7 +267,7 @@ ${user.memberPublicBranch}` ): Promise<{ status: 'open' | 'closed'; url: string } | null> => { const repoSegments = urlToRepoSegments(community.projectUrl) const userJoinRequest = ( - await this.gitHubService.searchRepoIssues({ + await this.gitHubService.searchRepoIssuesOrPrs({ repoOwner: repoSegments.owner, repoName: repoSegments.repo, creator: user.username, @@ -520,7 +520,7 @@ ${user.memberPublicBranch}` const existingUsers = await this.getCommunityMembers(community) const usersAdded = new Set() const repoSegments = urlToRepoSegments(community.projectUrl) - const joinRequests = await this.gitHubService.searchRepoIssues({ + const joinRequests = await this.gitHubService.searchRepoIssuesOrPrs({ repoOwner: repoSegments.owner, repoName: repoSegments.repo, token: user.pat, @@ -642,8 +642,9 @@ ${user.memberPublicBranch}` await this.govService.mustRun(command, community) } - public getCommunityIssues = async ( + public getCommunityIssuesOrPrs = async ( communityUrl: string, + getPrs = false, ): Promise => { const user = await this.userService.getUser() if (user == null) { @@ -666,14 +667,17 @@ ${user.memberPublicBranch}` const policies = ( await this.policyService.getPolicies(communityUrl) - ).filter((p) => p.motionType === 'concern') + ).filter((p) => + getPrs ? p.motionType === 'proposal' : p.motionType === 'concern', + ) const repoSegments = urlToRepoSegments(community.projectUrl) - const issues = await this.gitHubService.searchRepoIssues({ + const issues = await this.gitHubService.searchRepoIssuesOrPrs({ repoOwner: repoSegments.owner, repoName: repoSegments.repo, token: user.pat, state: 'open', + pullRequests: getPrs, }) const communityIssues: CommunityIssue[] = issues.map((issue) => { @@ -699,7 +703,7 @@ ${user.memberPublicBranch}` } } - public manageIssue = async ({ + public manageIssueOrPr = async ({ communityUrl, issueNumber, label, diff --git a/src/electron/services/GitHubService.ts b/src/electron/services/GitHubService.ts index 6e70e21..8ef106c 100644 --- a/src/electron/services/GitHubService.ts +++ b/src/electron/services/GitHubService.ts @@ -76,6 +76,7 @@ export type SearchRepoIssuesArgs = { creator?: string title?: string state?: 'open' | 'closed' | 'all' + pullRequests?: boolean } export type IssueSearchResults = { @@ -407,13 +408,14 @@ export class GitHubService { ) } - public searchRepoIssues = async ({ + public searchRepoIssuesOrPrs = async ({ repoOwner, repoName, creator, token, title, state = 'all', + pullRequests = false, }: SearchRepoIssuesArgs) => { let allResponses: IssueSearchResults[] = [] let currentResponse @@ -435,8 +437,20 @@ export class GitHubService { allResponses = [ ...allResponses, ...currentResponse.data.filter((i: any) => { - if (title == null) return !('pull_request' in i) - return !('pull_request' in i) && i.title === title + let keep = true + if (title != null && i.title !== title) { + keep = false + } + const isPullRequest = 'pull_request' in i + if (pullRequests && !isPullRequest) { + keep = false + } + if (!pullRequests && isPullRequest) { + keep = false + } + return keep + // if (title == null) return !('pull_request' in i) + // return !('pull_request' in i) && i.title === title }), ] page += 1 diff --git a/src/renderer/src/pages/dashboard/community/manage/ManageCommunity.tsx b/src/renderer/src/pages/dashboard/community/manage/ManageCommunity.tsx index e9cd6f4..cfb71f2 100644 --- a/src/renderer/src/pages/dashboard/community/manage/ManageCommunity.tsx +++ b/src/renderer/src/pages/dashboard/community/manage/ManageCommunity.tsx @@ -3,6 +3,7 @@ import { type FC, memo, useMemo } from 'react' import { useDataStore } from '../../../../store/store.js' import { IssuesPanel } from './issues/IssuesPanel.js' import { ManageCommunityOverview } from './overview/ManageCommunityOverview.js' +import { PullRequestsPanel } from './pullRequests/PullRequestsPanel.js' import { UserPanel } from './users/UserPanel.js' export const ManageCommunity: FC = memo(function ManageCommunity() { @@ -14,6 +15,8 @@ export const ManageCommunity: FC = memo(function ManageCommunity() { return UserPanel case 'issues': return IssuesPanel + case 'pull-requests': + return PullRequestsPanel default: return ManageCommunityOverview } diff --git a/src/renderer/src/pages/dashboard/community/manage/issues/IssuesPanel.tsx b/src/renderer/src/pages/dashboard/community/manage/issues/IssuesPanel.tsx index 05341c1..50b3836 100644 --- a/src/renderer/src/pages/dashboard/community/manage/issues/IssuesPanel.tsx +++ b/src/renderer/src/pages/dashboard/community/manage/issues/IssuesPanel.tsx @@ -39,7 +39,7 @@ export const IssuesPanel: FC = memo(function IssuesPanel() { null, ) const [selectedPolicy, setSelectedPolicy] = useState(null) - const manageIssue = useDataStore((s) => s.communityManage.manageIssue) + const manageIssue = useDataStore((s) => s.communityManage.manageIssueOrPr) const issues = useDataStore((s) => s.communityManage.issues) const [filteredIssues, setFilteredIssues] = useState(issues?.issues) const issuesLoading = useDataStore((s) => s.communityManage.issuesLoading) diff --git a/src/renderer/src/pages/dashboard/community/manage/pullRequests/PullRequestsPanel.tsx b/src/renderer/src/pages/dashboard/community/manage/pullRequests/PullRequestsPanel.tsx new file mode 100644 index 0000000..56f93d9 --- /dev/null +++ b/src/renderer/src/pages/dashboard/community/manage/pullRequests/PullRequestsPanel.tsx @@ -0,0 +1,250 @@ +import { + Button, + Dropdown, + Option, + Table, + TableBody, + TableCell, + TableHeader, + TableHeaderCell, + TableRow, +} from '@fluentui/react-components' +import { SearchBox } from '@fluentui/react-search-preview' +import { parse } from 'marked' +import { type FC, memo, useCallback, useEffect, useMemo, useState } from 'react' + +import { debounceAsync } from '~/shared' + +import { Policy } from '../../../../../../../electron/db/schema.js' +import type { CommunityIssue } from '../../../../../../../electron/services/index.js' +import { Loader } from '../../../../../components/Loader.js' +import { Message } from '../../../../../components/Message.js' +import { useDataStore } from '../../../../../store/store.js' +import { useMessageStyles } from '../../../../../styles/messages.js' +import { useManageCommunityStyles } from '../styles.js' + +const isManaged = (pullRequest: CommunityIssue): boolean => { + return pullRequest.policy != null +} + +export const PullRequestsPanel: FC = memo(function PullRequestsPanel() { + const styles = useManageCommunityStyles() + const messageStyles = useMessageStyles() + const [loading, setLoading] = useState(false) + const [successMessage, setSuccessMessage] = useState('') + const selectedCommunity = useDataStore( + (s) => s.communityManage.communityToManage, + )! + const [selectedPr, setSelectedPr] = useState(null) + const [selectedPolicy, setSelectedPolicy] = useState(null) + const managePr = useDataStore((s) => s.communityManage.manageIssueOrPr) + const pullRequests = useDataStore((s) => s.communityManage.pullRequests) + const [filteredPrs, setFilteredPrs] = useState(pullRequests?.issues) + const pullRequestsLoading = useDataStore( + (s) => s.communityManage.pullRequestsLoading, + ) + const [showManageButton, setShowManageButton] = useState(true) + const [search, setSearch] = useState('') + const [searchBox, setSearchBox] = useState('') + + const debounceSetSearch = useMemo(() => { + return debounceAsync(setSearch) + }, [setSearch]) + + useEffect(() => { + debounceSetSearch(searchBox) + }, [searchBox, debounceSetSearch]) + + useEffect(() => { + if (search !== '') { + const filteredIssues = pullRequests?.issues.filter((i) => { + return i.title.toLowerCase().includes(search.toLowerCase()) + }) + setFilteredPrs(filteredIssues) + } else { + setFilteredPrs(pullRequests?.issues) + } + }, [search, pullRequests, setFilteredPrs]) + + const selectPolicy = useCallback( + (policy: Policy) => { + console.log('POLICY') + console.log(policy) + setSelectedPolicy(policy) + }, + [setSelectedPolicy], + ) + + const onSelect = useCallback( + (issue: CommunityIssue) => { + setSelectedPr(issue) + setShowManageButton(true) + setSelectedPolicy(null) + }, + [setSelectedPr, setShowManageButton, setSelectedPolicy], + ) + + const manage = useCallback(async () => { + if (selectedPr != null && selectedPolicy != null) { + setLoading(true) + await managePr({ + communityUrl: selectedCommunity.url, + issueNumber: selectedPr.number, + label: selectedPolicy.githubLabel, + }) + setSuccessMessage( + [ + `Success. Pull Request #${selectedPr.number}, ${selectedPr.title}, is now marked to be managed by Gov4Git.`, + 'It may take take a few hours before the system creates a ballot for the issue.', + ].join(' '), + ) + setLoading(false) + } + }, [managePr, setLoading, selectedCommunity, selectedPr, selectedPolicy]) + + const dismissMessage = useCallback(() => { + setSuccessMessage('') + }, [setSuccessMessage]) + + return ( + +
+
+
+ setSearchBox(e.target.value)} + dismiss={ + // eslint-disable-next-line + setSearchBox('')} + className="codicon codicon-chrome-close" + > + } + /> +
+
+
+ + + + Managed + Pull Request + + + + {filteredPrs != null && + filteredPrs.map((i) => ( + onSelect(i)} + className={ + selectedPr != null && selectedPr.id === i.id + ? styles.selectedRow + : '' + } + > + + + {isManaged(i) && } + + + {i.title} + + ))} + +
+
+ {selectedPr != null && ( +
+ {successMessage !== '' && ( + + )} +
+ {!isManaged(selectedPr) && ( + <> + {showManageButton && ( + + )} + {!showManageButton && ( + <> +
+
Select a Policy:
+ + {pullRequests?.policies.map((p) => ( + + ))} + +
+ {selectedPolicy != null && ( + <> +
+
+ +
+ + )} + + )} + + )} + {isManaged(selectedPr) && ( + + Managed with Gov4Git using {selectedPr.policy?.title} + + )} +
+ +
+

{selectedPr.title}

+ + {selectedPr.html_url} + +
+
+
+ )} +
+ ) +}) diff --git a/src/renderer/src/pages/layout/DataLoader.tsx b/src/renderer/src/pages/layout/DataLoader.tsx index 13753c6..29c7b1a 100644 --- a/src/renderer/src/pages/layout/DataLoader.tsx +++ b/src/renderer/src/pages/layout/DataLoader.tsx @@ -20,6 +20,9 @@ export const DataLoader: FC = function DataLoader() { const fetchCommunityIssues = useDataStore( (s) => s.communityManage.fetchCommunityIssues, ) + const fetchCommunityPrs = useDataStore( + (s) => s.communityManage.fetchCommunityPullRequests, + ) useEffect(() => { let shouldUpdate = true @@ -48,6 +51,7 @@ export const DataLoader: FC = function DataLoader() { await Promise.allSettled([ fetchCommunityUsers(communityToManage, false, () => shouldUpdate), fetchCommunityIssues(communityToManage, false, () => shouldUpdate), + fetchCommunityPrs(communityToManage, false, () => shouldUpdate), ]) } } @@ -57,7 +61,12 @@ export const DataLoader: FC = function DataLoader() { return () => { shouldUpdate = false } - }, [communityToManage, fetchCommunityUsers, fetchCommunityIssues]) + }, [ + communityToManage, + fetchCommunityUsers, + fetchCommunityIssues, + fetchCommunityPrs, + ]) useEffect(() => { // void refreshCache() diff --git a/src/renderer/src/store/communityManageStore.ts b/src/renderer/src/store/communityManageStore.ts index 2dc77ea..1a366a3 100644 --- a/src/renderer/src/store/communityManageStore.ts +++ b/src/renderer/src/store/communityManageStore.ts @@ -16,8 +16,8 @@ const fetchUsers = serialAsync(async (url: string) => { return await communityService.getCommunityUsers(url) }) -const fetchIssues = serialAsync(async (url: string) => { - return await communityService.getCommunityIssues(url) +const fetchIssuesOrPrs = serialAsync(async (url: string, pr = false) => { + return await communityService.getCommunityIssuesOrPrs(url, pr) }) export const createCommunityManageStore: StateCreator< @@ -33,6 +33,8 @@ export const createCommunityManageStore: StateCreator< users: null, issuesLoading: false, issues: null, + pullRequestsLoading: false, + pullRequests: null, setState: (state) => { set((s) => { s.communityManage.state = state @@ -81,7 +83,9 @@ export const createCommunityManageStore: StateCreator< s.communityManage.issuesLoading = true }) } - const issues = await communityService.getCommunityIssues(community.url) + const issues = await communityService.getCommunityIssuesOrPrs( + community.url, + ) if (shouldUpdate()) { set((s) => { s.communityManage.issues = issues @@ -92,6 +96,31 @@ export const createCommunityManageStore: StateCreator< } }, `Failed to load issues for community ${community.name}`) }, + fetchCommunityPullRequests: async ( + community: Community, + silent = true, + shouldUpdate = () => true, + ) => { + await get().tryRun(async () => { + if (!silent) { + set((s) => { + s.communityManage.pullRequestsLoading = true + }) + } + const pullRequests = await communityService.getCommunityIssuesOrPrs( + community.url, + true, + ) + if (shouldUpdate()) { + set((s) => { + s.communityManage.pullRequests = pullRequests + if (!silent) { + s.communityManage.pullRequestsLoading = false + } + }) + } + }, `Failed to load pull requests for community ${community.name}`) + }, issueVotingCredits: serialAsync(async (args: IssueVotingCreditsArgs) => { await get().tryRun(async () => { await communityService.issueVotingCredits(args) @@ -102,12 +131,14 @@ export const createCommunityManageStore: StateCreator< await get().refreshCache(false) }, `Failed to issue ${args.credits} voting credits to ${args.username}.`) }), - manageIssue: serialAsync(async (args: ManageIssueArgs) => { + manageIssueOrPr: serialAsync(async (args: ManageIssueArgs) => { await get().tryRun(async () => { - await communityService.manageIssue(args) - const newIssues = await fetchIssues(args.communityUrl) + await communityService.manageIssueOrPr(args) + const newIssues = await fetchIssuesOrPrs(args.communityUrl) + const newPrs = await fetchIssuesOrPrs(args.communityUrl, true) set((s) => { s.communityManage.issues = newIssues + s.communityManage.pullRequests = newPrs }) }, `Failed to manage issue #${args.issueNumber}.`) }), diff --git a/src/renderer/src/store/types.ts b/src/renderer/src/store/types.ts index b0a57fd..4452080 100644 --- a/src/renderer/src/store/types.ts +++ b/src/renderer/src/store/types.ts @@ -134,19 +134,26 @@ export type CommunityManageStore = { users: CommunityUser[] | null issuesLoading: boolean issues: CommunityIssuesResponse | null + pullRequestsLoading: boolean + pullRequests: CommunityIssuesResponse | null setCommunity: (community: Community) => void fetchCommunityIssues: ( community: Community, silent?: boolean, shouldUpdate?: () => boolean, ) => Promise + fetchCommunityPullRequests: ( + community: Community, + silent?: boolean, + shouldUpdate?: () => boolean, + ) => Promise fetchCommunityUsers: ( Community: Community, silent?: boolean, shouldUpdate?: () => boolean, ) => Promise issueVotingCredits: (credits: IssueVotingCreditsArgs) => Promise - manageIssue: (args: ManageIssueArgs) => Promise + manageIssueOrPr: (args: ManageIssueArgs) => Promise approveUserRequest: ( community: Community, user: CommunityUser, diff --git a/src/shared/services/CommunityService.ts b/src/shared/services/CommunityService.ts index f710c51..ba36296 100644 --- a/src/shared/services/CommunityService.ts +++ b/src/shared/services/CommunityService.ts @@ -21,10 +21,11 @@ export abstract class AbstractCommunityService { public abstract issueVotingCredits( args: IssueVotingCreditsArgs, ): Promise - public abstract getCommunityIssues( + public abstract getCommunityIssuesOrPrs( communityUrl: string, + getPrs?: boolean, ): Promise - public abstract manageIssue(args: ManageIssueArgs): Promise + public abstract manageIssueOrPr(args: ManageIssueArgs): Promise public abstract deleteCommunity(url: string): Promise public abstract approveUserRequest( community: Community,