aboutsummaryrefslogtreecommitdiffstats
path: root/webui/src/pages/list
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2021-04-09 13:01:14 +0200
committerMichael Muré <batolettre@gmail.com>2021-04-09 13:01:14 +0200
commit1520f678f7a2bc6e01d9b01df5ce49f2f46be7d7 (patch)
treef6d71c1f29cf06ccab9e4ae434b19ab17caa4385 /webui/src/pages/list
parent0fd570171d171aa574d7f01d6033a9c01d668465 (diff)
parentbc5f618eba812859bf87ce2c31b278bd518d4555 (diff)
downloadgit-bug-1520f678f7a2bc6e01d9b01df5ce49f2f46be7d7.tar.gz
Merge remote-tracking branch 'origin/master' into dev-gh-bridge
Diffstat (limited to 'webui/src/pages/list')
-rw-r--r--webui/src/pages/list/BugRow.graphql3
-rw-r--r--webui/src/pages/list/BugRow.tsx16
-rw-r--r--webui/src/pages/list/Filter.tsx78
-rw-r--r--webui/src/pages/list/FilterToolbar.tsx75
-rw-r--r--webui/src/pages/list/ListIdentities.graphql13
-rw-r--r--webui/src/pages/list/ListLabels.graphql9
-rw-r--r--webui/src/pages/list/ListQuery.tsx147
7 files changed, 273 insertions, 68 deletions
diff --git a/webui/src/pages/list/BugRow.graphql b/webui/src/pages/list/BugRow.graphql
index 547c09d8..e4e2760c 100644
--- a/webui/src/pages/list/BugRow.graphql
+++ b/webui/src/pages/list/BugRow.graphql
@@ -9,5 +9,8 @@ fragment BugRow on Bug {
labels {
...Label
}
+ comments {
+ totalCount
+ }
...authored
}
diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx
index 8d8fb5cb..1f5d22aa 100644
--- a/webui/src/pages/list/BugRow.tsx
+++ b/webui/src/pages/list/BugRow.tsx
@@ -6,6 +6,7 @@ import TableRow from '@material-ui/core/TableRow/TableRow';
import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import { makeStyles } from '@material-ui/core/styles';
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import CommentOutlinedIcon from '@material-ui/icons/CommentOutlined';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import Date from 'src/components/Date';
@@ -74,6 +75,13 @@ const useStyles = makeStyles((theme) => ({
display: 'inline-block',
},
},
+ commentCount: {
+ fontSize: '1rem',
+ marginLeft: theme.spacing(0.5),
+ },
+ commentCountCell: {
+ display: 'inline-flex',
+ },
}));
type Props = {
@@ -82,6 +90,8 @@ type Props = {
function BugRow({ bug }: Props) {
const classes = useStyles();
+ // Subtract 1 from totalCount as 1 comment is the bug description
+ const commentCount = bug.comments.totalCount - 1;
return (
<TableRow hover>
<TableCell className={classes.cell}>
@@ -105,6 +115,12 @@ function BugRow({ bug }: Props) {
&nbsp;by {bug.author.displayName}
</div>
</div>
+ {commentCount > 0 && (
+ <span className={classes.commentCountCell}>
+ <CommentOutlinedIcon aria-label="Comment count" />
+ <span className={classes.commentCount}>{commentCount}</span>
+ </span>
+ )}
</TableCell>
</TableRow>
);
diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx
index 5c4a3d17..2e99eedf 100644
--- a/webui/src/pages/list/Filter.tsx
+++ b/webui/src/pages/list/Filter.tsx
@@ -1,14 +1,33 @@
import clsx from 'clsx';
import { LocationDescriptor } from 'history';
-import React, { useState, useRef } from 'react';
+import React, { useRef, useState, useEffect } 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((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 {
@@ -65,7 +84,7 @@ function stringify(params: Query): string {
const useStyles = makeStyles((theme) => ({
element: {
...theme.typography.body2,
- color: '#444',
+ color: theme.palette.text.secondary,
padding: theme.spacing(0, 1),
fontWeight: 400,
textDecoration: 'none',
@@ -75,7 +94,7 @@ const useStyles = makeStyles((theme) => ({
},
itemActive: {
fontWeight: 600,
- color: '#333',
+ color: theme.palette.text.primary,
},
icon: {
paddingRight: theme.spacing(0.5),
@@ -90,6 +109,7 @@ type FilterDropdownProps = {
itemActive: (key: string) => boolean;
icon?: React.ComponentType<SvgIconProps>;
to: (key: string) => LocationDescriptor;
+ hasFilter?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
function FilterDropdown({
@@ -98,12 +118,19 @@ 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 searchRef = useRef<HTMLButtonElement>(null);
const classes = useStyles({ active: false });
+ useEffect(() => {
+ searchRef && searchRef.current && searchRef.current.focus();
+ }, [filter]);
+
const content = (
<>
{Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
@@ -124,6 +151,7 @@ function FilterDropdown({
</button>
<Menu
getContentAnchorEl={null}
+ ref={searchRef}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
@@ -135,18 +163,37 @@ function FilterDropdown({
open={open}
onClose={() => setOpen(false)}
anchorEl={buttonRef.current}
+ PaperProps={{
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5,
+ width: '25ch',
+ },
+ }}
>
- {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);
+ }}
+ onKeyDown={(e) => e.stopPropagation()}
+ 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 +205,7 @@ export type FilterProps = {
icon?: React.ComponentType<SvgIconProps>;
children: React.ReactNode;
};
+
function Filter({ active, to, children, icon: Icon }: FilterProps) {
const classes = useStyles();
diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx
index 21626416..979bf530 100644
--- a/webui/src/pages/list/FilterToolbar.tsx
+++ b/webui/src/pages/list/FilterToolbar.tsx
@@ -8,19 +8,21 @@ import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import {
+ Filter,
FilterDropdown,
FilterProps,
- Filter,
parse,
- stringify,
Query,
+ stringify,
} from './Filter';
import { useBugCountQuery } from './FilterToolbar.generated';
+import { useListIdentitiesQuery } from './ListIdentities.generated';
+import { useListLabelsQuery } from './ListLabels.generated';
const useStyles = makeStyles((theme) => ({
toolbar: {
- backgroundColor: theme.palette.grey['100'],
- borderColor: theme.palette.grey['300'],
+ backgroundColor: theme.palette.primary.light,
+ borderColor: theme.palette.divider,
borderWidth: '1px 0',
borderStyle: 'solid',
margin: theme.spacing(0, -1),
@@ -35,12 +37,13 @@ type CountingFilterProps = {
query: string; // the query used as a source to count the number of element
children: React.ReactNode;
} & FilterProps;
+
function CountingFilter({ query, children, ...props }: CountingFilterProps) {
const { data, loading, error } = useBugCountQuery({
variables: { query },
});
- var prefix;
+ let prefix;
if (loading) prefix = '...';
else if (error || !data?.repository) prefix = '???';
// TODO: better prefixes & error handling
@@ -57,14 +60,44 @@ type Props = {
query: string;
queryLocation: (query: string) => LocationDescriptor;
};
+
function FilterToolbar({ query, queryLocation }: Props) {
const classes = useStyles();
const params: Query = parse(query);
+ const { data: identitiesData } = useListIdentitiesQuery();
+ const { data: labelsData } = useListLabelsQuery();
+
+ let identities: any = [];
+ let labels: any = [];
+
+ if (
+ identitiesData?.repository &&
+ identitiesData.repository.allIdentities &&
+ identitiesData.repository.allIdentities.nodes
+ ) {
+ identities = identitiesData.repository.allIdentities.nodes.map((node) => [
+ node.name,
+ node.name,
+ ]);
+ }
+
+ if (
+ labelsData?.repository &&
+ labelsData.repository.validLabels &&
+ labelsData.repository.validLabels.nodes
+ ) {
+ labels = labelsData.repository.validLabels.nodes.map((node) => [
+ node.name,
+ node.name,
+ ]);
+ }
const hasKey = (key: string): boolean =>
params[key] && params[key].length > 0;
const hasValue = (key: string, value: string): boolean =>
hasKey(key) && params[key].includes(value);
+ const containsValue = (key: string, value: string): boolean =>
+ hasKey(key) && params[key].indexOf(value) !== -1;
const loc = pipe(stringify, queryLocation);
const replaceParam = (key: string, value: string) => (
params: Query
@@ -78,6 +111,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]: [],
@@ -116,6 +163,22 @@ function FilterToolbar({ query, queryLocation }: Props) {
<Filter active={hasKey('label')}>Label</Filter>
*/}
<FilterDropdown
+ dropdown={identities}
+ itemActive={(key) => hasValue('author', key)}
+ to={(key) => pipe(toggleOrAddParam('author', key), loc)(params)}
+ hasFilter
+ >
+ Author
+ </FilterDropdown>
+ <FilterDropdown
+ dropdown={labels}
+ itemActive={(key) => containsValue('label', key)}
+ to={(key) => pipe(toggleOrAddParam('label', key), loc)(params)}
+ hasFilter
+ >
+ Labels
+ </FilterDropdown>
+ <FilterDropdown
dropdown={[
['id', 'ID'],
['creation', 'Newest'],
@@ -124,7 +187,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>
diff --git a/webui/src/pages/list/ListIdentities.graphql b/webui/src/pages/list/ListIdentities.graphql
new file mode 100644
index 00000000..73073ae8
--- /dev/null
+++ b/webui/src/pages/list/ListIdentities.graphql
@@ -0,0 +1,13 @@
+query ListIdentities {
+ repository {
+ allIdentities {
+ nodes {
+ id
+ humanId
+ name
+ email
+ displayName
+ }
+ }
+ }
+}
diff --git a/webui/src/pages/list/ListLabels.graphql b/webui/src/pages/list/ListLabels.graphql
new file mode 100644
index 00000000..dcb44b67
--- /dev/null
+++ b/webui/src/pages/list/ListLabels.graphql
@@ -0,0 +1,9 @@
+query ListLabels {
+ repository {
+ validLabels {
+ nodes {
+ name
+ }
+ }
+ }
+}
diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx
index 87c21e3c..2b46dca5 100644
--- a/webui/src/pages/list/ListQuery.tsx
+++ b/webui/src/pages/list/ListQuery.tsx
@@ -1,19 +1,23 @@
import { ApolloError } from '@apollo/client';
+import { pipe } from '@arrows/composition';
import React, { useState, useEffect, useRef } from 'react';
import { useLocation, useHistory, Link } from 'react-router-dom';
-import { Button } from '@material-ui/core';
+import { Button, FormControl, Menu, MenuItem } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import InputBase from '@material-ui/core/InputBase';
import Paper from '@material-ui/core/Paper';
-import { fade, makeStyles, Theme } from '@material-ui/core/styles';
+import { makeStyles, Theme } from '@material-ui/core/styles';
+import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
import Skeleton from '@material-ui/lab/Skeleton';
+import { useCurrentIdentityQuery } from '../../components/CurrentIdentity/CurrentIdentity.generated';
import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn';
+import { parse, Query, stringify } from './Filter';
import FilterToolbar from './FilterToolbar';
import List from './List';
import { useListBugsQuery } from './ListQuery.generated';
@@ -35,33 +39,27 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
},
header: {
display: 'flex',
- padding: theme.spacing(2),
- '& > h1': {
- ...theme.typography.h6,
- margin: theme.spacing(0, 2),
- },
- alignItems: 'center',
- justifyContent: 'space-between',
+ padding: theme.spacing(1),
},
filterissueLabel: {
fontSize: '14px',
fontWeight: 'bold',
paddingRight: '12px',
},
- filterissueContainer: {
+ form: {
display: 'flex',
- flexDirection: 'row',
- alignItems: 'flex-start',
- justifyContents: 'left',
+ flexGrow: 1,
+ marginRight: theme.spacing(1),
},
search: {
borderRadius: theme.shape.borderRadius,
- borderColor: fade(theme.palette.primary.main, 0.2),
+ color: theme.palette.text.secondary,
+ borderColor: theme.palette.divider,
borderStyle: 'solid',
borderWidth: '1px',
- backgroundColor: fade(theme.palette.primary.main, 0.05),
+ backgroundColor: theme.palette.primary.light,
padding: theme.spacing(0, 1),
- width: ({ searching }) => (searching ? '20rem' : '15rem'),
+ width: '100%',
transition: theme.transitions.create([
'width',
'borderColor',
@@ -69,13 +67,11 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
]),
},
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'],
+ borderBottomColor: theme.palette.divider,
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
display: 'flex',
@@ -91,7 +87,8 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
...theme.typography.h5,
padding: theme.spacing(8),
textAlign: 'center',
- borderBottomColor: theme.palette.grey['300'],
+ color: theme.palette.text.hint,
+ borderBottomColor: theme.palette.divider,
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
'& > p': {
@@ -99,21 +96,25 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
},
},
errorBox: {
- color: theme.palette.error.main,
+ color: theme.palette.error.dark,
'& > pre': {
fontSize: '1rem',
textAlign: 'left',
- backgroundColor: theme.palette.grey['900'],
- color: theme.palette.common.white,
+ borderColor: theme.palette.divider,
+ borderWidth: '1px',
+ borderRadius: theme.shape.borderRadius,
+ borderStyle: 'solid',
+ color: theme.palette.text.primary,
marginTop: theme.spacing(4),
padding: theme.spacing(2, 3),
},
},
greenButton: {
- backgroundColor: '#2ea44fd9',
- color: '#fff',
+ backgroundColor: theme.palette.success.main,
+ color: theme.palette.success.contrastText,
'&:hover': {
- backgroundColor: '#2ea44f',
+ backgroundColor: theme.palette.success.dark,
+ color: theme.palette.primary.contrastText,
},
},
}));
@@ -188,6 +189,8 @@ function ListQuery() {
const query = params.has('q') ? params.get('q') || '' : 'status:open';
const [input, setInput] = useState(query);
+ const [filterMenuIsOpen, setFilterMenuIsOpen] = useState(false);
+ const filterButtonRef = useRef<HTMLButtonElement>(null);
const classes = useStyles({ searching: !!input });
@@ -289,37 +292,87 @@ function ListQuery() {
history.push(queryLocation(input));
};
+ const {
+ loading: ciqLoading,
+ error: ciqError,
+ data: ciqData,
+ } = useCurrentIdentityQuery();
+ if (ciqError || ciqLoading || !ciqData?.repository?.userIdentity) {
+ return null;
+ }
+ const user = ciqData.repository.userIdentity;
+
+ const loc = pipe(stringify, queryLocation);
+ const qparams: Query = parse(query);
+ const replaceParam = (key: string, value: string) => (
+ params: Query
+ ): Query => ({
+ ...params,
+ [key]: [value],
+ });
+
return (
<Paper className={classes.main}>
<header className={classes.header}>
- <div className="filterissueContainer">
- <form onSubmit={formSubmit}>
- <label className={classes.filterissueLabel} htmlFor="issuefilter">
- Filter
- </label>
- <InputBase
- id="issuefilter"
- placeholder="Filter"
- value={input}
- onInput={(e: any) => setInput(e.target.value)}
- classes={{
- root: classes.search,
- focused: classes.searchFocused,
+ <form className={classes.form} onSubmit={formSubmit}>
+ <FormControl>
+ <Button
+ aria-haspopup="true"
+ ref={filterButtonRef}
+ onClick={(e) => setFilterMenuIsOpen(true)}
+ >
+ Filter <ArrowDropDownIcon />
+ </Button>
+ <Menu
+ open={filterMenuIsOpen}
+ onClose={() => setFilterMenuIsOpen(false)}
+ getContentAnchorEl={null}
+ anchorEl={filterButtonRef.current}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'left',
}}
- />
- <button type="submit" hidden>
- Search
- </button>
- </form>
- </div>
+ transformOrigin={{
+ vertical: 'top',
+ horizontal: 'left',
+ }}
+ >
+ <MenuItem
+ component={Link}
+ to={pipe(
+ replaceParam('author', user.displayName),
+ replaceParam('sort', 'creation'),
+ loc
+ )(qparams)}
+ onClick={() => setFilterMenuIsOpen(false)}
+ >
+ Your newest issues
+ </MenuItem>
+ </Menu>
+ </FormControl>
+ <InputBase
+ id="issuefilter"
+ placeholder="Filter"
+ value={input}
+ onInput={(e: any) => setInput(e.target.value)}
+ classes={{
+ root: classes.search,
+ focused: classes.searchFocused,
+ }}
+ />
+ <button type="submit" hidden>
+ Search
+ </button>
+ </form>
<IfLoggedIn>
{() => (
<Button
className={classes.greenButton}
variant="contained"
- href="/new"
+ component={Link}
+ to="/new"
>
- New issue
+ New bug
</Button>
)}
</IfLoggedIn>