diff --git a/webui/package-lock.json b/webui/package-lock.json
index 34384e959418d29c9c2826ad2ebdb7ecb269cb7b..8e8648b55d2fa8f1d87f375af23a3cffee277f86 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -2086,14 +2086,14 @@
}
},
"@material-ui/core": {
- "version": "4.8.3",
- "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.8.3.tgz",
- "integrity": "sha512-ZJbfJQmkuZCSQTf0nzpfZwizmDdCq8ruZxnPNFnhoKDqgJpMvV8TJRi8vdI9ls1tMuTqxlhyhw8556fxOpWpFQ==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.0.tgz",
+ "integrity": "sha512-zrrr8mPU5DDBYaVil4uJYauW41PjSn5otn7cqGsmWOY0t90fypr9nNgM7rRJaPz2AP6oRSDx1kBQt2igf5uelg==",
"requires": {
"@babel/runtime": "^7.4.4",
- "@material-ui/styles": "^4.8.2",
+ "@material-ui/styles": "^4.9.0",
"@material-ui/system": "^4.7.1",
- "@material-ui/types": "^4.1.1",
+ "@material-ui/types": "^5.0.0",
"@material-ui/utils": "^4.7.1",
"@types/react-transition-group": "^4.2.0",
"clsx": "^1.0.2",
@@ -2114,26 +2114,38 @@
"@babel/runtime": "^7.4.4"
}
},
+ "@material-ui/lab": {
+ "version": "4.0.0-alpha.40",
+ "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.40.tgz",
+ "integrity": "sha512-VwXCNFJKfctu9Ot9XP5u2SSzXpm2Fn7F/o08bUfrJDkMCuRc8MCGVnNhT+guZRZa35rR97uWKc3SGQ/LAv8yEg==",
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/utils": "^4.7.1",
+ "clsx": "^1.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0"
+ }
+ },
"@material-ui/styles": {
- "version": "4.8.2",
- "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.8.2.tgz",
- "integrity": "sha512-r5U+93pkpwQOmHTmwyn2sqTio6PHd873xvSHiKP6fdybAXXX6CZgVvh3W8saZNbYr/QXsS8OHmFv7sYJLt5Yfg==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.0.tgz",
+ "integrity": "sha512-nJHum4RqYBPWsjL/9JET8Z02FZ9gSizlg/7LWVFpIthNzpK6OQ5OSRR4T4x9/p+wK3t1qNn3b1uI4XpnZaPxOA==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.7.4",
- "@material-ui/types": "^4.1.1",
+ "@material-ui/types": "^5.0.0",
"@material-ui/utils": "^4.7.1",
"clsx": "^1.0.2",
"csstype": "^2.5.2",
"hoist-non-react-statics": "^3.2.1",
- "jss": "^10.0.0",
- "jss-plugin-camel-case": "^10.0.0",
- "jss-plugin-default-unit": "^10.0.0",
- "jss-plugin-global": "^10.0.0",
- "jss-plugin-nested": "^10.0.0",
- "jss-plugin-props-sort": "^10.0.0",
- "jss-plugin-rule-value-function": "^10.0.0",
- "jss-plugin-vendor-prefixer": "^10.0.0",
+ "jss": "^10.0.3",
+ "jss-plugin-camel-case": "^10.0.3",
+ "jss-plugin-default-unit": "^10.0.3",
+ "jss-plugin-global": "^10.0.3",
+ "jss-plugin-nested": "^10.0.3",
+ "jss-plugin-props-sort": "^10.0.3",
+ "jss-plugin-rule-value-function": "^10.0.3",
+ "jss-plugin-vendor-prefixer": "^10.0.3",
"prop-types": "^15.7.2"
}
},
@@ -2148,12 +2160,9 @@
}
},
"@material-ui/types": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-4.1.1.tgz",
- "integrity": "sha512-AN+GZNXytX9yxGi0JOfxHrRTbhFybjUJ05rnsBVjcB+16e466Z0Xe5IxawuOayVZgTBNDxmPKo5j4V6OnMtaSQ==",
- "requires": {
- "@types/react": "*"
- }
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.0.tgz",
+ "integrity": "sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg=="
},
"@material-ui/utils": {
"version": "4.7.1",
@@ -2471,9 +2480,9 @@
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw=="
},
"@types/react": {
- "version": "16.9.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.18.tgz",
- "integrity": "sha512-MvjiKX/kUE8o49ipppg49RDZ97p4XfW1WWksp/UlTUSJpisyhzd62pZAMXxAscFLoxfYOflkGANAnGkSeHTFQg==",
+ "version": "16.9.19",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.19.tgz",
+ "integrity": "sha512-LJV97//H+zqKWMms0kvxaKYJDG05U2TtQB3chRLF8MPNs+MQh/H1aGlyDUxjaHvu08EAGerdX2z4LTBc7ns77A==",
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
@@ -12382,9 +12391,9 @@
}
},
"popper.js": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz",
- "integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw=="
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
},
"portfinder": {
"version": "1.0.25",
diff --git a/webui/package.json b/webui/package.json
index 5926f547f94f1bf5a2be6d664eb9932a854bb997..e1055d7552fa9b317e2aee7d4f82ae98c8d6df50 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -3,9 +3,11 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@material-ui/core": "^4.3.3",
+ "@apollo/react-hooks": "^3.1.3",
+ "@material-ui/core": "^4.9.0",
"@material-ui/icons": "^4.2.1",
- "@material-ui/styles": "^4.3.3",
+ "@material-ui/lab": "^4.0.0-alpha.40",
+ "@material-ui/styles": "^4.9.0",
"apollo-boost": "^0.4.7",
"graphql": "^14.3.0",
"moment": "^2.24.0",
diff --git a/webui/src/__tests__/query.js b/webui/src/__tests__/query.js
new file mode 100644
index 0000000000000000000000000000000000000000..5f4b58eb85539a4dbf6ac7e7f6a9975e705c1721
--- /dev/null
+++ b/webui/src/__tests__/query.js
@@ -0,0 +1,62 @@
+import { parse, stringify, quote } from '../list/Filter';
+
+it('parses a simple query', () => {
+ expect(parse('foo:bar')).toEqual({
+ foo: ['bar'],
+ });
+});
+
+it('parses a query with multiple filters', () => {
+ expect(parse('foo:bar baz:foo-bar')).toEqual({
+ foo: ['bar'],
+ baz: ['foo-bar'],
+ });
+});
+
+it('parses a quoted query', () => {
+ expect(parse('foo:"bar"')).toEqual({
+ foo: ['bar'],
+ });
+
+ expect(parse("foo:'bar'")).toEqual({
+ foo: ['bar'],
+ });
+
+ expect(parse('foo:\'bar "nested" quotes\'')).toEqual({
+ foo: ['bar "nested" quotes'],
+ });
+
+ expect(parse("foo:'escaped\\' quotes'")).toEqual({
+ foo: ["escaped' quotes"],
+ });
+});
+
+it('parses a query with repetitions', () => {
+ expect(parse('foo:bar foo:baz')).toEqual({
+ foo: ['bar', 'baz'],
+ });
+});
+
+it('parses a complex query', () => {
+ expect(parse('foo:bar foo:baz baz:"foobar" idont:\'know\'')).toEqual({
+ foo: ['bar', 'baz'],
+ baz: ['foobar'],
+ idont: ['know'],
+ });
+});
+
+it('quotes values', () => {
+ expect(quote('foo')).toEqual('foo');
+ expect(quote('foo bar')).toEqual('"foo bar"');
+ expect(quote('foo "bar"')).toEqual(`'foo "bar"'`);
+ expect(quote(`foo "bar" 'baz'`)).toEqual(`"foo \\"bar\\" 'baz'"`);
+});
+
+it('stringifies params', () => {
+ expect(stringify({ foo: ['bar'] })).toEqual('foo:bar');
+ expect(stringify({ foo: ['bar baz'] })).toEqual('foo:"bar baz"');
+ expect(stringify({ foo: ['bar', 'baz'] })).toEqual('foo:bar foo:baz');
+ expect(stringify({ foo: ['bar'], baz: ['foobar'] })).toEqual(
+ 'foo:bar baz:foobar'
+ );
+});
diff --git a/webui/src/list/BugRow.js b/webui/src/list/BugRow.js
index 23414a36e276cf15c6a95afb414d088fd689ae04..add5c12fcfb8f74db097529f75396ae4bb59cc2e 100644
--- a/webui/src/list/BugRow.js
+++ b/webui/src/list/BugRow.js
@@ -3,6 +3,7 @@ import TableCell from '@material-ui/core/TableCell/TableCell';
import TableRow from '@material-ui/core/TableRow/TableRow';
import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
import gql from 'graphql-tag';
import React from 'react';
import { Link } from 'react-router-dom';
@@ -18,7 +19,7 @@ const Open = ({ className }) => (
const Closed = ({ className }) => (
-
+
);
diff --git a/webui/src/list/Filter.js b/webui/src/list/Filter.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6cf36335987b7d8dd28f627656adf111ae2c5ae
--- /dev/null
+++ b/webui/src/list/Filter.js
@@ -0,0 +1,154 @@
+import React, { useState, useRef } from 'react';
+import { Link } from 'react-router-dom';
+import { makeStyles } from '@material-ui/styles';
+import Menu from '@material-ui/core/Menu';
+import MenuItem from '@material-ui/core/MenuItem';
+import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
+
+function parse(query) {
+ // TODO: extract the rest of the query?
+ const params = {};
+
+ // TODO: support escaping without quotes
+ const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
+ let matches;
+ while ((matches = re.exec(query)) !== null) {
+ if (!params[matches[1]]) {
+ params[matches[1]] = [];
+ }
+
+ let value;
+ if (matches[4]) {
+ value = matches[4];
+ } else {
+ value = matches[2];
+ }
+ value = value.replace(/\\(.)/g, '$1');
+ params[matches[1]].push(value);
+ }
+ return params;
+}
+
+function quote(value) {
+ const hasSingle = value.includes("'");
+ const hasDouble = value.includes('"');
+ const hasSpaces = value.includes(' ');
+ if (!hasSingle && !hasDouble && !hasSpaces) {
+ return value;
+ }
+
+ if (!hasDouble) {
+ return `"${value}"`;
+ }
+
+ if (!hasSingle) {
+ return `'${value}'`;
+ }
+
+ value = value.replace(/"/g, '\\"');
+ return `"${value}"`;
+}
+
+function stringify(params) {
+ const parts = Object.entries(params).map(([key, values]) => {
+ return values.map(value => `${key}:${quote(value)}`);
+ });
+ return [].concat(...parts).join(' ');
+}
+
+const useStyles = makeStyles(theme => ({
+ element: {
+ ...theme.typography.body2,
+ color: ({ active }) => (active ? '#333' : '#444'),
+ padding: theme.spacing(0, 1),
+ fontWeight: ({ active }) => (active ? 600 : 400),
+ textDecoration: 'none',
+ display: 'flex',
+ background: 'none',
+ border: 'none',
+ },
+ itemActive: {
+ fontWeight: 600,
+ },
+ icon: {
+ paddingRight: theme.spacing(0.5),
+ },
+}));
+
+function Dropdown({ children, dropdown, itemActive, to, ...props }) {
+ const [open, setOpen] = useState(false);
+ const buttonRef = useRef();
+ const classes = useStyles();
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
+ const classes = useStyles({ active });
+
+ const content = (
+ <>
+ {Icon && }
+ {children}
+ >
+ );
+
+ if (dropdown) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ if (to) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return {content}
;
+}
+
+export default Filter;
+export { parse, stringify, quote };
diff --git a/webui/src/list/FilterToolbar.js b/webui/src/list/FilterToolbar.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d0b52b185526276ffa2bcb09bdbc2a5505af390
--- /dev/null
+++ b/webui/src/list/FilterToolbar.js
@@ -0,0 +1,125 @@
+import { makeStyles } from '@material-ui/styles';
+import { useQuery } from '@apollo/react-hooks';
+import gql from 'graphql-tag';
+import React from 'react';
+import Toolbar from '@material-ui/core/Toolbar';
+import ErrorOutline from '@material-ui/icons/ErrorOutline';
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import Filter, { parse, stringify } from './Filter';
+
+// simple pipe operator
+// pipe(o, f, g, h) <=> h(g(f(o)))
+// TODO: move this out?
+const pipe = (initial, ...funcs) => funcs.reduce((acc, f) => f(acc), initial);
+
+const useStyles = makeStyles(theme => ({
+ toolbar: {
+ backgroundColor: theme.palette.grey['100'],
+ borderColor: theme.palette.grey['300'],
+ borderWidth: '1px 0',
+ borderStyle: 'solid',
+ margin: theme.spacing(0, -1),
+ },
+ spacer: {
+ flex: 1,
+ },
+}));
+
+const BUG_COUNT_QUERY = gql`
+ query($query: String) {
+ defaultRepository {
+ bugs: allBugs(query: $query) {
+ totalCount
+ }
+ }
+ }
+`;
+
+// This prepends the filter text with a count
+function CountingFilter({ query, children, ...props }) {
+ const { data, loading, error } = useQuery(BUG_COUNT_QUERY, {
+ variables: { query },
+ });
+
+ var prefix;
+ if (loading) prefix = '...';
+ else if (error) prefix = '???';
+ // TODO: better prefixes & error handling
+ else prefix = data.defaultRepository.bugs.totalCount;
+
+ return (
+
+ {prefix} {children}
+
+ );
+}
+
+function FilterToolbar({ query, queryLocation }) {
+ const classes = useStyles();
+ const params = parse(query);
+
+ const hasKey = key => params[key] && params[key].length > 0;
+ const hasValue = (key, value) => hasKey(key) && params[key].includes(value);
+ const loc = params => pipe(params, stringify, queryLocation);
+ const replaceParam = (key, value) => params => ({
+ ...params,
+ [key]: [value],
+ });
+ const clearParam = key => params => ({
+ ...params,
+ [key]: [],
+ });
+
+ // TODO: author/label filters
+ return (
+
+
+ open
+
+
+ closed
+
+
+ {/*
+ Author
+ Label
+ */}
+ hasValue('sort', key)}
+ to={key => pipe(params, replaceParam('sort', key), loc)}
+ >
+ Sort
+
+
+ );
+}
+
+export default FilterToolbar;
diff --git a/webui/src/list/List.js b/webui/src/list/List.js
index 54b2fe9721866844f491974705b4f700256da437..63b7354529019f76dbdf69f597304acc5e92cf97 100644
--- a/webui/src/list/List.js
+++ b/webui/src/list/List.js
@@ -1,49 +1,17 @@
-import { makeStyles } from '@material-ui/styles';
-import IconButton from '@material-ui/core/IconButton';
import Table from '@material-ui/core/Table/Table';
import TableBody from '@material-ui/core/TableBody/TableBody';
-import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
-import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
import React from 'react';
import BugRow from './BugRow';
-const useStyles = makeStyles(theme => ({
- main: {
- maxWidth: 600,
- margin: 'auto',
- marginTop: theme.spacing(4),
- },
- pagination: {
- ...theme.typography.overline,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'flex-end',
- },
-}));
-
-function List({ bugs, nextPage, prevPage }) {
- const classes = useStyles();
- const { hasNextPage, hasPreviousPage } = bugs.pageInfo;
+function List({ bugs }) {
return (
-
-
-
- {bugs.edges.map(({ cursor, node }) => (
-
- ))}
-
-
-
-
-
Total: {bugs.totalCount}
-
-
-
-
-
-
-
-
+
+
+ {bugs.edges.map(({ cursor, node }) => (
+
+ ))}
+
+
);
}
diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js
index 869bca79824334de3fed0c1a355c3010aeb71321..8eeec24031774631c9b539dc83e7307ff3c5820d 100644
--- a/webui/src/list/ListQuery.js
+++ b/webui/src/list/ListQuery.js
@@ -1,19 +1,111 @@
-// @flow
-import CircularProgress from '@material-ui/core/CircularProgress';
+import { fade, makeStyles } from '@material-ui/core/styles';
+import IconButton from '@material-ui/core/IconButton';
+import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
+import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
+import ErrorOutline from '@material-ui/icons/ErrorOutline';
+import Paper from '@material-ui/core/Paper';
+import InputBase from '@material-ui/core/InputBase';
+import Skeleton from '@material-ui/lab/Skeleton';
import gql from 'graphql-tag';
-import React, { useState } from 'react';
-import { Query } from 'react-apollo';
+import React, { useState, useEffect, useRef } from 'react';
+import { useQuery } from '@apollo/react-hooks';
+import { useLocation, useHistory, Link } from 'react-router-dom';
import BugRow from './BugRow';
import List from './List';
+import FilterToolbar from './FilterToolbar';
+
+const useStyles = makeStyles(theme => ({
+ main: {
+ maxWidth: 800,
+ margin: 'auto',
+ marginTop: theme.spacing(4),
+ marginBottom: theme.spacing(4),
+ overflow: 'hidden',
+ },
+ pagination: {
+ ...theme.typography.overline,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ header: {
+ display: 'flex',
+ padding: theme.spacing(2),
+ '& > h1': {
+ ...theme.typography.h6,
+ margin: theme.spacing(0, 2),
+ },
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ search: {
+ borderRadius: theme.shape.borderRadius,
+ borderColor: fade(theme.palette.primary.main, 0.2),
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ backgroundColor: fade(theme.palette.primary.main, 0.05),
+ padding: theme.spacing(0, 1),
+ width: ({ searching }) => (searching ? '20rem' : '15rem'),
+ transition: theme.transitions.create(),
+ },
+ searchFocused: {
+ borderColor: fade(theme.palette.primary.main, 0.4),
+ backgroundColor: theme.palette.background.paper,
+ width: '20rem!important',
+ },
+ placeholderRow: {
+ padding: theme.spacing(1),
+ borderBottomColor: theme.palette.grey['300'],
+ borderBottomWidth: '1px',
+ borderBottomStyle: 'solid',
+ display: 'flex',
+ alignItems: 'center',
+ },
+ placeholderRowStatus: {
+ margin: theme.spacing(1, 2),
+ },
+ placeholderRowText: {
+ flex: 1,
+ },
+ message: {
+ ...theme.typography.h5,
+ padding: theme.spacing(8),
+ textAlign: 'center',
+ borderBottomColor: theme.palette.grey['300'],
+ borderBottomWidth: '1px',
+ borderBottomStyle: 'solid',
+ '& > p': {
+ margin: '0',
+ },
+ },
+ errorBox: {
+ color: theme.palette.error.main,
+ '& > pre': {
+ fontSize: '1rem',
+ textAlign: 'left',
+ backgroundColor: theme.palette.grey['900'],
+ color: theme.palette.common.white,
+ marginTop: theme.spacing(4),
+ padding: theme.spacing(2, 3),
+ },
+ },
+}));
const QUERY = gql`
- query($first: Int, $last: Int, $after: String, $before: String) {
+ query(
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+ $query: String
+ ) {
defaultRepository {
bugs: allBugs(
first: $first
last: $last
after: $after
before: $before
+ query: $query
) {
totalCount
edges {
@@ -35,30 +127,206 @@ const QUERY = gql`
${BugRow.fragment}
`;
+function editParams(params, callback) {
+ const cloned = new URLSearchParams(params.toString());
+ callback(cloned);
+ return cloned;
+}
+
+// TODO: factor this out
+const Placeholder = ({ count }) => {
+ const classes = useStyles();
+ return (
+ <>
+ {new Array(count).fill(null).map((_, i) => (
+
+ ))}
+ >
+ );
+};
+
+// TODO: factor this out
+const NoBug = () => {
+ const classes = useStyles();
+ return (
+
+
+
No results matched your search.
+
+ );
+};
+
+const Error = ({ error }) => {
+ const classes = useStyles();
+ return (
+
+
+
There was an error while fetching bug.
+
+ {error.message}
+
+
+ {JSON.stringify(error, null, 2)}
+
+
+ );
+};
+
function ListQuery() {
- const [page, setPage] = useState({ first: 10, after: null });
+ const location = useLocation();
+ const history = useHistory();
+ const params = new URLSearchParams(location.search);
+ const query = params.get('q') || '';
+
+ const [input, setInput] = useState(query);
+
+ const classes = useStyles({ searching: !!input });
+
+ // TODO is this the right way to do it?
+ const lastQuery = useRef();
+ useEffect(() => {
+ if (query !== lastQuery.current) {
+ setInput(query);
+ }
+ lastQuery.current = query;
+ }, [query, input, lastQuery]);
+
+ const page = {
+ first: params.get('first'),
+ last: params.get('last'),
+ after: params.get('after'),
+ before: params.get('before'),
+ };
+
+ // If nothing set, show the first 10 items
+ if (!page.first && !page.last) {
+ page.first = 10;
+ }
const perPage = page.first || page.last;
- const nextPage = pageInfo =>
- setPage({ first: perPage, after: pageInfo.endCursor });
- const prevPage = pageInfo =>
- setPage({ last: perPage, before: pageInfo.startCursor });
+
+ const { loading, error, data } = useQuery(QUERY, {
+ variables: {
+ ...page,
+ query,
+ },
+ });
+
+ let nextPage = null;
+ let previousPage = null;
+ let hasNextPage = false;
+ let hasPreviousPage = false;
+ let count = 0;
+ if (!loading && !error && data.defaultRepository.bugs) {
+ const bugs = data.defaultRepository.bugs;
+ hasNextPage = bugs.pageInfo.hasNextPage;
+ hasPreviousPage = bugs.pageInfo.hasPreviousPage;
+ count = bugs.totalCount;
+ // This computes the URL for the next page
+ nextPage = {
+ ...location,
+ search: editParams(params, p => {
+ p.delete('last');
+ p.delete('before');
+ p.set('first', perPage);
+ p.set('after', bugs.pageInfo.endCursor);
+ }).toString(),
+ };
+ // and this for the previous page
+ previousPage = {
+ ...location,
+ search: editParams(params, p => {
+ p.delete('first');
+ p.delete('after');
+ p.set('last', perPage);
+ p.set('before', bugs.pageInfo.startCursor);
+ }).toString(),
+ };
+ }
+
+ // Prepare params without paging for editing filters
+ const paramsWithoutPaging = editParams(params, p => {
+ p.delete('first');
+ p.delete('last');
+ p.delete('before');
+ p.delete('after');
+ });
+ // Returns a new location with the `q` param edited
+ const queryLocation = query => ({
+ ...location,
+ search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(),
+ });
+
+ let content;
+ if (loading) {
+ content = ;
+ } else if (error) {
+ content = ;
+ } else {
+ const bugs = data.defaultRepository.bugs;
+
+ if (bugs.totalCount === 0) {
+ content = ;
+ } else {
+ content =
;
+ }
+ }
+
+ const formSubmit = e => {
+ e.preventDefault();
+ history.push(queryLocation(input));
+ };
return (
-
- {({ loading, error, data }) => {
- if (loading) return ;
- if (error) return Error: {error}
;
- const bugs = data.defaultRepository.bugs;
- return (
- nextPage(bugs.pageInfo)}
- prevPage={() => prevPage(bugs.pageInfo)}
+
+
+
+
+
+
+ {content}
+
+
+
+
+
{loading ? 'Loading' : `Total: ${count}`}
+
+
+
+
+
);
}