webui: implement issue list sort

Quentin Gliech created

Change summary

webui/src/__tests__/query.js    |  4 
webui/src/list/Filter.js        | 77 +++++++++++++++++++++++++++++-----
webui/src/list/FilterToolbar.js | 17 ++++++
webui/src/list/ListQuery.js     | 21 ++++++---
4 files changed, 97 insertions(+), 22 deletions(-)

Detailed changes

webui/src/__tests__/query.js 🔗

@@ -7,9 +7,9 @@ it('parses a simple query', () => {
 });
 
 it('parses a query with multiple filters', () => {
-  expect(parse('foo:bar baz:foobar')).toEqual({
+  expect(parse('foo:bar baz:foo-bar')).toEqual({
     foo: ['bar'],
-    baz: ['foobar'],
+    baz: ['foo-bar'],
   });
 });
 

webui/src/list/Filter.js 🔗

@@ -1,13 +1,16 @@
-import React from 'react';
+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+):(\w+|(["'])(([^\3]|\\.)*)\3)+/g;
+  const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
   let matches;
   while ((matches = re.exec(query)) !== null) {
     if (!params[matches[1]]) {
@@ -58,20 +61,63 @@ const useStyles = makeStyles(theme => ({
     ...theme.typography.body2,
     color: ({ active }) => (active ? '#333' : '#444'),
     padding: theme.spacing(0, 1),
-    fontWeight: ({ active }) => (active ? 500 : 400),
+    fontWeight: ({ active }) => (active ? 600 : 400),
     textDecoration: 'none',
     display: 'flex',
-    alignSelf: ({ end }) => (end ? 'flex-end' : 'auto'),
     background: 'none',
     border: 'none',
   },
+  itemActive: {
+    fontWeight: 600,
+  },
   icon: {
     paddingRight: theme.spacing(0.5),
   },
 }));
 
-function Filter({ active, to, children, icon: Icon, end, ...props }) {
-  const classes = useStyles({ active, end });
+function Dropdown({ children, dropdown, itemActive, to, ...props }) {
+  const [open, setOpen] = useState(false);
+  const buttonRef = useRef();
+  const classes = useStyles();
+
+  return (
+    <>
+      <button ref={buttonRef} onClick={() => setOpen(!open)} {...props}>
+        {children}
+        <ArrowDropDown fontSize="small" />
+      </button>
+      <Menu
+        getContentAnchorEl={null}
+        anchorOrigin={{
+          vertical: 'bottom',
+          horizontal: 'left',
+        }}
+        transformOrigin={{
+          vertical: 'top',
+          horizontal: 'left',
+        }}
+        open={open}
+        onClose={() => setOpen(false)}
+        anchorEl={buttonRef.current}
+      >
+        {dropdown.map(([key, value]) => (
+          <MenuItem
+            component={Link}
+            to={to(key)}
+            className={itemActive(key) ? classes.itemActive : null}
+            onClick={() => setOpen(false)}
+            key={key}
+          >
+            {value}
+          </MenuItem>
+        ))}
+      </Menu>
+    </>
+  );
+}
+
+function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
+  const classes = useStyles({ active });
 
   const content = (
     <>
@@ -80,6 +126,19 @@ function Filter({ active, to, children, icon: Icon, end, ...props }) {
     </>
   );
 
+  if (dropdown) {
+    return (
+      <Dropdown
+        {...props}
+        to={to}
+        dropdown={dropdown}
+        className={classes.element}
+      >
+        {content}
+      </Dropdown>
+    );
+  }
+
   if (to) {
     return (
       <Link to={to} {...props} className={classes.element}>
@@ -88,11 +147,7 @@ function Filter({ active, to, children, icon: Icon, end, ...props }) {
     );
   }
 
-  return (
-    <button {...props} className={classes.element}>
-      {content}
-    </button>
-  );
+  return <div className={classes.element}>{content}</div>;
 }
 
 export default Filter;

webui/src/list/FilterToolbar.js 🔗

@@ -32,7 +32,7 @@ function FilterToolbar({ query, queryLocation }) {
   };
 
   // TODO: open/closed count
-  // TODO: author/label/sort filters
+  // TODO: author/label filters
   return (
     <Toolbar className={classes.toolbar}>
       <Filter
@@ -52,7 +52,20 @@ function FilterToolbar({ query, queryLocation }) {
       <div className={classes.spacer} />
       <Filter active={hasKey('author')}>Author</Filter>
       <Filter active={hasKey('label')}>Label</Filter>
-      <Filter active={hasKey('sort')}>Sort</Filter>
+      <Filter
+        dropdown={[
+          ['id', 'ID'],
+          ['creation', 'Newest'],
+          ['creation-asc', 'Oldest'],
+          ['edit', 'Recently updated'],
+          ['edit-asc', 'Least recently updated'],
+        ]}
+        active={hasKey('sort')}
+        itemActive={key => hasValue('sort', key)}
+        to={key => replaceParam('sort', key)}
+      >
+        Sort
+      </Filter>
     </Toolbar>
   );
 }

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() {
         <h1>Issues</h1>
         <form onSubmit={formSubmit}>
           <InputBase
+            placeholder="Filter"
             value={input}
             onInput={e => setInput(e.target.value)}
-            className={classes.search}
+            classes={{
+              root: classes.search,
+              focused: classes.searchFocused,
+            }}
           />
           <button type="submit" hidden>
             Search