1import { makeStyles } from '@material-ui/styles';
2import IconButton from '@material-ui/core/IconButton';
3import Toolbar from '@material-ui/core/Toolbar';
4import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
5import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
6import ErrorOutline from '@material-ui/icons/ErrorOutline';
7import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
8import Paper from '@material-ui/core/Paper';
9import Filter from './Filter';
10import Skeleton from '@material-ui/lab/Skeleton';
11import gql from 'graphql-tag';
12import React from 'react';
13import { useQuery } from '@apollo/react-hooks';
14import { useLocation, Link } from 'react-router-dom';
15import BugRow from './BugRow';
16import List from './List';
17
18const useStyles = makeStyles(theme => ({
19 main: {
20 maxWidth: 800,
21 margin: 'auto',
22 marginTop: theme.spacing(4),
23 marginBottom: theme.spacing(4),
24 overflow: 'hidden',
25 },
26 pagination: {
27 ...theme.typography.overline,
28 display: 'flex',
29 alignItems: 'center',
30 justifyContent: 'center',
31 },
32 toolbar: {
33 backgroundColor: theme.palette.grey['100'],
34 borderColor: theme.palette.grey['300'],
35 borderWidth: '1px 0',
36 borderStyle: 'solid',
37 margin: theme.spacing(0, -1),
38 },
39 header: {
40 ...theme.typography.h6,
41 padding: theme.spacing(2, 4),
42 },
43 spacer: {
44 flex: 1,
45 },
46 placeholderRow: {
47 padding: theme.spacing(1),
48 borderBottomColor: theme.palette.grey['300'],
49 borderBottomWidth: '1px',
50 borderBottomStyle: 'solid',
51 display: 'flex',
52 alignItems: 'center',
53 },
54 placeholderRowStatus: {
55 margin: theme.spacing(1, 2),
56 },
57 placeholderRowText: {
58 flex: 1,
59 },
60 noBug: {
61 ...theme.typography.h5,
62 padding: theme.spacing(8),
63 textAlign: 'center',
64 borderBottomColor: theme.palette.grey['300'],
65 borderBottomWidth: '1px',
66 borderBottomStyle: 'solid',
67 '& > p': {
68 margin: '0',
69 },
70 },
71}));
72
73const QUERY = gql`
74 query(
75 $first: Int
76 $last: Int
77 $after: String
78 $before: String
79 $query: String
80 ) {
81 defaultRepository {
82 bugs: allBugs(
83 first: $first
84 last: $last
85 after: $after
86 before: $before
87 query: $query
88 ) {
89 totalCount
90 edges {
91 cursor
92 node {
93 ...BugRow
94 }
95 }
96 pageInfo {
97 hasNextPage
98 hasPreviousPage
99 startCursor
100 endCursor
101 }
102 }
103 }
104 }
105
106 ${BugRow.fragment}
107`;
108
109function editParams(params, callback) {
110 const cloned = new URLSearchParams(params.toString());
111 callback(cloned);
112 return cloned;
113}
114
115// TODO: factor this out
116const Placeholder = ({ count }) => {
117 const classes = useStyles();
118 return (
119 <>
120 {new Array(count).fill(null).map((_, i) => (
121 <div key={i} className={classes.placeholderRow}>
122 <Skeleton
123 className={classes.placeholderRowStatus}
124 variant="circle"
125 width={20}
126 height={20}
127 />
128 <div className={classes.placeholderRowText}>
129 <Skeleton height={22} />
130 <Skeleton height={24} width="60%" />
131 </div>
132 </div>
133 ))}
134 </>
135 );
136};
137
138// TODO: factor this out
139const NoBug = () => {
140 const classes = useStyles();
141 return (
142 <div className={classes.noBug}>
143 <ErrorOutline fontSize="large" />
144 <p>No results matched your search.</p>
145 </div>
146 );
147};
148
149function ListQuery() {
150 const classes = useStyles();
151 const location = useLocation();
152 const params = new URLSearchParams(location.search);
153 const query = params.get('q');
154 const page = {
155 first: params.get('first'),
156 last: params.get('last'),
157 after: params.get('after'),
158 before: params.get('before'),
159 };
160
161 // If nothing set, show the first 10 items
162 if (!page.first && !page.last) {
163 page.first = 10;
164 }
165
166 const perPage = page.first || page.last;
167
168 const { loading, error, data } = useQuery(QUERY, {
169 variables: {
170 ...page,
171 query,
172 },
173 });
174
175 let nextPage = null;
176 let previousPage = null;
177 let hasNextPage = false;
178 let hasPreviousPage = false;
179 let count = 0;
180 if (!loading && !error && data.defaultRepository.bugs) {
181 const bugs = data.defaultRepository.bugs;
182 hasNextPage = bugs.pageInfo.hasNextPage;
183 hasPreviousPage = bugs.pageInfo.hasPreviousPage;
184 count = bugs.totalCount;
185 // This computes the URL for the next page
186 nextPage = {
187 ...location,
188 search: editParams(params, p => {
189 p.delete('last');
190 p.delete('before');
191 p.set('first', perPage);
192 p.set('after', bugs.pageInfo.endCursor);
193 }).toString(),
194 };
195 // and this for the previous page
196 previousPage = {
197 ...location,
198 search: editParams(params, p => {
199 p.delete('first');
200 p.delete('after');
201 p.set('last', perPage);
202 p.set('before', bugs.pageInfo.startCursor);
203 }).toString(),
204 };
205 }
206
207 let content;
208 if (loading) {
209 content = <Placeholder count={10} />;
210 } else if (error) {
211 content = <p>Error: {JSON.stringify(error)}</p>;
212 } else {
213 const bugs = data.defaultRepository.bugs;
214
215 if (bugs.totalCount === 0) {
216 content = <NoBug />;
217 } else {
218 content = <List bugs={bugs} />;
219 }
220 }
221
222 return (
223 <Paper className={classes.main}>
224 <header className={classes.header}>Issues</header>
225 <Toolbar className={classes.toolbar}>
226 {/* TODO */}
227 <Filter active icon={ErrorOutline}>
228 123 open
229 </Filter>
230 <Filter icon={CheckCircleOutline}>456 closed</Filter>
231 <div className={classes.spacer} />
232 <Filter>Author</Filter>
233 <Filter>Label</Filter>
234 <Filter>Sort</Filter>
235 </Toolbar>
236 {content}
237 <div className={classes.pagination}>
238 <IconButton
239 component={Link}
240 to={previousPage}
241 disabled={!hasPreviousPage}
242 >
243 <KeyboardArrowLeft />
244 </IconButton>
245 <div>{loading ? 'Loading' : `Total: ${count}`}</div>
246 <IconButton component={Link} to={nextPage} disabled={!hasNextPage}>
247 <KeyboardArrowRight />
248 </IconButton>
249 </div>
250 </Paper>
251 );
252}
253
254export default ListQuery;