From fa13550115144a6f39888960a80cc24890f83536 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 24 Jan 2020 00:43:02 +0100 Subject: webui: enhance the issue list page This starts some ground work for filtering & moves the pagination logic in the query params. Also has a nice loading placeholder. --- webui/src/list/ListQuery.js | 237 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 213 insertions(+), 24 deletions(-) (limited to 'webui/src/list/ListQuery.js') diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js index 869bca79..9cbfab67 100644 --- a/webui/src/list/ListQuery.js +++ b/webui/src/list/ListQuery.js @@ -1,19 +1,90 @@ -// @flow -import CircularProgress from '@material-ui/core/CircularProgress'; +import { makeStyles } from '@material-ui/styles'; +import IconButton from '@material-ui/core/IconButton'; +import Toolbar from '@material-ui/core/Toolbar'; +import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; +import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; +import ErrorOutline from '@material-ui/icons/ErrorOutline'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import Paper from '@material-ui/core/Paper'; +import Filter from './Filter'; +import Skeleton from '@material-ui/lab/Skeleton'; import gql from 'graphql-tag'; -import React, { useState } from 'react'; -import { Query } from 'react-apollo'; +import React from 'react'; +import { useQuery } from '@apollo/react-hooks'; +import { useLocation, Link } from 'react-router-dom'; import BugRow from './BugRow'; import List from './List'; +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', + }, + toolbar: { + backgroundColor: theme.palette.grey['100'], + borderColor: theme.palette.grey['300'], + borderWidth: '1px 0', + borderStyle: 'solid', + margin: theme.spacing(0, -1), + }, + header: { + ...theme.typography.h6, + padding: theme.spacing(2, 4), + }, + spacer: { + flex: 1, + }, + 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, + }, + noBug: { + ...theme.typography.h5, + padding: theme.spacing(8), + textAlign: 'center', + borderBottomColor: theme.palette.grey['300'], + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + '& > p': { + margin: '0', + }, + }, +})); + 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 +106,148 @@ 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.

+
+ ); +}; + function ListQuery() { - const [page, setPage] = useState({ first: 10, after: null }); + const classes = useStyles(); + const location = useLocation(); + const params = new URLSearchParams(location.search); + const query = params.get('q'); + 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(), + }; + } + + let content; + if (loading) { + content = ; + } else if (error) { + content =

Error: {JSON.stringify(error)}

; + } else { + const bugs = data.defaultRepository.bugs; + + if (bugs.totalCount === 0) { + content = ; + } else { + content = ; + } + } 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)} - /> - ); - }} -
+ +
Issues
+ + {/* TODO */} + + 123 open + + 456 closed +
+ Author + Label + Sort + + {content} +
+ + + +
{loading ? 'Loading' : `Total: ${count}`}
+ + + +
+ ); } -- cgit From 4d97e3a19a96e2361b35a0ccc0be74e0ba887214 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sat, 25 Jan 2020 11:40:08 +0100 Subject: webui: implement filtering --- webui/src/list/ListQuery.js | 140 +++++++++++++++++++++++++++++++++----------- 1 file changed, 106 insertions(+), 34 deletions(-) (limited to 'webui/src/list/ListQuery.js') diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js index 9cbfab67..b6a29702 100644 --- a/webui/src/list/ListQuery.js +++ b/webui/src/list/ListQuery.js @@ -1,19 +1,18 @@ -import { makeStyles } from '@material-ui/styles'; +import { fade, makeStyles } from '@material-ui/core/styles'; import IconButton from '@material-ui/core/IconButton'; -import Toolbar from '@material-ui/core/Toolbar'; import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; -import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import Paper from '@material-ui/core/Paper'; -import Filter from './Filter'; +import InputBase from '@material-ui/core/InputBase'; import Skeleton from '@material-ui/lab/Skeleton'; import gql from 'graphql-tag'; -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useQuery } from '@apollo/react-hooks'; -import { useLocation, Link } from 'react-router-dom'; +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: { @@ -29,19 +28,28 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', justifyContent: 'center', }, - toolbar: { - backgroundColor: theme.palette.grey['100'], - borderColor: theme.palette.grey['300'], - borderWidth: '1px 0', - borderStyle: 'solid', - margin: theme.spacing(0, -1), - }, header: { - ...theme.typography.h6, - padding: theme.spacing(2, 4), + display: 'flex', + padding: theme.spacing(2), + '& > h1': { + ...theme.typography.h6, + margin: theme.spacing(0, 2), + }, + alignItems: 'center', + justifyContent: 'space-between', }, - spacer: { - flex: 1, + 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), + ':focus': { + // TODO + borderColor: fade(theme.palette.primary.main, 0.4), + backgroundColor: theme.palette.background.paper, + }, }, placeholderRow: { padding: theme.spacing(1), @@ -57,7 +65,7 @@ const useStyles = makeStyles(theme => ({ placeholderRowText: { flex: 1, }, - noBug: { + message: { ...theme.typography.h5, padding: theme.spacing(8), textAlign: 'center', @@ -68,6 +76,17 @@ const useStyles = makeStyles(theme => ({ 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` @@ -139,18 +158,47 @@ const Placeholder = ({ count }) => { 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 classes = useStyles(); const location = useLocation(); + const history = useHistory(); const params = new URLSearchParams(location.search); const query = params.get('q'); + + const [input, setInput] = useState(query); + + // 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'), @@ -204,11 +252,24 @@ function ListQuery() { }; } + // 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 =

Error: {JSON.stringify(error)}

; + content = ; } else { const bugs = data.defaultRepository.bugs; @@ -219,31 +280,42 @@ function ListQuery() { } } + const formSubmit = e => { + e.preventDefault(); + history.push(queryLocation(input)); + }; + return ( -
Issues
- - {/* TODO */} - - 123 open - - 456 closed -
- Author - Label - Sort - +
+

Issues

+
+ setInput(e.target.value)} + className={classes.search} + /> + + +
+ {content}
{loading ? 'Loading' : `Total: ${count}`}
- +
-- cgit From ead5bad7854bc2342e0998c8a45f62e9aace7887 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 30 Jan 2020 02:05:36 +0100 Subject: webui: implement issue list sort --- webui/src/list/ListQuery.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) (limited to 'webui/src/list/ListQuery.js') diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.js index b6a29702..01113f6c 100644 --- a/webui/src/list/ListQuery.js +++ b/webui/src/list/ListQuery.js @@ -45,11 +45,13 @@ const useStyles = makeStyles(theme => ({ borderWidth: '1px', backgroundColor: fade(theme.palette.primary.main, 0.05), padding: theme.spacing(0, 1), - ':focus': { - // TODO - borderColor: fade(theme.palette.primary.main, 0.4), - backgroundColor: theme.palette.background.paper, - }, + 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), @@ -182,7 +184,6 @@ const Error = ({ error }) => { }; function ListQuery() { - const classes = useStyles(); const location = useLocation(); const history = useHistory(); const params = new URLSearchParams(location.search); @@ -190,6 +191,8 @@ function ListQuery() { const [input, setInput] = useState(query); + const classes = useStyles({ searching: !!input }); + // TODO is this the right way to do it? const lastQuery = useRef(); useEffect(() => { @@ -291,9 +294,13 @@ function ListQuery() {

Issues

setInput(e.target.value)} - className={classes.search} + classes={{ + root: classes.search, + focused: classes.searchFocused, + }} />