import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; import CheckIcon from '@mui/icons-material/Check'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { SvgIconProps } from '@mui/material/SvgIcon'; import TextField from '@mui/material/TextField'; import { darken } from '@mui/material/styles'; import makeStyles from '@mui/styles/makeStyles'; import withStyles from '@mui/styles/withStyles'; import clsx from 'clsx'; import * as React from 'react'; import { useRef, useState, useEffect } from 'react'; import { Location, Link } from 'react-router-dom'; import { Color } from '../../gqlTypes'; const CustomTextField = withStyles((theme) => ({ root: { margin: '0 8px 12px 8px', '& label.Mui-focused': { margin: '0 2px', color: theme.palette.text.secondary, }, '& .MuiInput-underline::before': { borderBottomColor: theme.palette.divider, }, '& .MuiInput-underline::after': { borderBottomColor: theme.palette.divider, }, }, }))(TextField); const ITEM_HEIGHT = 48; export type Query = { [key: string]: string[] }; function parse(query: string): Query { const params: Query = {}; let re = new RegExp(/([^:\s]+)(:('[^']*'\S*|"[^"]*"\S*|\S*))?/, 'g'); let matches; while ((matches = re.exec(query)) !== null) { if (!params[matches[1]]) { params[matches[1]] = []; } if (matches[3] !== undefined) { params[matches[1]].push(matches[3]); } else { params[matches[1]].push(''); } } return params; } function quote(value: string): string { const hasSpaces = value.includes(' '); const isSingleQuotedRegEx = RegExp(/^'.*'$/); const isDoubleQuotedRegEx = RegExp(/^".*"$/); const isQuoted = () => isDoubleQuotedRegEx.test(value) || isSingleQuotedRegEx.test(value); //Test if label name contains whitespace between quotes. If no quoates but //whitespace, then quote string. if (!isQuoted() && hasSpaces) { value = `"${value}"`; } //Convert single quote (tick) to double quote. This way quoting is always //uniform and can be relied upon by the label menu const hasSingle = value.includes(`'`); if (hasSingle) { value = value.replace(/'/g, `"`); } return value; } function stringify(params: Query): string { const parts: string[][] = Object.entries(params).map(([key, values]) => { return values.map((value) => value.length > 0 ? `${key}:${quote(value)}` : key ); }); return new Array().concat(...parts).join(' '); } const useStyles = makeStyles((theme) => ({ element: { ...theme.typography.body2, color: theme.palette.text.secondary, padding: theme.spacing(0, 1), fontWeight: 400, textDecoration: 'none', display: 'flex', background: 'none', border: 'none', }, itemActive: { fontWeight: 600, color: theme.palette.text.primary, }, icon: { paddingRight: theme.spacing(0.5), }, labelMenu: { '& .MuiMenu-paper': { //somehow using "width" won't override the default width... minWidth: '35ch', }, }, labelMenuItem: { whiteSpace: 'normal', wordBreak: 'break-word', display: 'flex', alignItems: 'initial', }, labelcolor: { minWidth: '0.5rem', display: 'flex', borderRadius: '0.25rem', marginRight: '5px', marginLeft: '3px', }, })); const _rgb = (color: Color) => 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; // Create a style object from the label RGB colors const createStyle = (color: Color) => ({ backgroundColor: _rgb(color), borderBottomColor: darken(_rgb(color), 0.2), }); type DropdownTuple = [string, string, Color?]; type FilterDropdownProps = { children: React.ReactNode; dropdown: DropdownTuple[]; itemActive: (key: string) => boolean; icon?: React.ComponentType; to: (key: string) => Location; hasFilter?: boolean; } & React.ButtonHTMLAttributes; function FilterDropdown({ children, dropdown, itemActive, icon: Icon, to, hasFilter, ...props }: FilterDropdownProps) { const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const buttonRef = useRef(null); const searchRef = useRef(null); const classes = useStyles({ active: false }); useEffect(() => { searchRef && searchRef.current && searchRef.current.focus(); }, [filter]); const content = ( <> {Icon && }
{children}
); return ( <> setOpen(false)} anchorEl={buttonRef.current} PaperProps={{ style: { maxHeight: ITEM_HEIGHT * 4.5, width: '25ch', }, }} > {hasFilter && ( { const { value } = e.target; setFilter(value); }} onKeyDown={(e) => e.stopPropagation()} value={filter} label={`Filter ${children}`} /> )} {dropdown .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase())) .map(([key, value, color]) => ( setOpen(false)} key={key} > {itemActive(key) && } {color && (
)} {value} ))}
); } export type FilterProps = { active: boolean; to: Location; // the target on click icon?: React.ComponentType; children: React.ReactNode; }; function Filter({ active, to, children, icon: Icon }: FilterProps) { const classes = useStyles(); const content = ( <> {Icon && }
{children}
); if (to) { return ( {content} ); } return (
{content}
); } export default Filter; export { parse, stringify, quote, FilterDropdown, Filter };