Skip to content

Commit

Permalink
Merge pull request #137 from gov4git/pull-management
Browse files Browse the repository at this point in the history
Add pull request management
  • Loading branch information
dworthen committed Apr 8, 2024
2 parents ab15810 + 0187f17 commit 3c11d1a
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-taxis-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gov4git-desktop-app': patch
---

Add pull requests management
16 changes: 10 additions & 6 deletions src/electron/services/CommunityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -520,7 +520,7 @@ ${user.memberPublicBranch}`
const existingUsers = await this.getCommunityMembers(community)
const usersAdded = new Set<string>()
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,
Expand Down Expand Up @@ -642,8 +642,9 @@ ${user.memberPublicBranch}`
await this.govService.mustRun(command, community)
}

public getCommunityIssues = async (
public getCommunityIssuesOrPrs = async (
communityUrl: string,
getPrs = false,
): Promise<CommunityIssuesResponse> => {
const user = await this.userService.getUser()
if (user == null) {
Expand All @@ -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) => {
Expand All @@ -699,7 +703,7 @@ ${user.memberPublicBranch}`
}
}

public manageIssue = async ({
public manageIssueOrPr = async ({
communityUrl,
issueNumber,
label,
Expand Down
20 changes: 17 additions & 3 deletions src/electron/services/GitHubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type SearchRepoIssuesArgs = {
creator?: string
title?: string
state?: 'open' | 'closed' | 'all'
pullRequests?: boolean
}

export type IssueSearchResults = {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const IssuesPanel: FC = memo(function IssuesPanel() {
null,
)
const [selectedPolicy, setSelectedPolicy] = useState<Policy | null>(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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CommunityIssue | null>(null)
const [selectedPolicy, setSelectedPolicy] = useState<Policy | null>(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 (
<Loader isLoading={pullRequestsLoading}>
<div className={styles.tableArea}>
<div className={styles.searchControls}>
<div className={styles.searchBox}>
<SearchBox
className={styles.searchInput}
size="medium"
placeholder="Search"
value={searchBox}
onChange={(e: any) => setSearchBox(e.target.value)}
dismiss={
// eslint-disable-next-line
<i
onClick={() => setSearchBox('')}
className="codicon codicon-chrome-close"
></i>
}
/>
</div>
<div></div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Managed</TableHeaderCell>
<TableHeaderCell>Pull Request</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{filteredPrs != null &&
filteredPrs.map((i) => (
<TableRow
key={i.id}
onClick={() => onSelect(i)}
className={
selectedPr != null && selectedPr.id === i.id
? styles.selectedRow
: ''
}
>
<TableCell>
<span
style={{
color: 'var(--colorBrandBackground)',
fontSize: '1.2rem',
}}
>
{isManaged(i) && <i className="codicon codicon-check" />}
</span>
</TableCell>
<TableCell>{i.title}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{selectedPr != null && (
<div className={styles.selectedIssueArea}>
{successMessage !== '' && (
<Message
className={messageStyles.success}
messages={[successMessage]}
onClose={dismissMessage}
/>
)}
<div className={styles.manageIssueFormArea}>
{!isManaged(selectedPr) && (
<>
{showManageButton && (
<Button
appearance="primary"
onClick={() => setShowManageButton(false)}
>
Manage with Gov4Git
</Button>
)}
{!showManageButton && (
<>
<div>
<div>Select a Policy:</div>
<Dropdown placeholder="Select a policy">
{pullRequests?.policies.map((p) => (
<Option
key={p.title}
value={p.title}
onClick={() => selectPolicy(p)}
>
{p.title}
</Option>
))}
</Dropdown>
</div>
{selectedPolicy != null && (
<>
<div
dangerouslySetInnerHTML={{
__html: parse(selectedPolicy.description ?? ''),
}}
></div>
<div>
<Button
disabled={loading || selectedPolicy == null}
onClick={manage}
appearance="primary"
>
{loading && (
<i className="codicon codicon-loading codicon-modifier-spin" />
)}
Manage with Gov4Git using {selectedPolicy?.title}
</Button>
</div>
</>
)}
</>
)}
</>
)}
{isManaged(selectedPr) && (
<strong>
Managed with Gov4Git using {selectedPr.policy?.title}
</strong>
)}
</div>

<hgroup className={styles.titleArea}>
<h2>{selectedPr.title}</h2>
<a href={selectedPr.html_url} target="_blank" rel="noreferrer">
{selectedPr.html_url}
</a>
</hgroup>
<div
className={styles.description}
dangerouslySetInnerHTML={{
__html: parse(selectedPr.body ?? ''),
}}
></div>
</div>
)}
</Loader>
)
})
Loading

0 comments on commit 3c11d1a

Please sign in to comment.