Skip to content

Commit a35e10e

Browse files
authored
Add pagination with Apollo (canada-ca#719)
This commit uses the new relay pagination helper to add next/previous buttons to the organizations list.
1 parent 594c497 commit a35e10e

10 files changed

Lines changed: 6401 additions & 8985 deletions

frontend/package-lock.json

Lines changed: 5676 additions & 8840 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"compile": "lingui compile"
1313
},
1414
"dependencies": {
15-
"@apollo/client": "^3.0.1",
15+
"@apollo/client": "^3.1.0",
1616
"@babel/runtime": "^7.10.4",
1717
"@chakra-ui/core": "^0.8.0",
1818
"@emotion/core": "^10.0.28",

frontend/src/Organizations.js

Lines changed: 67 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,49 @@
1-
import React, { useState, useEffect } from 'react'
1+
import React from 'react'
2+
import { number } from 'prop-types'
23
import { useQuery } from '@apollo/client'
34
import { Trans } from '@lingui/macro'
45
import { Layout } from './Layout'
56
import { ListOf } from './ListOf'
6-
import { Heading, Stack, useToast, Box, Divider } from '@chakra-ui/core'
7-
import { ORGANIZATIONS } from './graphql/queries'
7+
import { Button, Heading, Stack, useToast, Box, Divider } from '@chakra-ui/core'
8+
import {
9+
PAGINATED_ORGANIZATIONS,
10+
REVERSE_PAGINATED_ORGANIZATIONS,
11+
} from './graphql/queries'
812
import { useUserState } from './UserState'
913
import { Organization } from './Organization'
10-
import { PaginationButtons } from './PaginationButtons'
1114

12-
export default function Organisations() {
15+
export default function Organisations({ orgsPerPage = 10 }) {
1316
const { currentUser } = useUserState()
14-
const [orgs, setOrgs] = useState([])
15-
const [currentPage, setCurrentPage] = useState(1)
16-
const [orgsPerPage, setOrgsPerPage] = useState(10)
1717
const toast = useToast()
1818

19-
// This query is currently requesting the first 10 orgs
20-
const { loading, _error, data } = useQuery(ORGANIZATIONS, {
21-
context: {
22-
headers: {
23-
authorization: currentUser.jwt,
19+
const { loading, error, data, fetchMore } = useQuery(
20+
PAGINATED_ORGANIZATIONS,
21+
{
22+
variables: { after: '', first: orgsPerPage },
23+
context: {
24+
headers: {
25+
authorization: currentUser.jwt,
26+
},
27+
},
28+
onError: error => {
29+
const [_, message] = error.message.split(': ')
30+
toast({
31+
title: 'Error',
32+
description: message,
33+
status: 'failure',
34+
duration: 9000,
35+
isClosable: true,
36+
})
2437
},
2538
},
26-
onError: (error) => {
27-
const [_, message] = error.message.split(': ')
28-
toast({
29-
title: 'Error',
30-
description: message,
31-
status: 'failure',
32-
duration: 9000,
33-
isClosable: true,
34-
})
35-
},
36-
})
39+
)
3740

38-
useEffect(() => {
39-
const fetchOrgs = async () => {
40-
let organizations = []
41-
if (data && data.organizations.edges) {
42-
organizations = data.organizations.edges.map((e) => e.node)
43-
setOrgs(organizations)
44-
}
45-
}
46-
fetchOrgs()
47-
}, [data])
41+
if (error)
42+
return (
43+
<p>
44+
<Trans>error {error.message}</Trans>
45+
</p>
46+
)
4847

4948
if (loading)
5049
return (
@@ -53,14 +52,6 @@ export default function Organisations() {
5352
</p>
5453
)
5554

56-
// Get current orgs
57-
const indexOfLastOrg = currentPage * orgsPerPage
58-
const indexOfFirstOrg = indexOfLastOrg - orgsPerPage
59-
const currentOrgs = orgs.slice(indexOfFirstOrg, indexOfLastOrg)
60-
61-
// Change page
62-
const paginate = (pageNumber) => setCurrentPage(pageNumber)
63-
6455
return (
6556
<Layout>
6657
<Stack spacing={10} shouldWrapChildren>
@@ -70,7 +61,7 @@ export default function Organisations() {
7061
<Stack direction="row" spacing={4}>
7162
<Stack spacing={4} flexWrap="wrap">
7263
<ListOf
73-
elements={currentOrgs}
64+
elements={data.organizations.edges.map(e => e.node)}
7465
ifEmpty={() => <Trans>No Organizations</Trans>}
7566
>
7667
{({ name, slug, domainCount }, index) => (
@@ -86,15 +77,40 @@ export default function Organisations() {
8677
</ListOf>
8778
</Stack>
8879
</Stack>
89-
<PaginationButtons
90-
perPage={orgsPerPage}
91-
total={orgs.length}
92-
paginate={paginate}
93-
currentPage={currentPage}
94-
setPerPage={setOrgsPerPage}
95-
/>
80+
<Stack isInline align="center">
81+
<Button
82+
onClick={() =>
83+
fetchMore({
84+
query: REVERSE_PAGINATED_ORGANIZATIONS,
85+
variables: {
86+
before: data.organizations.pageInfo.startCursor,
87+
last: orgsPerPage,
88+
},
89+
})
90+
}
91+
aria-label="Previous page"
92+
>
93+
<Trans>Previous</Trans>
94+
</Button>
95+
96+
<Button
97+
onClick={() =>
98+
fetchMore({
99+
variables: {
100+
after: data.organizations.pageInfo.endCursor,
101+
first: orgsPerPage,
102+
},
103+
})
104+
}
105+
disable={data.organizations.pageInfo.hasNextPage}
106+
aria-label="Next page"
107+
>
108+
<Trans>Next</Trans>
109+
</Button>
110+
</Stack>
96111
</Stack>
97-
<Divider />
98112
</Layout>
99113
)
100114
}
115+
116+
Organisations.propTypes = { orgsPerPage: number }

frontend/src/SummaryGroup.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export function SummaryGroup() {
2626
},
2727
})
2828

29+
if (error) {
30+
return <p>{String(error)}</p>
31+
}
32+
2933
if (loading) {
3034
return (
3135
<p>
@@ -34,10 +38,6 @@ export function SummaryGroup() {
3438
)
3539
}
3640

37-
if (error) {
38-
return <p>{String(error)}</p>
39-
}
40-
4141
return (
4242
<SimpleGrid
4343
columns={[1, 1, 1, 2]}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React from 'react'
2+
import { createMemoryHistory } from 'history'
3+
import { ThemeProvider, theme } from '@chakra-ui/core'
4+
import { Router, Route, Switch } from 'react-router-dom'
5+
import { render, waitFor, fireEvent } from '@testing-library/react'
6+
import { MockedProvider } from '@apollo/client/testing'
7+
import Organizations from '../Organizations'
8+
import {
9+
PAGINATED_ORGANIZATIONS,
10+
REVERSE_PAGINATED_ORGANIZATIONS,
11+
} from '../graphql/queries'
12+
import { I18nProvider } from '@lingui/react'
13+
import { setupI18n } from '@lingui/core'
14+
import { UserStateProvider } from '../UserState'
15+
import { createCache } from '../client'
16+
17+
describe('<Organisations />', () => {
18+
describe('pagination', () => {
19+
const cache = createCache()
20+
cache.writeQuery({
21+
query: PAGINATED_ORGANIZATIONS,
22+
variables: { after: '', first: 1 },
23+
data: {
24+
organizations: {
25+
edges: [
26+
{
27+
cursor: 'YXJyYXljb25uZWN0aW9uOjA=',
28+
node: {
29+
id: 'T3JnYW5pemF0aW9uczoyCg==',
30+
acronym: 'ORG1',
31+
name: 'organization one',
32+
slug: 'organization-one',
33+
domainCount: 5,
34+
__typename: 'Organizations',
35+
},
36+
__typename: 'OrganizationsEdge',
37+
},
38+
],
39+
pageInfo: {
40+
hasNextPage: true,
41+
endCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
42+
hasPreviousPage: false,
43+
startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
44+
__typename: 'PageInfo',
45+
},
46+
__typename: 'OrganizationsConnection',
47+
},
48+
},
49+
})
50+
describe(`when the "next" button is clicked`, () => {
51+
it('displays the next pagination result', async () => {
52+
// We need two of these?
53+
const mocks = [
54+
{
55+
request: {
56+
query: PAGINATED_ORGANIZATIONS,
57+
variables: { after: 'YXJyYXljb25uZWN0aW9uOjA=', first: 1 },
58+
},
59+
result: {
60+
data: {
61+
organizations: {
62+
edges: [
63+
{
64+
cursor: 'YXJyYXljb25uZWN0aW9uOjA=',
65+
node: {
66+
id: 'T3JnYW5pemF0aW9uczoxCg==',
67+
acronym: 'ORG2',
68+
name: 'organization two',
69+
slug: 'organization-two',
70+
domainCount: 5,
71+
__typename: 'Organizations',
72+
},
73+
__typename: 'OrganizationsEdge',
74+
},
75+
],
76+
pageInfo: {
77+
hasNextPage: false,
78+
endCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
79+
hasPreviousPage: true,
80+
startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
81+
__typename: 'PageInfo',
82+
},
83+
__typename: 'OrganizationsConnection',
84+
},
85+
},
86+
},
87+
},
88+
{
89+
request: {
90+
query: PAGINATED_ORGANIZATIONS,
91+
variables: { after: 'YXJyYXljb25uZWN0aW9uOjA=', first: 1 },
92+
},
93+
result: {
94+
data: {
95+
organizations: {
96+
edges: [
97+
{
98+
cursor: 'YXJyYXljb25uZWN0aW9uOjA=',
99+
node: {
100+
id: 'T3JnYW5pemF0aW9uczoxCg==',
101+
acronym: 'ORG2',
102+
name: 'organization two',
103+
slug: 'organization-two',
104+
domainCount: 5,
105+
__typename: 'Organizations',
106+
},
107+
__typename: 'OrganizationsEdge',
108+
},
109+
],
110+
pageInfo: {
111+
hasNextPage: false,
112+
endCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
113+
hasPreviousPage: true,
114+
startCursor: 'YXJyYXljb25uZWN0aW9uOjA=',
115+
__typename: 'PageInfo',
116+
},
117+
__typename: 'OrganizationsConnection',
118+
},
119+
},
120+
},
121+
},
122+
]
123+
const history = createMemoryHistory({
124+
initialEntries: ['/organizations'],
125+
initialIndex: 0,
126+
})
127+
128+
const { getByText } = render(
129+
<UserStateProvider
130+
initialState={{ userName: null, jwt: null, tfa: null }}
131+
>
132+
<ThemeProvider theme={theme}>
133+
<I18nProvider i18n={setupI18n()}>
134+
<MockedProvider mocks={mocks} cache={cache}>
135+
<Router history={history}>
136+
<Switch>
137+
<Route
138+
path="/organizations"
139+
render={() => <Organizations orgsPerPage={1} />}
140+
/>
141+
</Switch>
142+
</Router>
143+
</MockedProvider>
144+
</I18nProvider>
145+
</ThemeProvider>
146+
</UserStateProvider>,
147+
)
148+
149+
await waitFor(() =>
150+
expect(getByText(/organization one/)).toBeInTheDocument(),
151+
)
152+
153+
const next = await waitFor(() => getByText('Next'))
154+
155+
await waitFor(() => {
156+
fireEvent.click(next)
157+
})
158+
159+
await waitFor(() =>
160+
expect(getByText(/organization two/)).toBeInTheDocument(),
161+
)
162+
})
163+
})
164+
})
165+
})

0 commit comments

Comments
 (0)