aboutsummaryrefslogtreecommitdiffstats
path: root/webui
diff options
context:
space:
mode:
authorQuentin Gliech <quentingliech@gmail.com>2020-02-04 20:57:43 +0100
committerQuentin Gliech <quentingliech@gmail.com>2020-02-11 20:54:37 +0100
commit6a502c145bd8f2e2e1a9c0b103c11f0433c60737 (patch)
tree72c3a23aa6c5df8013d53523fa4125a3e28063a8 /webui
parent9c570cac725fe7048ddd1d181b33b8fa1808e401 (diff)
downloadgit-bug-6a502c145bd8f2e2e1a9c0b103c11f0433c60737.tar.gz
webui: convert bug list to typescript
Diffstat (limited to 'webui')
-rw-r--r--webui/codegen.yaml4
-rw-r--r--webui/package-lock.json75
-rw-r--r--webui/package.json2
-rw-r--r--webui/src/bug/Bug.tsx2
-rw-r--r--webui/src/bug/BugQuery.tsx6
-rw-r--r--webui/src/list/BugRow.graphql14
-rw-r--r--webui/src/list/BugRow.tsx (renamed from webui/src/list/BugRow.js)45
-rw-r--r--webui/src/list/List.tsx (renamed from webui/src/list/List.js)4
-rw-r--r--webui/src/list/ListQuery.graphql37
-rw-r--r--webui/src/list/ListQuery.tsx (renamed from webui/src/list/ListQuery.js)168
10 files changed, 218 insertions, 139 deletions
diff --git a/webui/codegen.yaml b/webui/codegen.yaml
index 3cbcfb09..161fd1c7 100644
--- a/webui/codegen.yaml
+++ b/webui/codegen.yaml
@@ -12,6 +12,7 @@ generates:
- typescript
./src/:
plugins:
+ - add: '/* eslint-disable @typescript-eslint/no-unused-vars */'
- typescript-operations
- typescript-react-apollo
preset: near-operation-file
@@ -23,9 +24,6 @@ generates:
withHOC: false
withHooks: true
-config:
- documentMode: documentNode
-
hooks:
afterOneFileWrite:
- prettier --write
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 33b36565..5c948a49 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -1229,13 +1229,33 @@
}
},
"@graphql-codegen/add": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-1.12.1.tgz",
- "integrity": "sha512-i6+Al5+Z8WH4eIF4Nzsu2imXN1hLNPt+91v0Bm4n4XIOi3mbLtbEo8IxK354mOpriie1PCpUJq7Y9dofPONObA==",
+ "version": "1.12.2-alpha-ea7264f9.15",
+ "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-1.12.2-alpha-ea7264f9.15.tgz",
+ "integrity": "sha512-XfOZH2lIR3qw/mHqXThb32EA7NR37nPJpzuNtx1McGTy0sEEd5PVTLP4u89cgvMXfx18cMMM7ZWAnz2T7XCCkQ==",
"dev": true,
"requires": {
- "@graphql-codegen/plugin-helpers": "1.12.1",
+ "@graphql-codegen/plugin-helpers": "1.12.2-alpha-ea7264f9.15+ea7264f9",
"tslib": "1.10.0"
+ },
+ "dependencies": {
+ "@graphql-codegen/plugin-helpers": {
+ "version": "1.12.2-alpha-ea7264f9.15",
+ "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.12.2-alpha-ea7264f9.15.tgz",
+ "integrity": "sha512-EgRHQVFswVQUevMEtsrsA45JmTWj3UAUK8laMyDqbQQuOqlTOgpqdceTLYWWCpyfybaEaagw+rWpkwPXyUjWYQ==",
+ "dev": true,
+ "requires": {
+ "@graphql-toolkit/common": "0.9.7",
+ "camel-case": "4.1.1",
+ "common-tags": "1.8.0",
+ "constant-case": "3.0.3",
+ "import-from": "3.0.0",
+ "lower-case": "2.0.1",
+ "param-case": "3.0.3",
+ "pascal-case": "3.1.1",
+ "tslib": "1.10.0",
+ "upper-case": "2.0.1"
+ }
+ }
}
},
"@graphql-codegen/cli": {
@@ -1316,15 +1336,50 @@
}
},
"@graphql-codegen/near-operation-file-preset": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/@graphql-codegen/near-operation-file-preset/-/near-operation-file-preset-1.12.1.tgz",
- "integrity": "sha512-916+QqcUsnJOOgtRP4m7JDLnfwrQWufsRjdlDZL58pwrhNU0sRYObSyDH8RYhA8XIt5k29P+s2ZwHkGDRzGR1g==",
+ "version": "1.12.2-alpha-ea7264f9.15",
+ "resolved": "https://registry.npmjs.org/@graphql-codegen/near-operation-file-preset/-/near-operation-file-preset-1.12.2-alpha-ea7264f9.15.tgz",
+ "integrity": "sha512-jbj7+2FlHRLpqN3e44EZ88n2juImhMuXzv6Mlun4CEVkxC8zW6MYkptaeAxb+iCn2r2nO3vXNrNEPs/1czF97w==",
"dev": true,
"requires": {
- "@graphql-codegen/add": "1.12.1",
- "@graphql-codegen/plugin-helpers": "1.12.1",
- "@graphql-codegen/visitor-plugin-common": "1.12.1",
+ "@graphql-codegen/add": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+ "@graphql-codegen/plugin-helpers": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+ "@graphql-codegen/visitor-plugin-common": "1.12.2-alpha-ea7264f9.15+ea7264f9",
"tslib": "1.10.0"
+ },
+ "dependencies": {
+ "@graphql-codegen/plugin-helpers": {
+ "version": "1.12.2-alpha-ea7264f9.15",
+ "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.12.2-alpha-ea7264f9.15.tgz",
+ "integrity": "sha512-EgRHQVFswVQUevMEtsrsA45JmTWj3UAUK8laMyDqbQQuOqlTOgpqdceTLYWWCpyfybaEaagw+rWpkwPXyUjWYQ==",
+ "dev": true,
+ "requires": {
+ "@graphql-toolkit/common": "0.9.7",
+ "camel-case": "4.1.1",
+ "common-tags": "1.8.0",
+ "constant-case": "3.0.3",
+ "import-from": "3.0.0",
+ "lower-case": "2.0.1",
+ "param-case": "3.0.3",
+ "pascal-case": "3.1.1",
+ "tslib": "1.10.0",
+ "upper-case": "2.0.1"
+ }
+ },
+ "@graphql-codegen/visitor-plugin-common": {
+ "version": "1.12.2-alpha-ea7264f9.15",
+ "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.12.2-alpha-ea7264f9.15.tgz",
+ "integrity": "sha512-Y+4b5ArGOcXtGZ7gCLKhfOfiElH36uNSYs/8y0+9bxbjV1OuGfunnluysvpDSqIqatyVXviJh+P832VjO5Cviw==",
+ "dev": true,
+ "requires": {
+ "@graphql-codegen/plugin-helpers": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+ "@graphql-toolkit/relay-operation-optimizer": "0.9.7",
+ "auto-bind": "4.0.0",
+ "dependency-graph": "0.8.1",
+ "graphql-tag": "2.10.1",
+ "pascal-case": "3.1.1",
+ "tslib": "1.10.0"
+ }
+ }
}
},
"@graphql-codegen/plugin-helpers": {
diff --git a/webui/package.json b/webui/package.json
index 31a9eb73..031d411b 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -31,7 +31,7 @@
"devDependencies": {
"@graphql-codegen/cli": "^1.12.1",
"@graphql-codegen/fragment-matcher": "^1.12.1",
- "@graphql-codegen/near-operation-file-preset": "^1.12.1",
+ "@graphql-codegen/near-operation-file-preset": "^1.12.2-alpha-ea7264f9.15",
"@graphql-codegen/typescript-operations": "^1.12.1",
"@graphql-codegen/typescript-react-apollo": "^1.12.1",
"eslint-config-prettier": "^6.10.0",
diff --git a/webui/src/bug/Bug.tsx b/webui/src/bug/Bug.tsx
index 75b6ffff..3685b506 100644
--- a/webui/src/bug/Bug.tsx
+++ b/webui/src/bug/Bug.tsx
@@ -52,7 +52,7 @@ const useStyles = makeStyles(theme => ({
}));
type Props = {
- bug: BugFragment
+ bug: BugFragment;
};
function Bug({ bug }: Props) {
diff --git a/webui/src/bug/BugQuery.tsx b/webui/src/bug/BugQuery.tsx
index 6bf525e6..b436db5a 100644
--- a/webui/src/bug/BugQuery.tsx
+++ b/webui/src/bug/BugQuery.tsx
@@ -6,11 +6,13 @@ import { useGetBugQuery } from './BugQuery.generated';
import Bug from './Bug';
type Props = RouteComponentProps<{
- id: string
+ id: string;
}>;
const BugQuery: React.FC<Props> = ({ match }: Props) => {
- const { loading, error, data } = useGetBugQuery({ variables: { id: match.params.id } });
+ const { loading, error, data } = useGetBugQuery({
+ variables: { id: match.params.id },
+ });
if (loading) return <CircularProgress />;
if (error) return <p>Error: {error}</p>;
if (!data?.defaultRepository?.bug) return <p>404.</p>;
diff --git a/webui/src/list/BugRow.graphql b/webui/src/list/BugRow.graphql
new file mode 100644
index 00000000..3f9a1ef6
--- /dev/null
+++ b/webui/src/list/BugRow.graphql
@@ -0,0 +1,14 @@
+#import "../Author.graphql"
+#import "../Label.graphql"
+
+fragment BugRow on Bug {
+ id
+ humanId
+ title
+ status
+ createdAt
+ labels {
+ ...Label
+ }
+ ...authored
+}
diff --git a/webui/src/list/BugRow.js b/webui/src/list/BugRow.tsx
index add5c12f..6979b296 100644
--- a/webui/src/list/BugRow.js
+++ b/webui/src/list/BugRow.tsx
@@ -1,36 +1,41 @@
-import { makeStyles } from '@material-ui/styles';
+import { makeStyles } from '@material-ui/core/styles';
import TableCell from '@material-ui/core/TableCell/TableCell';
import TableRow from '@material-ui/core/TableRow/TableRow';
import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
-import gql from 'graphql-tag';
import React from 'react';
import { Link } from 'react-router-dom';
import Date from '../Date';
import Label from '../Label';
-import Author from '../Author';
+import { BugRowFragment } from './BugRow.generated';
+import { Status } from '../gqlTypes';
-const Open = ({ className }) => (
+type OpenClosedProps = { className: string };
+const Open = ({ className }: OpenClosedProps) => (
<Tooltip title="Open">
<ErrorOutline htmlColor="#28a745" className={className} />
</Tooltip>
);
-const Closed = ({ className }) => (
+const Closed = ({ className }: OpenClosedProps) => (
<Tooltip title="Closed">
<CheckCircleOutline htmlColor="#cb2431" className={className} />
</Tooltip>
);
-const Status = ({ status, className }) => {
+type StatusProps = { className: string; status: Status };
+const BugStatus: React.FC<StatusProps> = ({
+ status,
+ className,
+}: StatusProps) => {
switch (status) {
case 'OPEN':
return <Open className={className} />;
case 'CLOSED':
return <Closed className={className} />;
default:
- return 'unknown status ' + status;
+ return <p>{'unknown status ' + status}</p>;
}
};
@@ -57,7 +62,6 @@ const useStyles = makeStyles(theme => ({
fontWeight: 500,
},
details: {
- ...theme.typography.textSecondary,
lineHeight: '1.5rem',
color: theme.palette.text.secondary,
},
@@ -69,12 +73,16 @@ const useStyles = makeStyles(theme => ({
},
}));
-function BugRow({ bug }) {
+type Props = {
+ bug: BugRowFragment;
+};
+
+function BugRow({ bug }: Props) {
const classes = useStyles();
return (
<TableRow hover>
<TableCell className={classes.cell}>
- <Status status={bug.status} className={classes.status} />
+ <BugStatus status={bug.status} className={classes.status} />
<div className={classes.expand}>
<Link to={'bug/' + bug.humanId}>
<div className={classes.expand}>
@@ -99,21 +107,4 @@ function BugRow({ bug }) {
);
}
-BugRow.fragment = gql`
- fragment BugRow on Bug {
- id
- humanId
- title
- status
- createdAt
- labels {
- ...Label
- }
- ...authored
- }
-
- ${Label.fragment}
- ${Author.fragment}
-`;
-
export default BugRow;
diff --git a/webui/src/list/List.js b/webui/src/list/List.tsx
index 63b73545..23b193d4 100644
--- a/webui/src/list/List.js
+++ b/webui/src/list/List.tsx
@@ -2,8 +2,10 @@ import Table from '@material-ui/core/Table/Table';
import TableBody from '@material-ui/core/TableBody/TableBody';
import React from 'react';
import BugRow from './BugRow';
+import { BugListFragment } from './ListQuery.generated';
-function List({ bugs }) {
+type Props = { bugs: BugListFragment };
+function List({ bugs }: Props) {
return (
<Table>
<TableBody>
diff --git a/webui/src/list/ListQuery.graphql b/webui/src/list/ListQuery.graphql
new file mode 100644
index 00000000..bf9ea80a
--- /dev/null
+++ b/webui/src/list/ListQuery.graphql
@@ -0,0 +1,37 @@
+#import "./BugRow.graphql"
+
+query ListBugs(
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+ $query: String
+) {
+ defaultRepository {
+ bugs: allBugs(
+ first: $first
+ last: $last
+ after: $after
+ before: $before
+ query: $query
+ ) {
+ ...BugList
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ endCursor
+ }
+ }
+ }
+}
+
+fragment BugList on BugConnection {
+ totalCount
+ edges {
+ cursor
+ node {
+ ...BugRow
+ }
+ }
+}
diff --git a/webui/src/list/ListQuery.js b/webui/src/list/ListQuery.tsx
index 8eeec240..a9bb15df 100644
--- a/webui/src/list/ListQuery.js
+++ b/webui/src/list/ListQuery.tsx
@@ -1,4 +1,4 @@
-import { fade, makeStyles } from '@material-ui/core/styles';
+import { fade, makeStyles, Theme } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton';
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
@@ -6,15 +6,15 @@ import ErrorOutline from '@material-ui/icons/ErrorOutline';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
import Skeleton from '@material-ui/lab/Skeleton';
-import gql from 'graphql-tag';
import React, { useState, useEffect, useRef } from 'react';
-import { useQuery } from '@apollo/react-hooks';
import { useLocation, useHistory, Link } from 'react-router-dom';
-import BugRow from './BugRow';
+import { ApolloError } from 'apollo-boost';
import List from './List';
import FilterToolbar from './FilterToolbar';
+import { useListBugsQuery } from './ListQuery.generated';
-const useStyles = makeStyles(theme => ({
+type StylesProps = { searching?: boolean };
+const useStyles = makeStyles<Theme, StylesProps>(theme => ({
main: {
maxWidth: 800,
margin: 'auto',
@@ -46,7 +46,11 @@ const useStyles = makeStyles(theme => ({
backgroundColor: fade(theme.palette.primary.main, 0.05),
padding: theme.spacing(0, 1),
width: ({ searching }) => (searching ? '20rem' : '15rem'),
- transition: theme.transitions.create(),
+ transition: theme.transitions.create([
+ 'width',
+ 'borderColor',
+ 'backgroundColor',
+ ]),
},
searchFocused: {
borderColor: fade(theme.palette.primary.main, 0.4),
@@ -91,51 +95,21 @@ const useStyles = makeStyles(theme => ({
},
}));
-const QUERY = gql`
- query(
- $first: Int
- $last: Int
- $after: String
- $before: String
- $query: String
- ) {
- defaultRepository {
- bugs: allBugs(
- first: $first
- last: $last
- after: $after
- before: $before
- query: $query
- ) {
- totalCount
- edges {
- cursor
- node {
- ...BugRow
- }
- }
- pageInfo {
- hasNextPage
- hasPreviousPage
- startCursor
- endCursor
- }
- }
- }
- }
-
- ${BugRow.fragment}
-`;
-
-function editParams(params, callback) {
+function editParams(
+ params: URLSearchParams,
+ callback: (params: URLSearchParams) => void
+) {
const cloned = new URLSearchParams(params.toString());
callback(cloned);
return cloned;
}
// TODO: factor this out
-const Placeholder = ({ count }) => {
- const classes = useStyles();
+type PlaceholderProps = { count: number };
+const Placeholder: React.FC<PlaceholderProps> = ({
+ count,
+}: PlaceholderProps) => {
+ const classes = useStyles({});
return (
<>
{new Array(count).fill(null).map((_, i) => (
@@ -158,7 +132,7 @@ const Placeholder = ({ count }) => {
// TODO: factor this out
const NoBug = () => {
- const classes = useStyles();
+ const classes = useStyles({});
return (
<div className={classes.message}>
<ErrorOutline fontSize="large" />
@@ -167,8 +141,9 @@ const NoBug = () => {
);
};
-const Error = ({ error }) => {
- const classes = useStyles();
+type ErrorProps = { error: ApolloError };
+const Error: React.FC<ErrorProps> = ({ error }: ErrorProps) => {
+ const classes = useStyles({});
return (
<div className={[classes.errorBox, classes.message].join(' ')}>
<ErrorOutline fontSize="large" />
@@ -194,7 +169,7 @@ function ListQuery() {
const classes = useStyles({ searching: !!input });
// TODO is this the right way to do it?
- const lastQuery = useRef();
+ const lastQuery = useRef<string | null>(null);
useEffect(() => {
if (query !== lastQuery.current) {
setInput(query);
@@ -202,9 +177,10 @@ function ListQuery() {
lastQuery.current = query;
}, [query, input, lastQuery]);
+ const num = (param: string | null) => (param ? parseInt(param) : null);
const page = {
- first: params.get('first'),
- last: params.get('last'),
+ first: num(params.get('first')),
+ last: num(params.get('last')),
after: params.get('after'),
before: params.get('before'),
};
@@ -214,9 +190,9 @@ function ListQuery() {
page.first = 10;
}
- const perPage = page.first || page.last;
+ const perPage = (page.first || page.last || 10).toString();
- const { loading, error, data } = useQuery(QUERY, {
+ const { loading, error, data } = useListBugsQuery({
variables: {
...page,
query,
@@ -225,34 +201,34 @@ function ListQuery() {
let nextPage = null;
let previousPage = null;
- let hasNextPage = false;
- let hasPreviousPage = false;
let count = 0;
- if (!loading && !error && data.defaultRepository.bugs) {
+ if (!loading && !error && data?.defaultRepository?.bugs) {
const bugs = data.defaultRepository.bugs;
- hasNextPage = bugs.pageInfo.hasNextPage;
- hasPreviousPage = bugs.pageInfo.hasPreviousPage;
count = bugs.totalCount;
// This computes the URL for the next page
- nextPage = {
- ...location,
- search: editParams(params, p => {
- p.delete('last');
- p.delete('before');
- p.set('first', perPage);
- p.set('after', bugs.pageInfo.endCursor);
- }).toString(),
- };
+ if (bugs.pageInfo.hasNextPage) {
+ nextPage = {
+ ...location,
+ search: editParams(params, p => {
+ p.delete('last');
+ p.delete('before');
+ p.set('first', perPage);
+ p.set('after', bugs.pageInfo.endCursor);
+ }).toString(),
+ };
+ }
// and this for the previous page
- previousPage = {
- ...location,
- search: editParams(params, p => {
- p.delete('first');
- p.delete('after');
- p.set('last', perPage);
- p.set('before', bugs.pageInfo.startCursor);
- }).toString(),
- };
+ if (bugs.pageInfo.hasPreviousPage) {
+ previousPage = {
+ ...location,
+ search: editParams(params, p => {
+ p.delete('first');
+ p.delete('after');
+ p.set('last', perPage);
+ p.set('before', bugs.pageInfo.startCursor);
+ }).toString(),
+ };
+ }
}
// Prepare params without paging for editing filters
@@ -263,7 +239,7 @@ function ListQuery() {
p.delete('after');
});
// Returns a new location with the `q` param edited
- const queryLocation = query => ({
+ const queryLocation = (query: string) => ({
...location,
search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(),
});
@@ -273,7 +249,7 @@ function ListQuery() {
content = <Placeholder count={10} />;
} else if (error) {
content = <Error error={error} />;
- } else {
+ } else if (data?.defaultRepository) {
const bugs = data.defaultRepository.bugs;
if (bugs.totalCount === 0) {
@@ -283,7 +259,7 @@ function ListQuery() {
}
}
- const formSubmit = e => {
+ const formSubmit = (e: React.FormEvent) => {
e.preventDefault();
history.push(queryLocation(input));
};
@@ -296,7 +272,7 @@ function ListQuery() {
<InputBase
placeholder="Filter"
value={input}
- onInput={e => setInput(e.target.value)}
+ onInput={(e: any) => setInput(e.target.value)}
classes={{
root: classes.search,
focused: classes.searchFocused,
@@ -310,21 +286,25 @@ function ListQuery() {
<FilterToolbar query={query} queryLocation={queryLocation} />
{content}
<div className={classes.pagination}>
- <IconButton
- component={hasPreviousPage ? Link : 'button'}
- to={previousPage}
- disabled={!hasPreviousPage}
- >
- <KeyboardArrowLeft />
- </IconButton>
+ {previousPage ? (
+ <IconButton component={Link} to={previousPage}>
+ <KeyboardArrowLeft />
+ </IconButton>
+ ) : (
+ <IconButton disabled>
+ <KeyboardArrowLeft />
+ </IconButton>
+ )}
<div>{loading ? 'Loading' : `Total: ${count}`}</div>
- <IconButton
- component={hasNextPage ? Link : 'button'}
- to={nextPage}
- disabled={!hasNextPage}
- >
- <KeyboardArrowRight />
- </IconButton>
+ {nextPage ? (
+ <IconButton component={Link} to={nextPage}>
+ <KeyboardArrowRight />
+ </IconButton>
+ ) : (
+ <IconButton disabled>
+ <KeyboardArrowRight />
+ </IconButton>
+ )}
</div>
</Paper>
);