aboutsummaryrefslogtreecommitdiffstats
path: root/webui/src
diff options
context:
space:
mode:
Diffstat (limited to 'webui/src')
-rw-r--r--webui/src/App.tsx2
-rw-r--r--webui/src/components/BugTitleForm/BugTitleForm.tsx197
-rw-r--r--webui/src/components/BugTitleForm/SetTitle.graphql7
-rw-r--r--webui/src/components/CloseBugButton/CloseBug.graphql8
-rw-r--r--webui/src/components/CloseBugButton/CloseBugButton.tsx55
-rw-r--r--webui/src/components/ReopenBugButton/OpenBug.graphql7
-rw-r--r--webui/src/components/ReopenBugButton/ReopenBugButton.tsx55
-rw-r--r--webui/src/layout/CommentInput/CommentInput.tsx107
-rw-r--r--webui/src/pages/bug/Bug.tsx28
-rw-r--r--webui/src/pages/bug/CommentForm.tsx110
-rw-r--r--webui/src/pages/list/ListQuery.tsx57
-rw-r--r--webui/src/pages/new/NewBug.graphql7
-rw-r--r--webui/src/pages/new/NewBugPage.tsx118
13 files changed, 656 insertions, 102 deletions
diff --git a/webui/src/App.tsx b/webui/src/App.tsx
index 16663870..3a5ef025 100644
--- a/webui/src/App.tsx
+++ b/webui/src/App.tsx
@@ -4,12 +4,14 @@ import { Route, Switch } from 'react-router';
import Layout from './layout';
import BugPage from './pages/bug';
import ListPage from './pages/list';
+import NewBugPage from './pages/new/NewBugPage';
export default function App() {
return (
<Layout>
<Switch>
<Route path="/" exact component={ListPage} />
+ <Route path="/new" exact component={NewBugPage} />
<Route path="/bug/:id" exact component={BugPage} />
</Switch>
</Layout>
diff --git a/webui/src/components/BugTitleForm/BugTitleForm.tsx b/webui/src/components/BugTitleForm/BugTitleForm.tsx
new file mode 100644
index 00000000..16441c93
--- /dev/null
+++ b/webui/src/components/BugTitleForm/BugTitleForm.tsx
@@ -0,0 +1,197 @@
+import React, { useState } from 'react';
+
+import {
+ Button,
+ fade,
+ makeStyles,
+ TextField,
+ Typography,
+} from '@material-ui/core';
+
+import { TimelineDocument } from '../../pages/bug/TimelineQuery.generated';
+import Author from 'src/components/Author';
+import Date from 'src/components/Date';
+import { BugFragment } from 'src/pages/bug/Bug.generated';
+
+import { useSetTitleMutation } from './SetTitle.generated';
+
+/**
+ * Css in JS styles
+ */
+const useStyles = makeStyles((theme) => ({
+ header: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ headerTitle: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ readOnlyTitle: {
+ ...theme.typography.h5,
+ },
+ readOnlyId: {
+ ...theme.typography.subtitle1,
+ marginLeft: theme.spacing(1),
+ },
+ editButtonContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ alignItems: 'center',
+ minWidth: 200,
+ marginLeft: theme.spacing(2),
+ },
+ greenButton: {
+ marginLeft: '8px',
+ backgroundColor: '#2ea44fd9',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#2ea44f',
+ },
+ },
+ titleInput: {
+ borderRadius: theme.shape.borderRadius,
+ borderColor: fade(theme.palette.primary.main, 0.2),
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ backgroundColor: fade(theme.palette.primary.main, 0.05),
+ padding: theme.spacing(0, 0),
+ minWidth: 336,
+ transition: theme.transitions.create([
+ 'width',
+ 'borderColor',
+ 'backgroundColor',
+ ]),
+ },
+}));
+
+interface Props {
+ bug: BugFragment;
+}
+
+/**
+ * Component for bug title change
+ * @param bug Selected bug in list page
+ */
+function BugTitleForm({ bug }: Props) {
+ const [bugTitleEdition, setbugTitleEdition] = useState(false);
+ const [setTitle, { loading, error }] = useSetTitleMutation();
+ const [issueTitle, setIssueTitle] = useState(bug.title);
+ const classes = useStyles();
+ let issueTitleInput: any;
+
+ function isFormValid() {
+ if (issueTitleInput) {
+ return issueTitleInput.value.length > 0 ? true : false;
+ } else {
+ return false;
+ }
+ }
+
+ function submitNewTitle() {
+ if (!isFormValid()) return;
+ setTitle({
+ variables: {
+ input: {
+ prefix: bug.humanId,
+ title: issueTitleInput.value,
+ },
+ },
+ refetchQueries: [
+ // TODO: update the cache instead of refetching
+ {
+ query: TimelineDocument,
+ variables: {
+ id: bug.id,
+ first: 100,
+ },
+ },
+ ],
+ awaitRefetchQueries: true,
+ }).then(() => setbugTitleEdition(false));
+ }
+
+ function cancelChange() {
+ setIssueTitle(bug.title);
+ setbugTitleEdition(false);
+ }
+
+ function editableBugTitle() {
+ return (
+ <form className={classes.headerTitle} onSubmit={submitNewTitle}>
+ <TextField
+ inputRef={(node) => {
+ issueTitleInput = node;
+ }}
+ className={classes.titleInput}
+ variant="outlined"
+ fullWidth
+ margin="dense"
+ value={issueTitle}
+ onChange={(event: any) => setIssueTitle(event.target.value)}
+ />
+ <div className={classes.editButtonContainer}>
+ <Button
+ size="small"
+ variant="contained"
+ type="submit"
+ disabled={issueTitle.length === 0}
+ >
+ Save
+ </Button>
+ <Button size="small" onClick={() => cancelChange()}>
+ Cancel
+ </Button>
+ </div>
+ </form>
+ );
+ }
+
+ function readonlyBugTitle() {
+ return (
+ <div className={classes.headerTitle}>
+ <div>
+ <span className={classes.readOnlyTitle}>{bug.title}</span>
+ <span className={classes.readOnlyId}>{bug.humanId}</span>
+ </div>
+ <div className={classes.editButtonContainer}>
+ <Button
+ size="small"
+ variant="contained"
+ onClick={() => setbugTitleEdition(!bugTitleEdition)}
+ >
+ Edit
+ </Button>
+ <Button
+ className={classes.greenButton}
+ size="small"
+ variant="contained"
+ href="/new"
+ >
+ New issue
+ </Button>
+ </div>
+ </div>
+ );
+ }
+
+ if (loading) return <div>Loading...</div>;
+ if (error) return <div>Error</div>;
+
+ return (
+ <div className={classes.header}>
+ {bugTitleEdition ? editableBugTitle() : readonlyBugTitle()}
+ <div className="classes.headerSubtitle">
+ <Typography color={'textSecondary'}>
+ <Author author={bug.author} />
+ {' opened this bug '}
+ <Date date={bug.createdAt} />
+ </Typography>
+ </div>
+ </div>
+ );
+}
+
+export default BugTitleForm;
diff --git a/webui/src/components/BugTitleForm/SetTitle.graphql b/webui/src/components/BugTitleForm/SetTitle.graphql
new file mode 100644
index 00000000..b96af155
--- /dev/null
+++ b/webui/src/components/BugTitleForm/SetTitle.graphql
@@ -0,0 +1,7 @@
+mutation setTitle($input: SetTitleInput!) {
+ setTitle(input: $input) {
+ bug {
+ id
+ }
+ }
+} \ No newline at end of file
diff --git a/webui/src/components/CloseBugButton/CloseBug.graphql b/webui/src/components/CloseBugButton/CloseBug.graphql
new file mode 100644
index 00000000..e2f4bff2
--- /dev/null
+++ b/webui/src/components/CloseBugButton/CloseBug.graphql
@@ -0,0 +1,8 @@
+# Write your query or mutation here
+mutation closeBug($input: CloseBugInput!) {
+ closeBug(input: $input) {
+ bug {
+ id
+ }
+ }
+} \ No newline at end of file
diff --git a/webui/src/components/CloseBugButton/CloseBugButton.tsx b/webui/src/components/CloseBugButton/CloseBugButton.tsx
new file mode 100644
index 00000000..19f56cab
--- /dev/null
+++ b/webui/src/components/CloseBugButton/CloseBugButton.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import Button from '@material-ui/core/Button';
+
+import { BugFragment } from 'src/pages/bug/Bug.generated';
+import { TimelineDocument } from 'src/pages/bug/TimelineQuery.generated';
+
+import { useCloseBugMutation } from './CloseBug.generated';
+
+interface Props {
+ bug: BugFragment;
+ disabled: boolean;
+}
+
+function CloseBugButton({ bug, disabled }: Props) {
+ const [closeBug, { loading, error }] = useCloseBugMutation();
+
+ function closeBugAction() {
+ closeBug({
+ variables: {
+ input: {
+ prefix: bug.id,
+ },
+ },
+ refetchQueries: [
+ // TODO: update the cache instead of refetching
+ {
+ query: TimelineDocument,
+ variables: {
+ id: bug.id,
+ first: 100,
+ },
+ },
+ ],
+ awaitRefetchQueries: true,
+ });
+ }
+
+ if (loading) return <div>Loading...</div>;
+ if (error) return <div>Error</div>;
+
+ return (
+ <div>
+ <Button
+ variant="contained"
+ onClick={() => closeBugAction()}
+ disabled={bug.status === 'CLOSED' || disabled}
+ >
+ Close issue
+ </Button>
+ </div>
+ );
+}
+
+export default CloseBugButton;
diff --git a/webui/src/components/ReopenBugButton/OpenBug.graphql b/webui/src/components/ReopenBugButton/OpenBug.graphql
new file mode 100644
index 00000000..cf9e49e5
--- /dev/null
+++ b/webui/src/components/ReopenBugButton/OpenBug.graphql
@@ -0,0 +1,7 @@
+mutation openBug($input: OpenBugInput!) {
+ openBug(input: $input) {
+ bug {
+ id
+ }
+ }
+} \ No newline at end of file
diff --git a/webui/src/components/ReopenBugButton/ReopenBugButton.tsx b/webui/src/components/ReopenBugButton/ReopenBugButton.tsx
new file mode 100644
index 00000000..195ca512
--- /dev/null
+++ b/webui/src/components/ReopenBugButton/ReopenBugButton.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import Button from '@material-ui/core/Button';
+
+import { BugFragment } from 'src/pages/bug/Bug.generated';
+import { TimelineDocument } from 'src/pages/bug/TimelineQuery.generated';
+
+import { useOpenBugMutation } from './OpenBug.generated';
+
+interface Props {
+ bug: BugFragment;
+ disabled: boolean;
+}
+
+function ReopenBugButton({ bug, disabled }: Props) {
+ const [openBug, { loading, error }] = useOpenBugMutation();
+
+ function openBugAction() {
+ openBug({
+ variables: {
+ input: {
+ prefix: bug.id,
+ },
+ },
+ refetchQueries: [
+ // TODO: update the cache instead of refetching
+ {
+ query: TimelineDocument,
+ variables: {
+ id: bug.id,
+ first: 100,
+ },
+ },
+ ],
+ awaitRefetchQueries: true,
+ });
+ }
+
+ if (loading) return <div>Loading...</div>;
+ if (error) return <div>Error</div>;
+
+ return (
+ <div>
+ <Button
+ variant="contained"
+ onClick={() => openBugAction()}
+ disabled={bug.status === 'OPEN' || disabled}
+ >
+ Reopen issue
+ </Button>
+ </div>
+ );
+}
+
+export default ReopenBugButton;
diff --git a/webui/src/layout/CommentInput/CommentInput.tsx b/webui/src/layout/CommentInput/CommentInput.tsx
new file mode 100644
index 00000000..86cc7dbb
--- /dev/null
+++ b/webui/src/layout/CommentInput/CommentInput.tsx
@@ -0,0 +1,107 @@
+import React, { useState, useEffect } from 'react';
+
+import Tab from '@material-ui/core/Tab';
+import Tabs from '@material-ui/core/Tabs';
+import TextField from '@material-ui/core/TextField';
+import { makeStyles } from '@material-ui/core/styles';
+
+import Content from 'src/components/Content';
+
+/**
+ * Styles
+ */
+const useStyles = makeStyles((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',
+ },
+}));
+
+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 = {
+ inputProps?: any;
+ loading: boolean;
+ onChange: (comment: string) => void;
+};
+
+/**
+ * Component for issue comment input
+ *
+ * @param inputProps Reset input value
+ * @param loading Disable input when component not ready yet
+ * @param onChange Callback to return input value changes
+ */
+function CommentInput({ inputProps, loading, onChange }: Props) {
+ const [input, setInput] = useState<string>('');
+ const [tab, setTab] = useState(0);
+ const classes = useStyles();
+
+ useEffect(() => {
+ if (inputProps) setInput(inputProps.value);
+ }, [inputProps]);
+
+ useEffect(() => {
+ onChange(input);
+ }, [input, onChange]);
+
+ return (
+ <div>
+ <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
+ 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>
+ );
+}
+
+export default CommentInput;
diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx
index 8d6d11cc..bd6e44c4 100644
--- a/webui/src/pages/bug/Bug.tsx
+++ b/webui/src/pages/bug/Bug.tsx
@@ -1,10 +1,8 @@
import React from 'react';
-import Typography from '@material-ui/core/Typography/Typography';
import { makeStyles } from '@material-ui/core/styles';
-import Author from 'src/components/Author';
-import Date from 'src/components/Date';
+import BugTitleForm from 'src/components/BugTitleForm/BugTitleForm';
import Label from 'src/components/Label';
import IfLoggedIn from 'src/layout/IfLoggedIn';
@@ -12,21 +10,19 @@ import { BugFragment } from './Bug.generated';
import CommentForm from './CommentForm';
import TimelineQuery from './TimelineQuery';
+/**
+ * Css in JS Styles
+ */
const useStyles = makeStyles((theme) => ({
main: {
maxWidth: 1000,
margin: 'auto',
marginTop: theme.spacing(4),
+ overflow: 'hidden',
},
header: {
marginLeft: theme.spacing(3) + 40,
- },
- title: {
- ...theme.typography.h5,
- },
- id: {
- ...theme.typography.subtitle1,
- marginLeft: theme.spacing(1),
+ marginRight: theme.spacing(2),
},
container: {
display: 'flex',
@@ -73,17 +69,11 @@ type Props = {
function Bug({ bug }: Props) {
const classes = useStyles();
+
return (
<main className={classes.main}>
<div className={classes.header}>
- <span className={classes.title}>{bug.title}</span>
- <span className={classes.id}>{bug.humanId}</span>
-
- <Typography color={'textSecondary'}>
- <Author author={bug.author} />
- {' opened this bug '}
- <Date date={bug.createdAt} />
- </Typography>
+ <BugTitleForm bug={bug} />
</div>
<div className={classes.container}>
@@ -92,7 +82,7 @@ function Bug({ bug }: Props) {
<IfLoggedIn>
{() => (
<div className={classes.commentForm}>
- <CommentForm bugId={bug.id} />
+ <CommentForm bug={bug} />
</div>
)}
</IfLoggedIn>
diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx
index f2a2eb6c..c623dabb 100644
--- a/webui/src/pages/bug/CommentForm.tsx
+++ b/webui/src/pages/bug/CommentForm.tsx
@@ -2,13 +2,13 @@ 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 CommentInput from '../../layout/CommentInput/CommentInput';
+import CloseBugButton from 'src/components/CloseBugButton/CloseBugButton';
+import ReopenBugButton from 'src/components/ReopenBugButton/ReopenBugButton';
+import { BugFragment } from './Bug.generated';
import { useAddCommentMutation } from './CommentForm.generated';
import { TimelineDocument } from './TimelineQuery.generated';
@@ -30,40 +30,24 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
display: 'flex',
justifyContent: 'flex-end',
},
+ greenButton: {
+ marginLeft: '8px',
+ backgroundColor: '#2ea44fd9',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#2ea44f',
+ },
+ },
}));
-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;
+ bug: BugFragment;
};
-function CommentForm({ bugId }: Props) {
+function CommentForm({ bug }: Props) {
const [addComment, { loading }] = useAddCommentMutation();
- const [input, setInput] = useState<string>('');
- const [tab, setTab] = useState(0);
+ const [issueComment, setIssueComment] = useState('');
+ const [inputProp, setInputProp] = useState<any>('');
const classes = useStyles({ loading });
const form = useRef<HTMLFormElement>(null);
@@ -71,8 +55,8 @@ function CommentForm({ bugId }: Props) {
addComment({
variables: {
input: {
- prefix: bugId,
- message: input,
+ prefix: bug.id,
+ message: issueComment,
},
},
refetchQueries: [
@@ -80,60 +64,50 @@ function CommentForm({ bugId }: Props) {
{
query: TimelineDocument,
variables: {
- id: bugId,
+ id: bug.id,
first: 100,
},
},
],
awaitRefetchQueries: true,
- }).then(() => setInput(''));
+ }).then(() => resetForm());
};
+ function resetForm() {
+ setInputProp({
+ value: '',
+ });
+ }
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
- submit();
+ if (issueComment.length > 0) submit();
};
- const handleKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
- // Submit on cmd/ctrl+enter
- if ((e.metaKey || e.altKey) && e.keyCode === 13) {
- submit();
- }
- };
+ function getCloseButton() {
+ return <CloseBugButton bug={bug} disabled={issueComment.length > 0} />;
+ }
+
+ function getReopenButton() {
+ return <ReopenBugButton bug={bug} disabled={issueComment.length > 0} />;
+ }
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>
+ <CommentInput
+ inputProps={inputProp}
+ loading={loading}
+ onChange={(comment: string) => setIssueComment(comment)}
+ />
<div className={classes.actions}>
+ {bug.status === 'OPEN' ? getCloseButton() : getReopenButton()}
<Button
+ className={classes.greenButton}
variant="contained"
color="primary"
type="submit"
- disabled={loading}
+ disabled={loading || issueComment.length === 0}
>
Comment
</Button>
diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx
index 7eb6f4c5..424ffac0 100644
--- a/webui/src/pages/list/ListQuery.tsx
+++ b/webui/src/pages/list/ListQuery.tsx
@@ -2,6 +2,7 @@ import { ApolloError } from '@apollo/client';
import React, { useState, useEffect, useRef } from 'react';
import { useLocation, useHistory, Link } from 'react-router-dom';
+import { Button } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import InputBase from '@material-ui/core/InputBase';
import Paper from '@material-ui/core/Paper';
@@ -40,6 +41,17 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
alignItems: 'center',
justifyContent: 'space-between',
},
+ filterissueLabel: {
+ fontSize: '14px',
+ fontWeight: 'bold',
+ paddingRight: '12px',
+ },
+ filterissueContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ justifyContents: 'left',
+ },
search: {
borderRadius: theme.shape.borderRadius,
borderColor: fade(theme.palette.primary.main, 0.2),
@@ -95,6 +107,13 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({
padding: theme.spacing(2, 3),
},
},
+ greenButton: {
+ backgroundColor: '#2ea44fd9',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#2ea44f',
+ },
+ },
}));
function editParams(
@@ -271,21 +290,29 @@ function ListQuery() {
return (
<Paper className={classes.main}>
<header className={classes.header}>
- <h1>Issues</h1>
- <form onSubmit={formSubmit}>
- <InputBase
- placeholder="Filter"
- value={input}
- onInput={(e: any) => setInput(e.target.value)}
- classes={{
- root: classes.search,
- focused: classes.searchFocused,
- }}
- />
- <button type="submit" hidden>
- Search
- </button>
- </form>
+ <div className="filterissueContainer">
+ <form onSubmit={formSubmit}>
+ <label className={classes.filterissueLabel} htmlFor="issuefilter">
+ Filter
+ </label>
+ <InputBase
+ id="issuefilter"
+ placeholder="Filter"
+ value={input}
+ onInput={(e: any) => setInput(e.target.value)}
+ classes={{
+ root: classes.search,
+ focused: classes.searchFocused,
+ }}
+ />
+ <button type="submit" hidden>
+ Search
+ </button>
+ </form>
+ </div>
+ <Button className={classes.greenButton} variant="contained" href="/new">
+ New issue
+ </Button>
</header>
<FilterToolbar query={query} queryLocation={queryLocation} />
{content}
diff --git a/webui/src/pages/new/NewBug.graphql b/webui/src/pages/new/NewBug.graphql
new file mode 100644
index 00000000..92df016e
--- /dev/null
+++ b/webui/src/pages/new/NewBug.graphql
@@ -0,0 +1,7 @@
+mutation newBug($input: NewBugInput!) {
+ newBug(input: $input) {
+ bug {
+ humanId
+ }
+ }
+} \ No newline at end of file
diff --git a/webui/src/pages/new/NewBugPage.tsx b/webui/src/pages/new/NewBugPage.tsx
new file mode 100644
index 00000000..c70cddaa
--- /dev/null
+++ b/webui/src/pages/new/NewBugPage.tsx
@@ -0,0 +1,118 @@
+import React, { FormEvent, useState } from 'react';
+
+import { Button } from '@material-ui/core';
+import Paper from '@material-ui/core/Paper';
+import TextField from '@material-ui/core/TextField/TextField';
+import { fade, makeStyles, Theme } from '@material-ui/core/styles';
+
+import CommentInput from '../../layout/CommentInput/CommentInput';
+
+import { useNewBugMutation } from './NewBug.generated';
+
+/**
+ * Css in JS styles
+ */
+const useStyles = makeStyles((theme: Theme) => ({
+ main: {
+ maxWidth: 800,
+ margin: 'auto',
+ marginTop: theme.spacing(4),
+ marginBottom: theme.spacing(4),
+ padding: theme.spacing(2),
+ overflow: 'hidden',
+ },
+ titleInput: {
+ borderRadius: theme.shape.borderRadius,
+ borderColor: fade(theme.palette.primary.main, 0.2),
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ backgroundColor: fade(theme.palette.primary.main, 0.05),
+ padding: theme.spacing(0, 0),
+ transition: theme.transitions.create([
+ 'width',
+ 'borderColor',
+ 'backgroundColor',
+ ]),
+ },
+ form: {
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ actions: {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ },
+ greenButton: {
+ backgroundColor: '#2ea44fd9',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#2ea44f',
+ },
+ },
+}));
+
+/**
+ * Form to create a new issue
+ */
+function NewBugPage() {
+ const [newBug, { loading, error }] = useNewBugMutation();
+ const [issueTitle, setIssueTitle] = useState('');
+ const [issueComment, setIssueComment] = useState('');
+ const classes = useStyles();
+ let issueTitleInput: any;
+
+ function submitNewIssue(e: FormEvent) {
+ e.preventDefault();
+ if (!isFormValid()) return;
+ newBug({
+ variables: {
+ input: {
+ title: issueTitle,
+ message: issueComment,
+ },
+ },
+ });
+ issueTitleInput.value = '';
+ }
+
+ function isFormValid() {
+ return issueTitle.length > 0 && issueComment.length > 0 ? true : false;
+ }
+
+ if (loading) return <div>Loading...</div>;
+ if (error) return <div>Error</div>;
+
+ return (
+ <Paper className={classes.main}>
+ <form className={classes.form} onSubmit={submitNewIssue}>
+ <TextField
+ inputRef={(node) => {
+ issueTitleInput = node;
+ }}
+ label="Title"
+ className={classes.titleInput}
+ variant="outlined"
+ fullWidth
+ margin="dense"
+ onChange={(event: any) => setIssueTitle(event.target.value)}
+ />
+ <CommentInput
+ loading={false}
+ onChange={(comment: string) => setIssueComment(comment)}
+ />
+ <div className={classes.actions}>
+ <Button
+ className={classes.greenButton}
+ variant="contained"
+ type="submit"
+ disabled={isFormValid() ? false : true}
+ >
+ Submit new issue
+ </Button>
+ </div>
+ </form>
+ </Paper>
+ );
+}
+
+export default NewBugPage;