ListQuery.js

  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;