aboutsummaryrefslogtreecommitdiffstats
path: root/webui
diff options
context:
space:
mode:
authorQuentin Gliech <quentingliech@gmail.com>2020-02-11 20:16:56 +0100
committerQuentin Gliech <quentingliech@gmail.com>2020-02-11 20:54:38 +0100
commite5f52401b2a839881fedef5a446f0ed21d2d34c2 (patch)
tree2baee2e99ffe2833b9bb6acb317e951606f72ba2 /webui
parentb83670821cfb36de211c1d9bc077dad43496d7eb (diff)
downloadgit-bug-e5f52401b2a839881fedef5a446f0ed21d2d34c2.tar.gz
webui: typecheck remaining bug list components
Diffstat (limited to 'webui')
-rw-r--r--webui/package-lock.json11
-rw-r--r--webui/package.json2
-rw-r--r--webui/src/CurrentIdentity.tsx3
-rw-r--r--webui/src/Label.tsx3
-rw-r--r--webui/src/__tests__/query.ts (renamed from webui/src/__tests__/query.js)0
-rw-r--r--webui/src/list/Filter.tsx (renamed from webui/src/list/Filter.js)99
-rw-r--r--webui/src/list/FilterToolbar.graphql7
-rw-r--r--webui/src/list/FilterToolbar.tsx (renamed from webui/src/list/FilterToolbar.js)80
8 files changed, 128 insertions, 77 deletions
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 5c948a49..78551d98 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -127,6 +127,11 @@
"uuid": "^3.1.0"
}
},
+ "@arrows/composition": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
+ "integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
+ },
"@babel/code-frame": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
@@ -4536,9 +4541,9 @@
}
},
"clsx": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz",
- "integrity": "sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg=="
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz",
+ "integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA=="
},
"co": {
"version": "4.6.0",
diff --git a/webui/package.json b/webui/package.json
index 031d411b..03ac4da9 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
+ "@arrows/composition": "^1.2.2",
"@material-ui/core": "^4.9.0",
"@material-ui/icons": "^4.2.1",
"@material-ui/lab": "^4.0.0-alpha.40",
@@ -13,6 +14,7 @@
"@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.3",
"apollo-boost": "^0.4.7",
+ "clsx": "^1.1.0",
"graphql": "^14.6.0",
"graphql.macro": "^1.4.2",
"moment": "^2.24.0",
diff --git a/webui/src/CurrentIdentity.tsx b/webui/src/CurrentIdentity.tsx
index 0a697cdd..07ff648c 100644
--- a/webui/src/CurrentIdentity.tsx
+++ b/webui/src/CurrentIdentity.tsx
@@ -14,8 +14,7 @@ const CurrentIdentity = () => {
const classes = useStyles();
const { loading, error, data } = useCurrentIdentityQuery();
- if (error || loading || !data?.defaultRepository?.userIdentity)
- return null;
+ if (error || loading || !data?.defaultRepository?.userIdentity) return null;
const user = data.defaultRepository.userIdentity;
return (
diff --git a/webui/src/Label.tsx b/webui/src/Label.tsx
index e200f929..68c50b9d 100644
--- a/webui/src/Label.tsx
+++ b/webui/src/Label.tsx
@@ -18,7 +18,8 @@ const getTextColor = (background: string) =>
? common.white // White on dark backgrounds
: common.black; // And black on light ones
-const _rgb = (color: Color) => 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
+const _rgb = (color: Color) =>
+ 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
// Create a style object from the label RGB colors
const createStyle = (color: Color) => ({
diff --git a/webui/src/__tests__/query.js b/webui/src/__tests__/query.ts
index 5f4b58eb..5f4b58eb 100644
--- a/webui/src/__tests__/query.js
+++ b/webui/src/__tests__/query.ts
diff --git a/webui/src/list/Filter.js b/webui/src/list/Filter.tsx
index a6cf3633..d0091306 100644
--- a/webui/src/list/Filter.js
+++ b/webui/src/list/Filter.tsx
@@ -1,13 +1,18 @@
import React, { useState, useRef } from 'react';
import { Link } from 'react-router-dom';
-import { makeStyles } from '@material-ui/styles';
+import { LocationDescriptor } from 'history';
+import clsx from 'clsx';
+import { makeStyles } from '@material-ui/core/styles';
+import { SvgIconProps } from '@material-ui/core/SvgIcon';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
-function parse(query) {
+export type Query = { [key: string]: Array<string> };
+
+function parse(query: string): Query {
// TODO: extract the rest of the query?
- const params = {};
+ const params: Query = {};
// TODO: support escaping without quotes
const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
@@ -29,7 +34,7 @@ function parse(query) {
return params;
}
-function quote(value) {
+function quote(value: string): string {
const hasSingle = value.includes("'");
const hasDouble = value.includes('"');
const hasSpaces = value.includes(' ');
@@ -49,19 +54,19 @@ function quote(value) {
return `"${value}"`;
}
-function stringify(params) {
- const parts = Object.entries(params).map(([key, values]) => {
+function stringify(params: Query): string {
+ const parts: string[][] = Object.entries(params).map(([key, values]) => {
return values.map(value => `${key}:${quote(value)}`);
});
- return [].concat(...parts).join(' ');
+ return new Array<string>().concat(...parts).join(' ');
}
const useStyles = makeStyles(theme => ({
element: {
...theme.typography.body2,
- color: ({ active }) => (active ? '#333' : '#444'),
+ color: '#444',
padding: theme.spacing(0, 1),
- fontWeight: ({ active }) => (active ? 600 : 400),
+ fontWeight: 400,
textDecoration: 'none',
display: 'flex',
background: 'none',
@@ -69,21 +74,51 @@ const useStyles = makeStyles(theme => ({
},
itemActive: {
fontWeight: 600,
+ color: '#333',
},
icon: {
paddingRight: theme.spacing(0.5),
},
}));
-function Dropdown({ children, dropdown, itemActive, to, ...props }) {
+type DropdownTuple = [string, string];
+
+type FilterDropdownProps = {
+ children: React.ReactNode;
+ dropdown: DropdownTuple[];
+ itemActive: (key: string) => boolean;
+ icon?: React.ComponentType<SvgIconProps>;
+ to: (key: string) => LocationDescriptor;
+} & React.ButtonHTMLAttributes<HTMLButtonElement>;
+
+function FilterDropdown({
+ children,
+ dropdown,
+ itemActive,
+ icon: Icon,
+ to,
+ ...props
+}: FilterDropdownProps) {
const [open, setOpen] = useState(false);
- const buttonRef = useRef();
- const classes = useStyles();
+ const buttonRef = useRef<HTMLButtonElement>(null);
+ const classes = useStyles({ active: false });
+
+ const content = (
+ <>
+ {Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
+ <div>{children}</div>
+ </>
+ );
return (
<>
- <button ref={buttonRef} onClick={() => setOpen(!open)} {...props}>
- {children}
+ <button
+ ref={buttonRef}
+ onClick={() => setOpen(!open)}
+ className={classes.element}
+ {...props}
+ >
+ {content}
<ArrowDropDown fontSize="small" />
</button>
<Menu
@@ -104,7 +139,7 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
<MenuItem
component={Link}
to={to(key)}
- className={itemActive(key) ? classes.itemActive : null}
+ className={itemActive(key) ? classes.itemActive : undefined}
onClick={() => setOpen(false)}
key={key}
>
@@ -116,8 +151,14 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
);
}
-function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
- const classes = useStyles({ active });
+export type FilterProps = {
+ active: boolean;
+ to: LocationDescriptor;
+ icon?: React.ComponentType<SvgIconProps>;
+ children: React.ReactNode;
+};
+function Filter({ active, to, children, icon: Icon }: FilterProps) {
+ const classes = useStyles();
const content = (
<>
@@ -126,29 +167,23 @@ function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
</>
);
- if (dropdown) {
+ if (to) {
return (
- <Dropdown
- {...props}
+ <Link
to={to}
- dropdown={dropdown}
- className={classes.element}
+ className={clsx(classes.element, active && classes.itemActive)}
>
{content}
- </Dropdown>
- );
- }
-
- if (to) {
- return (
- <Link to={to} {...props} className={classes.element}>
- {content}
</Link>
);
}
- return <div className={classes.element}>{content}</div>;
+ return (
+ <div className={clsx(classes.element, active && classes.itemActive)}>
+ {content}
+ </div>
+ );
}
export default Filter;
-export { parse, stringify, quote };
+export { parse, stringify, quote, FilterDropdown, Filter };
diff --git a/webui/src/list/FilterToolbar.graphql b/webui/src/list/FilterToolbar.graphql
new file mode 100644
index 00000000..644a4eed
--- /dev/null
+++ b/webui/src/list/FilterToolbar.graphql
@@ -0,0 +1,7 @@
+query BugCount($query: String) {
+ defaultRepository {
+ bugs: allBugs(query: $query) {
+ totalCount
+ }
+ }
+}
diff --git a/webui/src/list/FilterToolbar.js b/webui/src/list/FilterToolbar.tsx
index 4d0b52b1..2aaf7f84 100644
--- a/webui/src/list/FilterToolbar.js
+++ b/webui/src/list/FilterToolbar.tsx
@@ -1,16 +1,19 @@
-import { makeStyles } from '@material-ui/styles';
-import { useQuery } from '@apollo/react-hooks';
-import gql from 'graphql-tag';
+import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
+import { LocationDescriptor } from 'history';
+import { pipe } from '@arrows/composition';
import Toolbar from '@material-ui/core/Toolbar';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
-import Filter, { parse, stringify } from './Filter';
-
-// simple pipe operator
-// pipe(o, f, g, h) <=> h(g(f(o)))
-// TODO: move this out?
-const pipe = (initial, ...funcs) => funcs.reduce((acc, f) => f(acc), initial);
+import {
+ FilterDropdown,
+ FilterProps,
+ Filter,
+ parse,
+ stringify,
+ Query,
+} from './Filter';
+import { useBugCountQuery } from './FilterToolbar.generated';
const useStyles = makeStyles(theme => ({
toolbar: {
@@ -25,25 +28,19 @@ const useStyles = makeStyles(theme => ({
},
}));
-const BUG_COUNT_QUERY = gql`
- query($query: String) {
- defaultRepository {
- bugs: allBugs(query: $query) {
- totalCount
- }
- }
- }
-`;
-
// This prepends the filter text with a count
-function CountingFilter({ query, children, ...props }) {
- const { data, loading, error } = useQuery(BUG_COUNT_QUERY, {
+type CountingFilterProps = {
+ query: string;
+ children: React.ReactNode;
+} & FilterProps;
+function CountingFilter({ query, children, ...props }: CountingFilterProps) {
+ const { data, loading, error } = useBugCountQuery({
variables: { query },
});
var prefix;
if (loading) prefix = '...';
- else if (error) prefix = '???';
+ else if (error || !data?.defaultRepository) prefix = '???';
// TODO: better prefixes & error handling
else prefix = data.defaultRepository.bugs.totalCount;
@@ -54,18 +51,26 @@ function CountingFilter({ query, children, ...props }) {
);
}
-function FilterToolbar({ query, queryLocation }) {
+type Props = {
+ query: string;
+ queryLocation: (query: string) => LocationDescriptor;
+};
+function FilterToolbar({ query, queryLocation }: Props) {
const classes = useStyles();
- const params = parse(query);
+ const params: Query = parse(query);
- const hasKey = key => params[key] && params[key].length > 0;
- const hasValue = (key, value) => hasKey(key) && params[key].includes(value);
- const loc = params => pipe(params, stringify, queryLocation);
- const replaceParam = (key, value) => params => ({
+ 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 loc = pipe(stringify, queryLocation);
+ const replaceParam = (key: string, value: string) => (
+ params: Query
+ ): Query => ({
...params,
[key]: [value],
});
- const clearParam = key => params => ({
+ const clearParam = (key: string) => (params: Query): Query => ({
...params,
[key]: [],
});
@@ -76,12 +81,11 @@ function FilterToolbar({ query, queryLocation }) {
<CountingFilter
active={hasValue('status', 'open')}
query={pipe(
- params,
replaceParam('status', 'open'),
clearParam('sort'),
stringify
- )}
- to={pipe(params, replaceParam('status', 'open'), loc)}
+ )(params)}
+ to={pipe(replaceParam('status', 'open'), loc)(params)}
icon={ErrorOutline}
>
open
@@ -89,12 +93,11 @@ function FilterToolbar({ query, queryLocation }) {
<CountingFilter
active={hasValue('status', 'closed')}
query={pipe(
- params,
replaceParam('status', 'closed'),
clearParam('sort'),
stringify
- )}
- to={pipe(params, replaceParam('status', 'closed'), loc)}
+ )(params)}
+ to={pipe(replaceParam('status', 'closed'), loc)(params)}
icon={CheckCircleOutline}
>
closed
@@ -104,7 +107,7 @@ function FilterToolbar({ query, queryLocation }) {
<Filter active={hasKey('author')}>Author</Filter>
<Filter active={hasKey('label')}>Label</Filter>
*/}
- <Filter
+ <FilterDropdown
dropdown={[
['id', 'ID'],
['creation', 'Newest'],
@@ -112,12 +115,11 @@ function FilterToolbar({ query, queryLocation }) {
['edit', 'Recently updated'],
['edit-asc', 'Least recently updated'],
]}
- active={hasKey('sort')}
itemActive={key => hasValue('sort', key)}
- to={key => pipe(params, replaceParam('sort', key), loc)}
+ to={key => pipe(replaceParam('sort', key), loc)(params)}
>
Sort
- </Filter>
+ </FilterDropdown>
</Toolbar>
);
}