diff options
-rw-r--r-- | bridge/github/import.go | 2 | ||||
-rw-r--r-- | webui/.eslintrc.js | 6 | ||||
-rw-r--r-- | webui/package-lock.json | 5 | ||||
-rw-r--r-- | webui/package.json | 4 | ||||
-rw-r--r-- | webui/src/App.tsx | 65 | ||||
-rw-r--r-- | webui/src/Date.tsx | 12 | ||||
-rw-r--r-- | webui/src/Label.graphql | 8 | ||||
-rw-r--r-- | webui/src/__tests__/query.ts | 2 | ||||
-rw-r--r-- | webui/src/apollo.ts | 18 | ||||
-rw-r--r-- | webui/src/components/Author.tsx (renamed from webui/src/Author.tsx) | 5 | ||||
-rw-r--r-- | webui/src/components/Content/ImageTag.tsx (renamed from webui/src/tag/ImageTag.tsx) | 3 | ||||
-rw-r--r-- | webui/src/components/Content/PreTag.tsx (renamed from webui/src/tag/PreTag.tsx) | 3 | ||||
-rw-r--r-- | webui/src/components/Content/index.tsx (renamed from webui/src/Content.tsx) | 4 | ||||
-rw-r--r-- | webui/src/components/Date.tsx | 20 | ||||
-rw-r--r-- | webui/src/components/Label.tsx (renamed from webui/src/Label.tsx) | 8 | ||||
-rw-r--r-- | webui/src/components/fragments.graphql (renamed from webui/src/Author.graphql) | 11 | ||||
-rw-r--r-- | webui/src/index.tsx | 29 | ||||
-rw-r--r-- | webui/src/layout/CurrentIdentity.graphql (renamed from webui/src/CurrentIdentity.graphql) | 0 | ||||
-rw-r--r-- | webui/src/layout/CurrentIdentity.tsx (renamed from webui/src/CurrentIdentity.tsx) | 3 | ||||
-rw-r--r-- | webui/src/layout/Header.tsx | 50 | ||||
-rw-r--r-- | webui/src/layout/index.tsx | 18 | ||||
-rw-r--r-- | webui/src/pages/bug/Bug.graphql (renamed from webui/src/bug/Bug.graphql) | 3 | ||||
-rw-r--r-- | webui/src/pages/bug/Bug.tsx (renamed from webui/src/bug/Bug.tsx) | 29 | ||||
-rw-r--r-- | webui/src/pages/bug/BugQuery.graphql (renamed from webui/src/bug/BugQuery.graphql) | 0 | ||||
-rw-r--r-- | webui/src/pages/bug/BugQuery.tsx (renamed from webui/src/bug/BugQuery.tsx) | 3 | ||||
-rw-r--r-- | webui/src/pages/bug/CommentForm.graphql | 5 | ||||
-rw-r--r-- | webui/src/pages/bug/CommentForm.tsx | 146 | ||||
-rw-r--r-- | webui/src/pages/bug/LabelChange.tsx (renamed from webui/src/bug/LabelChange.tsx) | 11 | ||||
-rw-r--r-- | webui/src/pages/bug/LabelChangeFragment.graphql (renamed from webui/src/bug/LabelChangeFragment.graphql) | 3 | ||||
-rw-r--r-- | webui/src/pages/bug/Message.tsx (renamed from webui/src/bug/Message.tsx) | 11 | ||||
-rw-r--r-- | webui/src/pages/bug/MessageCommentFragment.graphql (renamed from webui/src/bug/MessageCommentFragment.graphql) | 2 | ||||
-rw-r--r-- | webui/src/pages/bug/MessageCreateFragment.graphql (renamed from webui/src/bug/MessageCreateFragment.graphql) | 2 | ||||
-rw-r--r-- | webui/src/pages/bug/SetStatus.tsx (renamed from webui/src/bug/SetStatus.tsx) | 21 | ||||
-rw-r--r-- | webui/src/pages/bug/SetStatusFragment.graphql (renamed from webui/src/bug/SetStatusFragment.graphql) | 2 | ||||
-rw-r--r-- | webui/src/pages/bug/SetTitle.tsx (renamed from webui/src/bug/SetTitle.tsx) | 24 | ||||
-rw-r--r-- | webui/src/pages/bug/SetTitleFragment.graphql (renamed from webui/src/bug/SetTitleFragment.graphql) | 2 | ||||
-rw-r--r-- | webui/src/pages/bug/Timeline.tsx (renamed from webui/src/bug/Timeline.tsx) | 3 | ||||
-rw-r--r-- | webui/src/pages/bug/TimelineQuery.graphql (renamed from webui/src/bug/TimelineQuery.graphql) | 0 | ||||
-rw-r--r-- | webui/src/pages/bug/TimelineQuery.tsx (renamed from webui/src/bug/TimelineQuery.tsx) | 3 | ||||
-rw-r--r-- | webui/src/pages/bug/index.tsx | 1 | ||||
-rw-r--r-- | webui/src/pages/list/BugRow.graphql (renamed from webui/src/list/BugRow.graphql) | 3 | ||||
-rw-r--r-- | webui/src/pages/list/BugRow.tsx (renamed from webui/src/list/BugRow.tsx) | 15 | ||||
-rw-r--r-- | webui/src/pages/list/Filter.tsx (renamed from webui/src/list/Filter.tsx) | 11 | ||||
-rw-r--r-- | webui/src/pages/list/FilterToolbar.graphql (renamed from webui/src/list/FilterToolbar.graphql) | 0 | ||||
-rw-r--r-- | webui/src/pages/list/FilterToolbar.tsx (renamed from webui/src/list/FilterToolbar.tsx) | 17 | ||||
-rw-r--r-- | webui/src/pages/list/List.tsx (renamed from webui/src/list/List.tsx) | 3 | ||||
-rw-r--r-- | webui/src/pages/list/ListQuery.graphql (renamed from webui/src/list/ListQuery.graphql) | 0 | ||||
-rw-r--r-- | webui/src/pages/list/ListQuery.tsx (renamed from webui/src/list/ListQuery.tsx) | 9 | ||||
-rw-r--r-- | webui/src/pages/list/index.ts | 1 | ||||
-rw-r--r-- | webui/src/theme.ts | 11 | ||||
-rw-r--r-- | webui/tsconfig.json | 16 |
51 files changed, 450 insertions, 183 deletions
diff --git a/bridge/github/import.go b/bridge/github/import.go index b30be73a..e80b9cfd 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -161,7 +161,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline b, _, err = repo.NewBugRaw( author, issue.CreatedAt.Unix(), - issue.Title, + issue.Title, // TODO: this is the *current* title, not the original one cleanText, nil, map[string]string{ diff --git a/webui/.eslintrc.js b/webui/.eslintrc.js index 7adbb8d5..2dfa7543 100644 --- a/webui/.eslintrc.js +++ b/webui/.eslintrc.js @@ -29,9 +29,13 @@ module.exports = { position: 'after', }, ], - groups: [['builtin', 'external'], 'parent', ['sibling', 'index']], + pathGroupsExcludedImportTypes: ["builtin"], + groups: [['builtin', 'external'], ['internal', 'parent'], ['sibling', 'index']], 'newlines-between': 'always', }, ], }, + settings: { + 'import/internal-regex': '^src/', + }, }; diff --git a/webui/package-lock.json b/webui/package-lock.json index 9bc9576c..d0c7d6f4 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -14608,6 +14608,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" }, + "react-moment": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.7.tgz", + "integrity": "sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g==" + }, "react-router": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", diff --git a/webui/package.json b/webui/package.json index cf61b883..902ed1db 100644 --- a/webui/package.json +++ b/webui/package.json @@ -21,6 +21,7 @@ "react": "^16.8.6", "react-apollo": "^3.1.3", "react-dom": "^16.8.6", + "react-moment": "^0.9.7", "react-router": "^5.0.0", "react-router-dom": "^5.0.0", "react-scripts": "^3.3.1", @@ -48,7 +49,8 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", "generate": "graphql-codegen", - "lint": "eslint src --ext .ts --ext .tsx --ext .js --ext .jsx --ext .graphql" + "lint": "eslint src --ext .ts --ext .tsx --ext .js --ext .jsx --ext .graphql", + "clean": "rimraf src/**.generated.* src/schema.json src/gqlTypes.* src/fragmentTypes.*" }, "proxy": "http://localhost:3001", "browserslist": [ diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 6f66a6ec..16663870 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,68 +1,17 @@ -import AppBar from '@material-ui/core/AppBar'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import Toolbar from '@material-ui/core/Toolbar'; -import { - createMuiTheme, - ThemeProvider, - makeStyles, -} from '@material-ui/core/styles'; import React from 'react'; import { Route, Switch } from 'react-router'; -import { Link } from 'react-router-dom'; -import CurrentIdentity from './CurrentIdentity'; -import BugQuery from './bug/BugQuery'; -import ListQuery from './list/ListQuery'; - -const theme = createMuiTheme({ - palette: { - primary: { - main: '#263238', - }, - }, -}); - -const useStyles = makeStyles(theme => ({ - offset: { - ...theme.mixins.toolbar, - }, - filler: { - flexGrow: 1, - }, - appTitle: { - ...theme.typography.h6, - color: 'white', - textDecoration: 'none', - display: 'flex', - alignItems: 'center', - }, - logo: { - height: '42px', - marginRight: theme.spacing(2), - }, -})); +import Layout from './layout'; +import BugPage from './pages/bug'; +import ListPage from './pages/list'; export default function App() { - const classes = useStyles(); - return ( - <ThemeProvider theme={theme}> - <CssBaseline /> - <AppBar position="fixed" color="primary"> - <Toolbar> - <Link to="/" className={classes.appTitle}> - <img src="/logo.svg" className={classes.logo} alt="git-bug" /> - git-bug - </Link> - <div className={classes.filler}></div> - <CurrentIdentity /> - </Toolbar> - </AppBar> - <div className={classes.offset} /> + <Layout> <Switch> - <Route path="/" exact component={ListQuery} /> - <Route path="/bug/:id" exact component={BugQuery} /> + <Route path="/" exact component={ListPage} /> + <Route path="/bug/:id" exact component={BugPage} /> </Switch> - </ThemeProvider> + </Layout> ); } diff --git a/webui/src/Date.tsx b/webui/src/Date.tsx deleted file mode 100644 index 9380d2fc..00000000 --- a/webui/src/Date.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import moment from 'moment'; -import React from 'react'; - -type Props = { date: string }; -const Date = ({ date }: Props) => ( - <Tooltip title={moment(date).format('MMMM D, YYYY, h:mm a')}> - <span> {moment(date).fromNow()} </span> - </Tooltip> -); - -export default Date; diff --git a/webui/src/Label.graphql b/webui/src/Label.graphql deleted file mode 100644 index 22522ada..00000000 --- a/webui/src/Label.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment Label on Label { - name - color { - R - G - B - } -} diff --git a/webui/src/__tests__/query.ts b/webui/src/__tests__/query.ts index 5f4b58eb..2f04817c 100644 --- a/webui/src/__tests__/query.ts +++ b/webui/src/__tests__/query.ts @@ -1,4 +1,4 @@ -import { parse, stringify, quote } from '../list/Filter'; +import { parse, stringify, quote } from 'src/pages/list/Filter'; it('parses a simple query', () => { expect(parse('foo:bar')).toEqual({ diff --git a/webui/src/apollo.ts b/webui/src/apollo.ts new file mode 100644 index 00000000..785f0e7f --- /dev/null +++ b/webui/src/apollo.ts @@ -0,0 +1,18 @@ +import ApolloClient from 'apollo-boost'; +import { + IntrospectionFragmentMatcher, + InMemoryCache, +} from 'apollo-cache-inmemory'; + +import introspectionQueryResultData from './fragmentTypes'; + +const client = new ApolloClient({ + uri: '/graphql', + cache: new InMemoryCache({ + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }), +}); + +export default client; diff --git a/webui/src/Author.tsx b/webui/src/components/Author.tsx index 852cd2b7..9ac1da52 100644 --- a/webui/src/Author.tsx +++ b/webui/src/components/Author.tsx @@ -1,8 +1,9 @@ +import React from 'react'; + import MAvatar from '@material-ui/core/Avatar'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import React from 'react'; -import { AuthoredFragment } from './Author.generated'; +import { AuthoredFragment } from './fragments.generated'; type Props = AuthoredFragment & { className?: string; diff --git a/webui/src/tag/ImageTag.tsx b/webui/src/components/Content/ImageTag.tsx index bdb36873..70ee1bc0 100644 --- a/webui/src/tag/ImageTag.tsx +++ b/webui/src/components/Content/ImageTag.tsx @@ -1,6 +1,7 @@ -import { makeStyles } from '@material-ui/styles'; import React from 'react'; +import { makeStyles } from '@material-ui/styles'; + const useStyles = makeStyles({ tag: { maxWidth: '100%', diff --git a/webui/src/tag/PreTag.tsx b/webui/src/components/Content/PreTag.tsx index d3b4c273..5256ab12 100644 --- a/webui/src/tag/PreTag.tsx +++ b/webui/src/components/Content/PreTag.tsx @@ -1,6 +1,7 @@ -import { makeStyles } from '@material-ui/styles'; import React from 'react'; +import { makeStyles } from '@material-ui/styles'; + const useStyles = makeStyles({ tag: { maxWidth: '100%', diff --git a/webui/src/Content.tsx b/webui/src/components/Content/index.tsx index 3a7af2f8..56e52e1e 100644 --- a/webui/src/Content.tsx +++ b/webui/src/components/Content/index.tsx @@ -4,8 +4,8 @@ import parse from 'remark-parse'; import remark2react from 'remark-react'; import unified from 'unified'; -import ImageTag from './tag/ImageTag'; -import PreTag from './tag/PreTag'; +import ImageTag from './ImageTag'; +import PreTag from './PreTag'; type Props = { markdown: string }; const Content: React.FC<Props> = ({ markdown }: Props) => { diff --git a/webui/src/components/Date.tsx b/webui/src/components/Date.tsx new file mode 100644 index 00000000..146a3496 --- /dev/null +++ b/webui/src/components/Date.tsx @@ -0,0 +1,20 @@ +import moment from 'moment'; +import React from 'react'; +import Moment from 'react-moment'; + +import Tooltip from '@material-ui/core/Tooltip/Tooltip'; + +const HOUR = 1000 * 3600; +const DAY = 24 * HOUR; +const WEEK = 7 * DAY; + +type Props = { date: string }; +const Date = ({ date }: Props) => ( + <Tooltip title={moment(date).format('LLLL')}> + <span> + on <Moment date={date} format="ll" fromNowDuring={WEEK} /> + </span> + </Tooltip> +); + +export default Date; diff --git a/webui/src/Label.tsx b/webui/src/components/Label.tsx index a33b4c2c..1fb8caea 100644 --- a/webui/src/Label.tsx +++ b/webui/src/components/Label.tsx @@ -1,13 +1,15 @@ +import React from 'react'; + import { common } from '@material-ui/core/colors'; import { makeStyles } from '@material-ui/core/styles'; import { getContrastRatio, darken, } from '@material-ui/core/styles/colorManipulator'; -import React from 'react'; -import { LabelFragment } from './Label.generated'; -import { Color } from './gqlTypes'; +import { Color } from 'src/gqlTypes'; + +import { LabelFragment } from './fragments.generated'; // Minimum contrast between the background and the text color const contrastThreshold = 2.5; diff --git a/webui/src/Author.graphql b/webui/src/components/fragments.graphql index 76d66b91..03a235f9 100644 --- a/webui/src/Author.graphql +++ b/webui/src/components/fragments.graphql @@ -1,3 +1,14 @@ +# Label.tsx +fragment Label on Label { + name + color { + R + G + B + } +} + +# Author.tsx fragment authored on Authored { author { name diff --git a/webui/src/index.tsx b/webui/src/index.tsx index c64daf0c..9bdaddca 100644 --- a/webui/src/index.tsx +++ b/webui/src/index.tsx @@ -1,36 +1,19 @@ -import { createMuiTheme } from '@material-ui/core/styles'; -import ThemeProvider from '@material-ui/styles/ThemeProvider'; -import ApolloClient from 'apollo-boost'; -import { - IntrospectionFragmentMatcher, - InMemoryCache, -} from 'apollo-cache-inmemory'; import React from 'react'; import { ApolloProvider } from 'react-apollo'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; -import App from './App'; -import introspectionQueryResultData from './fragmentTypes'; - -const theme = createMuiTheme(); +import ThemeProvider from '@material-ui/styles/ThemeProvider'; -const client = new ApolloClient({ - uri: '/graphql', - cache: new InMemoryCache({ - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }), -}); +import App from './App'; +import apolloClient from './apollo'; +import theme from './theme'; ReactDOM.render( - <ApolloProvider client={client}> + <ApolloProvider client={apolloClient}> <BrowserRouter> <ThemeProvider theme={theme}> - <React.Suspense fallback={'Loading…'}> - <App /> - </React.Suspense> + <App /> </ThemeProvider> </BrowserRouter> </ApolloProvider>, diff --git a/webui/src/CurrentIdentity.graphql b/webui/src/layout/CurrentIdentity.graphql index 2794a40f..2794a40f 100644 --- a/webui/src/CurrentIdentity.graphql +++ b/webui/src/layout/CurrentIdentity.graphql diff --git a/webui/src/CurrentIdentity.tsx b/webui/src/layout/CurrentIdentity.tsx index 256f44c4..21f489ef 100644 --- a/webui/src/CurrentIdentity.tsx +++ b/webui/src/layout/CurrentIdentity.tsx @@ -1,6 +1,7 @@ +import React from 'react'; + import Avatar from '@material-ui/core/Avatar'; import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; diff --git a/webui/src/layout/Header.tsx b/webui/src/layout/Header.tsx new file mode 100644 index 00000000..317d3e23 --- /dev/null +++ b/webui/src/layout/Header.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import { makeStyles } from '@material-ui/core/styles'; + +import CurrentIdentity from './CurrentIdentity'; + +const useStyles = makeStyles(theme => ({ + offset: { + ...theme.mixins.toolbar, + }, + filler: { + flexGrow: 1, + }, + appTitle: { + ...theme.typography.h6, + color: 'white', + textDecoration: 'none', + display: 'flex', + alignItems: 'center', + }, + logo: { + height: '42px', + marginRight: theme.spacing(2), + }, +})); + +function Header() { + const classes = useStyles(); + + return ( + <> + <AppBar position="fixed" color="primary"> + <Toolbar> + <Link to="/" className={classes.appTitle}> + <img src="/logo.svg" className={classes.logo} alt="git-bug" /> + git-bug + </Link> + <div className={classes.filler}></div> + <CurrentIdentity /> + </Toolbar> + </AppBar> + <div className={classes.offset} /> + </> + ); +} + +export default Header; diff --git a/webui/src/layout/index.tsx b/webui/src/layout/index.tsx new file mode 100644 index 00000000..42a0cfc1 --- /dev/null +++ b/webui/src/layout/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import CssBaseline from '@material-ui/core/CssBaseline'; + +import Header from './Header'; + +type Props = { children: React.ReactNode }; +function Layout({ children }: Props) { + return ( + <> + <CssBaseline /> + <Header /> + {children} + </> + ); +} + +export default Layout; diff --git a/webui/src/bug/Bug.graphql b/webui/src/pages/bug/Bug.graphql index 112024aa..498242c0 100644 --- a/webui/src/bug/Bug.graphql +++ b/webui/src/pages/bug/Bug.graphql @@ -1,5 +1,4 @@ -#import "../Label.graphql" -#import "../Author.graphql" +#import "../components/fragments.graphql" fragment Bug on Bug { id diff --git a/webui/src/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index f4029a5f..3c4bb63b 100644 --- a/webui/src/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -1,17 +1,19 @@ +import React from 'react'; + import Typography from '@material-ui/core/Typography/Typography'; import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; -import Author from '../Author'; -import Date from '../Date'; -import Label from '../Label'; +import Author from 'src/components/Author'; +import Date from 'src/components/Date'; +import Label from 'src/components/Label'; import { BugFragment } from './Bug.generated'; +import CommentForm from './CommentForm'; import TimelineQuery from './TimelineQuery'; const useStyles = makeStyles(theme => ({ main: { - maxWidth: 800, + maxWidth: 1000, margin: 'auto', marginTop: theme.spacing(4), }, @@ -39,6 +41,9 @@ const useStyles = makeStyles(theme => ({ marginTop: theme.spacing(2), flex: '0 0 200px', }, + sidebarTitle: { + fontWeight: 'bold', + }, labelList: { listStyle: 'none', padding: 0, @@ -51,6 +56,12 @@ const useStyles = makeStyles(theme => ({ display: 'block', }, }, + noLabel: { + ...theme.typography.body2, + }, + commentForm: { + marginLeft: 48, + }, })); type Props = { @@ -75,10 +86,16 @@ function Bug({ bug }: Props) { <div className={classes.container}> <div className={classes.timeline}> <TimelineQuery id={bug.id} /> + <div className={classes.commentForm}> + <CommentForm bugId={bug.id} /> + </div> </div> <div className={classes.sidebar}> - <Typography variant={'subtitle1'}>Labels</Typography> + <span className={classes.sidebarTitle}>Labels</span> <ul className={classes.labelList}> + {bug.labels.length === 0 && ( + <span className={classes.noLabel}>None yet</span> + )} {bug.labels.map(l => ( <li className={classes.label} key={l.name}> <Label label={l} key={l.name} /> diff --git a/webui/src/bug/BugQuery.graphql b/webui/src/pages/bug/BugQuery.graphql index cdc4723f..cdc4723f 100644 --- a/webui/src/bug/BugQuery.graphql +++ b/webui/src/pages/bug/BugQuery.graphql diff --git a/webui/src/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx index 2ecf718c..2a70a2f8 100644 --- a/webui/src/bug/BugQuery.tsx +++ b/webui/src/pages/bug/BugQuery.tsx @@ -1,7 +1,8 @@ -import CircularProgress from '@material-ui/core/CircularProgress'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import CircularProgress from '@material-ui/core/CircularProgress'; + import Bug from './Bug'; import { useGetBugQuery } from './BugQuery.generated'; diff --git a/webui/src/pages/bug/CommentForm.graphql b/webui/src/pages/bug/CommentForm.graphql new file mode 100644 index 00000000..33d21193 --- /dev/null +++ b/webui/src/pages/bug/CommentForm.graphql @@ -0,0 +1,5 @@ +mutation AddComment($input: AddCommentInput!) { + addComment(input: $input) { + operation { id } + } +} diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx new file mode 100644 index 00000000..3724baf0 --- /dev/null +++ b/webui/src/pages/bug/CommentForm.tsx @@ -0,0 +1,146 @@ +import React, { useState, useRef } from 'react'; + +import Button from '@material-ui/core/Button'; +import Paper from '@material-ui/core/Paper'; +import Tab from '@material-ui/core/Tab'; +import Tabs from '@material-ui/core/Tabs'; +import TextField from '@material-ui/core/TextField'; +import { makeStyles, Theme } from '@material-ui/core/styles'; + +import Content from 'src/components/Content'; + +import { useAddCommentMutation } from './CommentForm.generated'; +import { TimelineDocument } from './TimelineQuery.generated'; + +type StyleProps = { loading: boolean }; +const useStyles = makeStyles<Theme, StyleProps>(theme => ({ + container: { + margin: theme.spacing(2, 0), + padding: theme.spacing(0, 2, 2, 2), + }, + textarea: {}, + tabContent: { + margin: theme.spacing(2, 0), + }, + preview: { + borderBottom: `solid 3px ${theme.palette.grey['200']}`, + minHeight: '5rem', + }, + actions: { + display: 'flex', + justifyContent: 'flex-end', + }, +})); + +type TabPanelProps = { + children: React.ReactNode; + value: number; + index: number; +} & React.HTMLProps<HTMLDivElement>; +function TabPanel({ children, value, index, ...props }: TabPanelProps) { + return ( + <div + role="tabpanel" + hidden={value !== index} + id={`editor-tabpanel-${index}`} + aria-labelledby={`editor-tab-${index}`} + {...props} + > + {value === index && children} + </div> + ); +} + +const a11yProps = (index: number) => ({ + id: `editor-tab-${index}`, + 'aria-controls': `editor-tabpanel-${index}`, +}); + +type Props = { + bugId: string; +}; + +function CommentForm({ bugId }: Props) { + const [addComment, { loading }] = useAddCommentMutation(); + const [input, setInput] = useState<string>(''); + const [tab, setTab] = useState(0); + const classes = useStyles({ loading }); + const form = useRef<HTMLFormElement>(null); + + const submit = () => { + addComment({ + variables: { + input: { + prefix: bugId, + message: input, + }, + }, + refetchQueries: [ + // TODO: update the cache instead of refetching + { + query: TimelineDocument, + variables: { + id: bugId, + first: 100, + }, + }, + ], + awaitRefetchQueries: true, + }).then(() => setInput('')); + }; + + const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + submit(); + }; + + const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => { + // Submit on cmd/ctrl+enter + if ((e.metaKey || e.altKey) && e.keyCode === 13) { + submit(); + } + }; + + return ( + <Paper className={classes.container}> + <form onSubmit={handleSubmit} ref={form}> + <Tabs value={tab} onChange={(_, t) => setTab(t)}> + <Tab label="Write" {...a11yProps(0)} /> + <Tab label="Preview" {...a11yProps(1)} /> + </Tabs> + <div className={classes.tabContent}> + <TabPanel value={tab} index={0}> + <TextField + onKeyDown={handleKeyDown} + fullWidth + label="Comment" + placeholder="Leave a comment" + className={classes.textarea} + multiline + value={input} + variant="filled" + rows="4" // TODO: rowsMin support + onChange={(e: any) => setInput(e.target.value)} + disabled={loading} + /> + </TabPanel> + <TabPanel value={tab} index={1} className={classes.preview}> + <Content markdown={input} /> + </TabPanel> + </div> + <div className={classes.actions}> + <Button + variant="contained" + color="primary" + type="submit" + disabled={loading} + > + Comment + </Button> + </div> + </form> + </Paper> + ); +} + +export default CommentForm; diff --git a/webui/src/bug/LabelChange.tsx b/webui/src/pages/bug/LabelChange.tsx index 572579bd..93fa8a32 100644 --- a/webui/src/bug/LabelChange.tsx +++ b/webui/src/pages/bug/LabelChange.tsx @@ -1,15 +1,16 @@ -import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import Author from '../Author'; -import Date from '../Date'; -import Label from '../Label'; +import { makeStyles } from '@material-ui/core/styles'; + +import Author from 'src/components/Author'; +import Date from 'src/components/Date'; +import Label from 'src/components/Label'; import { LabelChangeFragment } from './LabelChangeFragment.generated'; const useStyles = makeStyles(theme => ({ main: { - ...theme.typography.body1, + ...theme.typography.body2, marginLeft: theme.spacing(1) + 40, }, author: { diff --git a/webui/src/bug/LabelChangeFragment.graphql b/webui/src/pages/bug/LabelChangeFragment.graphql index 631de70c..82d41235 100644 --- a/webui/src/bug/LabelChangeFragment.graphql +++ b/webui/src/pages/bug/LabelChangeFragment.graphql @@ -1,5 +1,4 @@ -#import "../Author.graphql" -#import "../Label.graphql" +#import "../../components/fragments.graphql" fragment LabelChange on LabelChangeTimelineItem { date diff --git a/webui/src/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index c8d0710d..4a438b77 100644 --- a/webui/src/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -1,11 +1,11 @@ +import React from 'react'; + import Paper from '@material-ui/core/Paper'; import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; -import Author from '../Author'; -import { Avatar } from '../Author'; -import Content from '../Content'; -import Date from '../Date'; +import Author, { Avatar } from 'src/components/Author'; +import Content from 'src/components/Content'; +import Date from 'src/components/Date'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; @@ -31,6 +31,7 @@ const useStyles = makeStyles(theme => ({ padding: '0.5rem 1rem', borderBottom: '1px solid #ddd', display: 'flex', + backgroundColor: '#e2f1ff', }, title: { flex: 1, diff --git a/webui/src/bug/MessageCommentFragment.graphql b/webui/src/pages/bug/MessageCommentFragment.graphql index 38d626d0..00f8342d 100644 --- a/webui/src/bug/MessageCommentFragment.graphql +++ b/webui/src/pages/bug/MessageCommentFragment.graphql @@ -1,4 +1,4 @@ -#import "../Author.graphql" +#import "../../components/fragments.graphql" fragment AddComment on AddCommentTimelineItem { createdAt diff --git a/webui/src/bug/MessageCreateFragment.graphql b/webui/src/pages/bug/MessageCreateFragment.graphql index 08477470..4cae819d 100644 --- a/webui/src/bug/MessageCreateFragment.graphql +++ b/webui/src/pages/bug/MessageCreateFragment.graphql @@ -1,4 +1,4 @@ -#import "../Author.graphql" +#import "../../components/fragments.graphql" fragment Create on CreateTimelineItem { createdAt diff --git a/webui/src/bug/SetStatus.tsx b/webui/src/pages/bug/SetStatus.tsx index 3e1a7989..e9674424 100644 --- a/webui/src/bug/SetStatus.tsx +++ b/webui/src/pages/bug/SetStatus.tsx @@ -1,16 +1,21 @@ -import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import Author from '../Author'; -import Date from '../Date'; +import { makeStyles } from '@material-ui/core/styles'; + +import { Status } from '../../gqlTypes'; +import Author from 'src/components/Author'; +import Date from 'src/components/Date'; import { SetStatusFragment } from './SetStatusFragment.generated'; const useStyles = makeStyles(theme => ({ main: { - ...theme.typography.body1, + ...theme.typography.body2, marginLeft: theme.spacing(1) + 40, }, + author: { + fontWeight: 'bold', + }, })); type Props = { @@ -19,10 +24,14 @@ type Props = { function SetStatus({ op }: Props) { const classes = useStyles(); + const status = { [Status.Open]: 'reopened', [Status.Closed]: 'closed' }[ + op.status + ]; + return ( <div className={classes.main}> - <Author author={op.author} bold /> - <span> {op.status.toLowerCase()} this</span> + <Author author={op.author} className={classes.author} /> + <span> {status} this </span> <Date date={op.date} /> </div> ); diff --git a/webui/src/bug/SetStatusFragment.graphql b/webui/src/pages/bug/SetStatusFragment.graphql index 0fdea01b..d8380409 100644 --- a/webui/src/bug/SetStatusFragment.graphql +++ b/webui/src/pages/bug/SetStatusFragment.graphql @@ -1,4 +1,4 @@ -#import "../Author.graphql" +#import "../../components/fragments.graphql" fragment SetStatus on SetStatusTimelineItem { date diff --git a/webui/src/bug/SetTitle.tsx b/webui/src/pages/bug/SetTitle.tsx index 0b088e0b..64b97517 100644 --- a/webui/src/bug/SetTitle.tsx +++ b/webui/src/pages/bug/SetTitle.tsx @@ -1,17 +1,25 @@ -import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import Author from '../Author'; -import Date from '../Date'; +import { makeStyles } from '@material-ui/core/styles'; + +import Author from 'src/components/Author'; +import Date from 'src/components/Date'; import { SetTitleFragment } from './SetTitleFragment.generated'; const useStyles = makeStyles(theme => ({ main: { - ...theme.typography.body1, + ...theme.typography.body2, marginLeft: theme.spacing(1) + 40, }, - bold: { + author: { + fontWeight: 'bold', + }, + before: { + fontWeight: 'bold', + textDecoration: 'line-through', + }, + after: { fontWeight: 'bold', }, })); @@ -24,11 +32,11 @@ function SetTitle({ op }: Props) { const classes = useStyles(); return ( <div className={classes.main}> - <Author author={op.author} className={classes.bold} /> + <Author author={op.author} className={classes.author} /> <span> changed the title from </span> - <span className={classes.bold}>{op.was}</span> + <span className={classes.before}>{op.was}</span> <span> to </span> - <span className={classes.bold}>{op.title}</span> + <span className={classes.after}>{op.title}</span> <Date date={op.date} /> </div> ); diff --git a/webui/src/bug/SetTitleFragment.graphql b/webui/src/pages/bug/SetTitleFragment.graphql index 432c4449..2225dfd3 100644 --- a/webui/src/bug/SetTitleFragment.graphql +++ b/webui/src/pages/bug/SetTitleFragment.graphql @@ -1,4 +1,4 @@ -#import "../Author.graphql" +#import "../../components/fragments.graphql" fragment SetTitle on SetTitleTimelineItem { date diff --git a/webui/src/bug/Timeline.tsx b/webui/src/pages/bug/Timeline.tsx index ba0f9fc7..73c88cdf 100644 --- a/webui/src/bug/Timeline.tsx +++ b/webui/src/pages/bug/Timeline.tsx @@ -1,6 +1,7 @@ -import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + import LabelChange from './LabelChange'; import Message from './Message'; import SetStatus from './SetStatus'; diff --git a/webui/src/bug/TimelineQuery.graphql b/webui/src/pages/bug/TimelineQuery.graphql index 6d78ab7f..6d78ab7f 100644 --- a/webui/src/bug/TimelineQuery.graphql +++ b/webui/src/pages/bug/TimelineQuery.graphql diff --git a/webui/src/bug/TimelineQuery.tsx b/webui/src/pages/bug/TimelineQuery.tsx index 9c4cf183..74eed52b 100644 --- a/webui/src/bug/TimelineQuery.tsx +++ b/webui/src/pages/bug/TimelineQuery.tsx @@ -1,6 +1,7 @@ -import CircularProgress from '@material-ui/core/CircularProgress'; import React from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; + import Timeline from './Timeline'; import { useTimelineQuery } from './TimelineQuery.generated'; diff --git a/webui/src/pages/bug/index.tsx b/webui/src/pages/bug/index.tsx new file mode 100644 index 00000000..a3bbcea4 --- /dev/null +++ b/webui/src/pages/bug/index.tsx @@ -0,0 +1 @@ +export { default } from './BugQuery'; diff --git a/webui/src/list/BugRow.graphql b/webui/src/pages/list/BugRow.graphql index 3f9a1ef6..547c09d8 100644 --- a/webui/src/list/BugRow.graphql +++ b/webui/src/pages/list/BugRow.graphql @@ -1,5 +1,4 @@ -#import "../Author.graphql" -#import "../Label.graphql" +#import "../../components/fragments.graphql" fragment BugRow on Bug { id diff --git a/webui/src/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx index f94538a7..9c1883a3 100644 --- a/webui/src/list/BugRow.tsx +++ b/webui/src/pages/list/BugRow.tsx @@ -1,15 +1,16 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + 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 { makeStyles } from '@material-ui/core/styles'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; -import React from 'react'; -import { Link } from 'react-router-dom'; -import Date from '../Date'; -import Label from '../Label'; -import { Status } from '../gqlTypes'; +import Date from 'src/components/Date'; +import Label from 'src/components/Label'; +import { Status } from 'src/gqlTypes'; import { BugRowFragment } from './BugRow.generated'; @@ -99,9 +100,9 @@ function BugRow({ bug }: Props) { </div> </Link> <div className={classes.details}> - {bug.humanId} opened + {bug.humanId} opened <Date date={bug.createdAt} /> - by {bug.author.displayName} + by {bug.author.displayName} </div> </div> </TableCell> diff --git a/webui/src/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 30b52de8..0635e7f0 100644 --- a/webui/src/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -1,12 +1,13 @@ +import clsx from 'clsx'; +import { LocationDescriptor } from 'history'; +import React, { useState, useRef } 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 ArrowDropDown from '@material-ui/icons/ArrowDropDown'; -import clsx from 'clsx'; -import { LocationDescriptor } from 'history'; -import React, { useState, useRef } from 'react'; -import { Link } from 'react-router-dom'; export type Query = { [key: string]: Array<string> }; @@ -153,7 +154,7 @@ function FilterDropdown({ export type FilterProps = { active: boolean; - to: LocationDescriptor; + to: LocationDescriptor; // the target on click icon?: React.ComponentType<SvgIconProps>; children: React.ReactNode; }; diff --git a/webui/src/list/FilterToolbar.graphql b/webui/src/pages/list/FilterToolbar.graphql index cd103f44..cd103f44 100644 --- a/webui/src/list/FilterToolbar.graphql +++ b/webui/src/pages/list/FilterToolbar.graphql diff --git a/webui/src/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index b95b10bc..c568a9dd 100644 --- a/webui/src/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -1,10 +1,11 @@ import { pipe } from '@arrows/composition'; +import { LocationDescriptor } from 'history'; +import React from 'react'; + import Toolbar from '@material-ui/core/Toolbar'; import { makeStyles } from '@material-ui/core/styles'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; -import { LocationDescriptor } from 'history'; -import React from 'react'; import { FilterDropdown, @@ -31,7 +32,7 @@ const useStyles = makeStyles(theme => ({ // This prepends the filter text with a count type CountingFilterProps = { - query: string; + query: string; // the query used as a source to count the number of element children: React.ReactNode; } & FilterProps; function CountingFilter({ query, children, ...props }: CountingFilterProps) { @@ -71,6 +72,12 @@ function FilterToolbar({ query, queryLocation }: Props) { ...params, [key]: [value], }); + const toggleParam = (key: string, value: string) => ( + params: Query + ): Query => ({ + ...params, + [key]: params[key] && params[key].includes(value) ? [] : [value], + }); const clearParam = (key: string) => (params: Query): Query => ({ ...params, [key]: [], @@ -86,7 +93,7 @@ function FilterToolbar({ query, queryLocation }: Props) { clearParam('sort'), stringify )(params)} - to={pipe(replaceParam('status', 'open'), loc)(params)} + to={pipe(toggleParam('status', 'open'), loc)(params)} icon={ErrorOutline} > open @@ -98,7 +105,7 @@ function FilterToolbar({ query, queryLocation }: Props) { clearParam('sort'), stringify )(params)} - to={pipe(replaceParam('status', 'closed'), loc)(params)} + to={pipe(toggleParam('status', 'closed'), loc)(params)} icon={CheckCircleOutline} > closed diff --git a/webui/src/list/List.tsx b/webui/src/pages/list/List.tsx index cebd13f2..c1cae122 100644 --- a/webui/src/list/List.tsx +++ b/webui/src/pages/list/List.tsx @@ -1,6 +1,7 @@ +import React from 'react'; + 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'; diff --git a/webui/src/list/ListQuery.graphql b/webui/src/pages/list/ListQuery.graphql index ded60c8a..ded60c8a 100644 --- a/webui/src/list/ListQuery.graphql +++ b/webui/src/pages/list/ListQuery.graphql diff --git a/webui/src/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 84b72431..2d8c698a 100644 --- a/webui/src/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -1,3 +1,7 @@ +import { ApolloError } from 'apollo-boost'; +import React, { useState, useEffect, useRef } from 'react'; +import { useLocation, useHistory, Link } from 'react-router-dom'; + import IconButton from '@material-ui/core/IconButton'; import InputBase from '@material-ui/core/InputBase'; import Paper from '@material-ui/core/Paper'; @@ -6,9 +10,6 @@ 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 { ApolloError } from 'apollo-boost'; -import React, { useState, useEffect, useRef } from 'react'; -import { useLocation, useHistory, Link } from 'react-router-dom'; import FilterToolbar from './FilterToolbar'; import List from './List'; @@ -163,7 +164,7 @@ function ListQuery() { const location = useLocation(); const history = useHistory(); const params = new URLSearchParams(location.search); - const query = params.get('q') || ''; + const query = params.has('q') ? params.get('q') || '' : 'status:open'; const [input, setInput] = useState(query); diff --git a/webui/src/pages/list/index.ts b/webui/src/pages/list/index.ts new file mode 100644 index 00000000..8a91ce70 --- /dev/null +++ b/webui/src/pages/list/index.ts @@ -0,0 +1 @@ +export { default } from './ListQuery'; diff --git a/webui/src/theme.ts b/webui/src/theme.ts new file mode 100644 index 00000000..d41cd731 --- /dev/null +++ b/webui/src/theme.ts @@ -0,0 +1,11 @@ +import { createMuiTheme } from '@material-ui/core/styles'; + +const theme = createMuiTheme({ + palette: { + primary: { + main: '#263238', + }, + }, +}); + +export default theme; diff --git a/webui/tsconfig.json b/webui/tsconfig.json index 30ba544b..4e83eef3 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -14,7 +18,13 @@ "isolatedModules": true, "noEmit": true, "jsx": "react", - "typeRoots": ["node_modules/@types/", "types/"] + "typeRoots": [ + "node_modules/@types/", + "types/" + ], + "baseUrl": "." }, - "include": ["src"] + "include": [ + "src" + ] } |