aboutsummaryrefslogtreecommitdiffstats
path: root/webui
diff options
context:
space:
mode:
Diffstat (limited to 'webui')
-rw-r--r--webui/.eslintrc.js6
-rw-r--r--webui/package-lock.json5
-rw-r--r--webui/package.json4
-rw-r--r--webui/src/App.tsx65
-rw-r--r--webui/src/Date.tsx12
-rw-r--r--webui/src/Label.graphql8
-rw-r--r--webui/src/__tests__/query.ts2
-rw-r--r--webui/src/apollo.ts18
-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.tsx20
-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.tsx29
-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.tsx50
-rw-r--r--webui/src/layout/index.tsx18
-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.graphql5
-rw-r--r--webui/src/pages/bug/CommentForm.tsx146
-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.tsx1
-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.ts1
-rw-r--r--webui/src/theme.ts11
-rw-r--r--webui/tsconfig.json16
50 files changed, 449 insertions, 182 deletions
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>&nbsp;
<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&nbsp;
<Date date={bug.createdAt} />
- by {bug.author.displayName}
+ &nbsp;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"
+ ]
}