From d8dcb74ff4d0b7fd1a49aa63b36851452fb9e9af Mon Sep 17 00:00:00 2001 From: Sochima Biereagu Date: Fri, 4 Aug 2023 11:36:10 +0100 Subject: [PATCH] [CRA-RXJS-SC] Fix PR API fetch (#580) * chore: init * chore: react-paginate * chore: fetch open/closed PRs separately to accurately get count * chore: add pagination to PRs * chore: format * chore: fixed broken pull request page --------- Co-authored-by: Jerry Hogan --- cra-rxjs-styled-components/package-lock.json | 20 +++ cra-rxjs-styled-components/package.json | 1 + .../pull-request/PullRequest.stories.tsx | 14 +- .../pull-request/PullRequest.data.tsx | 92 +++++++++---- .../pull-request/PullRequest.style.tsx | 129 ++++++------------ .../pull-request/PullRequest.type.ts | 7 +- .../pull-request/PullRequest.view.tsx | 38 +++++- .../src/constants/url.constants.ts | 15 +- .../routes/repo/repository-pull-request.tsx | 5 +- 9 files changed, 192 insertions(+), 129 deletions(-) diff --git a/cra-rxjs-styled-components/package-lock.json b/cra-rxjs-styled-components/package-lock.json index 0c1a06eb0..1a8c5ce10 100644 --- a/cra-rxjs-styled-components/package-lock.json +++ b/cra-rxjs-styled-components/package-lock.json @@ -25,6 +25,7 @@ "react": "^17.0.2", "react-content-loader": "^6.2.0", "react-dom": "^17.0.2", + "react-paginate": "^8.1.3", "react-router-dom": "^6.2.1", "react-scripts": "5.0.1", "rxjs": "^7.5.2", @@ -28421,6 +28422,17 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/react-paginate": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.2.0.tgz", + "integrity": "sha512-sJCz1PW+9PNIjUSn919nlcRVuleN2YPoFBOvL+6TPgrH/3lwphqiSOgdrLafLdyLDxsgK+oSgviqacF4hxsDIw==", + "dependencies": { + "prop-types": "^15" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -55071,6 +55083,14 @@ "integrity": "sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==", "dev": true }, + "react-paginate": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.2.0.tgz", + "integrity": "sha512-sJCz1PW+9PNIjUSn919nlcRVuleN2YPoFBOvL+6TPgrH/3lwphqiSOgdrLafLdyLDxsgK+oSgviqacF4hxsDIw==", + "requires": { + "prop-types": "^15" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", diff --git a/cra-rxjs-styled-components/package.json b/cra-rxjs-styled-components/package.json index 752a95818..99d8205a7 100644 --- a/cra-rxjs-styled-components/package.json +++ b/cra-rxjs-styled-components/package.json @@ -18,6 +18,7 @@ "react": "^17.0.2", "react-content-loader": "^6.2.0", "react-dom": "^17.0.2", + "react-paginate": "^8.1.3", "react-router-dom": "^6.2.1", "react-scripts": "5.0.1", "rxjs": "^7.5.2", diff --git a/cra-rxjs-styled-components/src/components/pull-request/PullRequest.stories.tsx b/cra-rxjs-styled-components/src/components/pull-request/PullRequest.stories.tsx index 91ac06664..48500da81 100644 --- a/cra-rxjs-styled-components/src/components/pull-request/PullRequest.stories.tsx +++ b/cra-rxjs-styled-components/src/components/pull-request/PullRequest.stories.tsx @@ -29,7 +29,7 @@ OpenPullRequests.args = { state: 'open', messageCount: 0, merged_at: null, - review_comments_url: '/', + repository_url: '/', comments: '', }, { @@ -40,7 +40,7 @@ OpenPullRequests.args = { state: 'open', messageCount: 0, merged_at: null, - review_comments_url: '/', + repository_url: '/', comments: '', }, { @@ -51,7 +51,7 @@ OpenPullRequests.args = { state: 'open', messageCount: 0, merged_at: null, - review_comments_url: '/', + repository_url: '/', comments: '', }, ], @@ -72,7 +72,7 @@ MergedPullRequests.args = { messageCount: 0, isMerged: true, merged_at: date, - review_comments_url: '/', + repository_url: '/', comments: '', }, { @@ -84,7 +84,7 @@ MergedPullRequests.args = { messageCount: 0, isMerged: true, merged_at: date, - review_comments_url: '/', + repository_url: '/', comments: '', }, ], @@ -103,7 +103,7 @@ ClosedPullRequests.args = { state: 'closed', messageCount: 0, merged_at: null, - review_comments_url: '/', + repository_url: '/', comments: '', }, { @@ -114,7 +114,7 @@ ClosedPullRequests.args = { state: 'closed', messageCount: 0, merged_at: null, - review_comments_url: '/', + repository_url: '/', comments: '', }, ], diff --git a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.data.tsx b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.data.tsx index fc1813d0a..cfe13bd4c 100644 --- a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.data.tsx +++ b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.data.tsx @@ -10,75 +10,115 @@ import { import React, { useEffect, useState } from 'react'; import type { PRTabValues } from '../types'; -import { PULLS_URL } from '../../../constants/url.constants'; -import type { PullRequest } from './PullRequest.type'; +import { + OPEN_PULLS_URL, + CLOSED_PULLS_URL, +} from '../../../constants/url.constants'; +import type { PullRequest, PullRequests } from './PullRequest.type'; import PullRequestView from './PullRequest.view'; import { fromFetchWithAuth } from '../../../hooks/auth/from-fetch-with-auth'; import { useParams } from 'react-router-dom'; export default function PullRequestCtrl() { const [activeTab, setActiveTab] = useState('open'); - const [pullRequests, setPullRequests] = useState([]); + const [openPRPage, setOpenPRPage] = useState(1); + const [closedPRPage, setClosedPRPage] = useState(1); + const setPRPage = activeTab === 'open' ? setOpenPRPage : setClosedPRPage; + const [openPullRequests, setOpenPullRequests] = useState({ + total_count: 0, + items: [], + }); + const [closedPullRequests, setClosedPullRequests] = useState({ + total_count: 0, + items: [], + }); + const { username, repo } = useParams(); useEffect(() => { if (username && repo) { - const subscription: Subscription = fromFetchWithAuth( - PULLS_URL(username, repo), + const openPRSubscription: Subscription = fromFetchWithAuth( + OPEN_PULLS_URL(username, repo, openPRPage), + { + selector: async (response: Response) => { + const res = await response.json(); + return res as any; + }, + } + ) + .pipe( + filter((pulls) => !!pulls.total_count), + switchMap((pulls: PullRequests) => { + const requests = pulls.items.map(createCommentCountRequest); + return zip(...requests).pipe( + map(mergePullRequestsWithCommentCount(pulls)) + ); + }), + tap(setOpenPullRequests) + ) + .subscribe(); + + const closedPRSubscription: Subscription = fromFetchWithAuth( + CLOSED_PULLS_URL(username, repo, closedPRPage), { - selector: (response: Response) => { - return response.json(); + selector: async (response: Response) => { + const res = await response.json(); + return res as any; }, } ) .pipe( - filter((pulls) => !!pulls.length), - switchMap((pulls: PullRequest[]) => { - const requests = pulls.map(createCommentCountRequest); + filter((pulls) => !!pulls.total_count), + switchMap((pulls: PullRequests) => { + const requests = pulls.items.map(createCommentCountRequest); return zip(...requests).pipe( map(mergePullRequestsWithCommentCount(pulls)) ); }), - tap(setPullRequests) + tap(setClosedPullRequests) ) .subscribe(); return () => { - subscription.unsubscribe(); + openPRSubscription.unsubscribe(); + closedPRSubscription.unsubscribe(); }; } - }, [username, repo]); + }, [username, repo, openPRPage, closedPRPage]); - const OPEN_PRS: PullRequest[] = pullRequests?.filter( - (pr) => pr.state === 'open' - ); - const CLOSED_PRS: PullRequest[] = pullRequests?.filter( - (pr) => pr.state === 'closed' - ); + const PRS = + activeTab === 'open' ? openPullRequests.items : closedPullRequests.items; return ( ); } function createCommentCountRequest(pr: PullRequest): Observable { - return fromFetchWithAuth(pr.review_comments_url, { + const review_comments_url = `${pr.repository_url}/pulls/${pr.number}/comments`; + return fromFetchWithAuth(review_comments_url, { selector: (response: Response) => { return response.json(); }, }); } -function mergePullRequestsWithCommentCount(pulls: PullRequest[]) { - return (counts: number[]): PullRequest[] => { - return pulls.map((p: PullRequest, index: number) => ({ +function mergePullRequestsWithCommentCount(pulls: PullRequests) { + return (counts: number[]): PullRequests => { + const items = pulls.items.map((p: PullRequest, index: number) => ({ ...p, comments: counts[index], })); + return { + total_count: pulls.total_count, + items, + }; }; } diff --git a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.style.tsx b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.style.tsx index 12937d477..7ecf7696d 100644 --- a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.style.tsx +++ b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.style.tsx @@ -13,100 +13,57 @@ export const Content = styled.div` border-radius: 6px; `; export const PaginationContainer = styled.div` - display: flex; - justify-content: center; padding: 10px 0; - & > span { - min-width: 32px; - padding: 5px 10px; - font-style: normal; - line-height: 20px; - color: ${colors.gray800}; - text-align: center; - white-space: nowrap; - vertical-align: middle; + /* pagination */ + .pagination { + display: flex; + padding: 0; + justify-content: center; + list-style: none; cursor: pointer; - -webkit-user-select: none; - user-select: none; - border: 1px solid transparent; - border-radius: 6px; - transition: border-color 0.2s cubic-bezier(0.3, 0, 0.5, 1); } - & .disabled { - color: ${colors.gray400}; - cursor: default; - border-color: transparent; + .pagination a { + padding: 1px; + border-radius: 5px; + color: var(--default-text-color); + font-size: 14px; + padding: 5px 10px; + margin: 0 5px; + } + + .pagination__link { + font-weight: bold; + } + + .pagination__item a:hover { + text-decoration: none; + border: 1px solid var(--text-muted); + } + + .pagination__link_end a { + color: #539bf5; + text-decoration: none; + padding: 7px 10px; + } + .pagination__link_end a:hover { + border: 1px solid var(--text-muted); } - & > .prev { - position: relative; - &::before { - display: inline-block; - width: 16px; - height: 16px; - vertical-align: text-bottom; - content: ''; - background-color: currentColor; - margin-right: 4px; - -webkit-clip-path: polygon( - 9.8px 12.8px, - 8.7px 12.8px, - 4.5px 8.5px, - 4.5px 7.5px, - 8.7px 3.2px, - 9.8px 4.3px, - 6.1px 8px, - 9.8px 11.7px, - 9.8px 12.8px - ); - clip-path: polygon( - 9.8px 12.8px, - 8.7px 12.8px, - 4.5px 8.5px, - 4.5px 7.5px, - 8.7px 3.2px, - 9.8px 4.3px, - 6.1px 8px, - 9.8px 11.7px, - 9.8px 12.8px - ); - } + .pagination__link--active a { + color: #fff; + background: #316dca; + } + .pagination__link--active a:hover { + border: none !important; } - & > .next { - position: relative; - &::after { - display: inline-block; - width: 16px; - height: 16px; - vertical-align: text-bottom; - content: ''; - background-color: currentColor; - margin-left: 4px; - -webkit-clip-path: polygon( - 6.2px 3.2px, - 7.3px 3.2px, - 11.5px 7.5px, - 11.5px 8.5px, - 7.3px 12.8px, - 6.2px 11.7px, - 9.9px 8px, - 6.2px 4.3px, - 6.2px 3.2px - ); - clip-path: polygon( - 6.2px 3.2px, - 7.3px 3.2px, - 11.5px 7.5px, - 11.5px 8.5px, - 7.3px 12.8px, - 6.2px 11.7px, - 9.9px 8px, - 6.2px 4.3px, - 6.2px 3.2px - ); - } + .pagination__link--disabled, + .pagination__link--disabled a { + color: #545d68 !important; + text-decoration: none !important; + border: none !important; + cursor: default; } `; diff --git a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.type.ts b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.type.ts index 59be84641..8a1fbdcbd 100644 --- a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.type.ts +++ b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.type.ts @@ -9,6 +9,11 @@ export type PullRequest = { messageCount: number; isMerged?: boolean; merged_at: string | null; - review_comments_url: string; + repository_url: string; comments: any; }; + +export type PullRequests = { + total_count: number; + items: PullRequest[]; +}; diff --git a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.view.tsx b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.view.tsx index adebd1931..905c59455 100644 --- a/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.view.tsx +++ b/cra-rxjs-styled-components/src/components/pull-request/pull-request/PullRequest.view.tsx @@ -5,28 +5,43 @@ import type { PullRequest } from './PullRequest.type'; import PullRequestCard from '../pull-request-card/PullRequestCard'; import PullRequestTabHeader from '../pr-tab-header/PRTabHeader'; import { getPullsState } from '../../../helpers/getPullsState'; +import ReactPaginate from 'react-paginate'; +import { PULLS_PER_PAGE } from '../../../constants/url.constants'; type PullRequestProps = { pullRequests: PullRequest[]; + activeTab: PRTabValues; changeActiveTab: (value: PRTabValues) => void; openPRCount: number; closedPRCount: number; + setPRPage: (value: number) => void; }; export default function PullRequestView({ pullRequests, + activeTab, changeActiveTab, openPRCount, closedPRCount, + setPRPage, }: PullRequestProps) { - const changeTab = changeActiveTab || (() => {}); + const totalPRsCount = activeTab === 'open' ? openPRCount : closedPRCount; + const pageCount = Math.ceil(totalPRsCount / PULLS_PER_PAGE); + + // Invoke when user click to request another page. + const handlePageClick = (event: { selected: number }) => { + const page = event.selected + 1; + setPRPage(page); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + return ( {pullRequests.map((pr, index) => ( ))} + - Previous - Next + null} + containerClassName={'pagination'} + pageClassName={'pagination__item'} + previousClassName={'pagination__link_end'} + nextClassName={'pagination__link_end'} + disabledClassName={'pagination__link--disabled'} + activeClassName={'pagination__link--active'} + /> ); diff --git a/cra-rxjs-styled-components/src/constants/url.constants.ts b/cra-rxjs-styled-components/src/constants/url.constants.ts index 7cc6af35b..017a5e004 100644 --- a/cra-rxjs-styled-components/src/constants/url.constants.ts +++ b/cra-rxjs-styled-components/src/constants/url.constants.ts @@ -3,6 +3,7 @@ import { IssueType, State } from '../types/types'; export const API_URL_BASE = process.env.REACT_APP_API_URL; export const APP_BASE_URL = process.env.REACT_APP_BASE_URL; export const GITHUB_URL_BASE = `https://api.github.com`; +export const PULLS_PER_PAGE = 25; export const REDIRECT_URL = `${APP_BASE_URL}/redirect`; @@ -32,8 +33,18 @@ export const USER_REPO_LIST = (user: string, page: string = '1') => export const GISTS_URL = (user: string) => `${GITHUB_URL_BASE}/users/${user}/gists?per_page=10`; -export const PULLS_URL = (owner: string, repoName: string) => - `${GITHUB_URL_BASE}/repos/${owner}/${repoName}/pulls?state=all`; +export const PULLS_URL = ( + owner: string, + repoName: string, + page: string = '1' +) => + `${GITHUB_URL_BASE}/search/issues?q=repo:${owner}/${repoName}&per_page=30&page=${page}`; + +export const OPEN_PULLS_URL = (owner: string, repoName: string, page = 1) => + `${GITHUB_URL_BASE}/search/issues?q=repo:${owner}/${repoName}+is:open+is:pr&page=${page}&per_page=${PULLS_PER_PAGE}`; + +export const CLOSED_PULLS_URL = (owner: string, repoName: string, page = 1) => + `${GITHUB_URL_BASE}/search/issues?q=repo:${owner}/${repoName}+is:closed+is:pr&page=${page}&per_page=${PULLS_PER_PAGE}`; export const ISSUE_PR_SEARCH = ( user: string, diff --git a/cra-rxjs-styled-components/src/routes/repo/repository-pull-request.tsx b/cra-rxjs-styled-components/src/routes/repo/repository-pull-request.tsx index 93acf7f28..bab81f241 100644 --- a/cra-rxjs-styled-components/src/routes/repo/repository-pull-request.tsx +++ b/cra-rxjs-styled-components/src/routes/repo/repository-pull-request.tsx @@ -1,6 +1,5 @@ -import PullRequest from '../../components/pull-request/pull-request/PullRequest.view'; +import PullRequestCtrl from '../../components/pull-request/pull-request/PullRequest.data'; export default function RepoPullRequest() { - // @ts-ignore - return ; + return ; }