diff options
author | Quentin Gliech <quentingliech@gmail.com> | 2020-02-04 20:57:43 +0100 |
---|---|---|
committer | Quentin Gliech <quentingliech@gmail.com> | 2020-02-11 20:54:37 +0100 |
commit | 6a502c145bd8f2e2e1a9c0b103c11f0433c60737 (patch) | |
tree | 72c3a23aa6c5df8013d53523fa4125a3e28063a8 /webui | |
parent | 9c570cac725fe7048ddd1d181b33b8fa1808e401 (diff) | |
download | git-bug-6a502c145bd8f2e2e1a9c0b103c11f0433c60737.tar.gz |
webui: convert bug list to typescript
Diffstat (limited to 'webui')
-rw-r--r-- | webui/codegen.yaml | 4 | ||||
-rw-r--r-- | webui/package-lock.json | 75 | ||||
-rw-r--r-- | webui/package.json | 2 | ||||
-rw-r--r-- | webui/src/bug/Bug.tsx | 2 | ||||
-rw-r--r-- | webui/src/bug/BugQuery.tsx | 6 | ||||
-rw-r--r-- | webui/src/list/BugRow.graphql | 14 | ||||
-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.graphql | 37 | ||||
-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> ); |