feat: multiple label filter

Aien Saidi created

- add search functionality in menu items

Change summary

webui/src/pages/list/Filter.tsx        | 60 +++++++++++++++++++++------
webui/src/pages/list/FilterToolbar.tsx | 34 +++++++++++----
2 files changed, 71 insertions(+), 23 deletions(-)

Detailed changes

webui/src/pages/list/Filter.tsx 🔗

@@ -1,14 +1,26 @@
 import clsx from 'clsx';
 import { LocationDescriptor } from 'history';
-import React, { useState, useRef } from 'react';
+import React, { useRef, useState } from 'react';
 import { Link } from 'react-router-dom';
 
 import Menu from '@material-ui/core/Menu';
 import MenuItem from '@material-ui/core/MenuItem';
 import { SvgIconProps } from '@material-ui/core/SvgIcon';
-import { makeStyles } from '@material-ui/core/styles';
+import TextField from '@material-ui/core/TextField';
+import { makeStyles, withStyles } from '@material-ui/core/styles';
 import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
 
+const CustomTextField = withStyles({
+  root: {
+    margin: '0 8px 12px 8px',
+    '& label.Mui-focused': {
+      margin: '0 2px',
+    },
+  },
+})(TextField);
+
+const ITEM_HEIGHT = 48;
+
 export type Query = { [key: string]: string[] };
 
 function parse(query: string): Query {
@@ -90,6 +102,7 @@ type FilterDropdownProps = {
   itemActive: (key: string) => boolean;
   icon?: React.ComponentType<SvgIconProps>;
   to: (key: string) => LocationDescriptor;
+  hasFilter?: boolean;
 } & React.ButtonHTMLAttributes<HTMLButtonElement>;
 
 function FilterDropdown({
@@ -98,9 +111,11 @@ function FilterDropdown({
   itemActive,
   icon: Icon,
   to,
+  hasFilter,
   ...props
 }: FilterDropdownProps) {
   const [open, setOpen] = useState(false);
+  const [filter, setFilter] = useState<string>('');
   const buttonRef = useRef<HTMLButtonElement>(null);
   const classes = useStyles({ active: false });
 
@@ -135,18 +150,36 @@ function FilterDropdown({
         open={open}
         onClose={() => setOpen(false)}
         anchorEl={buttonRef.current}
+        PaperProps={{
+          style: {
+            maxHeight: ITEM_HEIGHT * 4.5,
+            width: '20ch',
+          },
+        }}
       >
-        {dropdown.map(([key, value]) => (
-          <MenuItem
-            component={Link}
-            to={to(key)}
-            className={itemActive(key) ? classes.itemActive : undefined}
-            onClick={() => setOpen(false)}
-            key={key}
-          >
-            {value}
-          </MenuItem>
-        ))}
+        {hasFilter && (
+          <CustomTextField
+            onChange={(e) => {
+              const { value } = e.target;
+              setFilter(value);
+            }}
+            value={filter}
+            label={`Filter ${children}`}
+          />
+        )}
+        {dropdown
+          .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase()))
+          .map(([key, value]) => (
+            <MenuItem
+              component={Link}
+              to={to(key)}
+              className={itemActive(key) ? classes.itemActive : undefined}
+              onClick={() => setOpen(false)}
+              key={key}
+            >
+              {value}
+            </MenuItem>
+          ))}
       </Menu>
     </>
   );
@@ -158,6 +191,7 @@ export type FilterProps = {
   icon?: React.ComponentType<SvgIconProps>;
   children: React.ReactNode;
 };
+
 function Filter({ active, to, children, icon: Icon }: FilterProps) {
   const classes = useStyles();
 

webui/src/pages/list/FilterToolbar.tsx 🔗

@@ -109,6 +109,20 @@ function FilterToolbar({ query, queryLocation }: Props) {
     ...params,
     [key]: params[key] && params[key].includes(value) ? [] : [value],
   });
+  const toggleOrAddParam = (key: string, value: string) => (
+    params: Query
+  ): Query => {
+    const values = params[key];
+    return {
+      ...params,
+      [key]:
+        params[key] && params[key].includes(value)
+          ? values.filter((v) => v !== value)
+          : values
+          ? [...values, value]
+          : [value],
+    };
+  };
   const clearParam = (key: string) => (params: Query): Query => ({
     ...params,
     [key]: [],
@@ -150,18 +164,18 @@ function FilterToolbar({ query, queryLocation }: Props) {
         dropdown={identities}
         itemActive={(key) => hasValue('author', key)}
         to={(key) => pipe(replaceParam('author', key), loc)(params)}
+        hasFilter
       >
         Author
       </FilterDropdown>
-      {labels.length ? (
-        <FilterDropdown
-          dropdown={labels}
-          itemActive={(key) => hasValue('label', key)}
-          to={(key) => pipe(replaceParam('label', key), loc)(params)}
-        >
-          Label
-        </FilterDropdown>
-      ) : null}
+      <FilterDropdown
+        dropdown={labels}
+        itemActive={(key) => hasValue('label', key)}
+        to={(key) => pipe(toggleOrAddParam('label', key), loc)(params)}
+        hasFilter
+      >
+        Label
+      </FilterDropdown>
       <FilterDropdown
         dropdown={[
           ['id', 'ID'],
@@ -171,7 +185,7 @@ function FilterToolbar({ query, queryLocation }: Props) {
           ['edit-asc', 'Least recently updated'],
         ]}
         itemActive={(key) => hasValue('sort', key)}
-        to={(key) => pipe(replaceParam('sort', key), loc)(params)}
+        to={(key) => pipe(toggleParam('sort', key), loc)(params)}
       >
         Sort
       </FilterDropdown>