diff options
Diffstat (limited to 'webui/src')
21 files changed, 677 insertions, 109 deletions
diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 16663870..b9ade974 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,15 +1,17 @@ import React from 'react'; import { Route, Switch } from 'react-router'; -import Layout from './layout'; +import Layout from './components/Header'; 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/Author.tsx b/webui/src/components/Author.tsx index 9ac1da52..d60e8969 100644 --- a/webui/src/components/Author.tsx +++ b/webui/src/components/Author.tsx @@ -3,7 +3,7 @@ import React from 'react'; import MAvatar from '@material-ui/core/Avatar'; import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import { AuthoredFragment } from './fragments.generated'; +import { AuthoredFragment } from '../graphql/fragments.generated'; type Props = AuthoredFragment & { className?: string; diff --git a/webui/src/components/BugTitleForm/BugTitleForm.tsx b/webui/src/components/BugTitleForm/BugTitleForm.tsx new file mode 100644 index 00000000..c47eab31 --- /dev/null +++ b/webui/src/components/BugTitleForm/BugTitleForm.tsx @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; + +import { + Button, + fade, + makeStyles, + TextField, + Typography, +} from '@material-ui/core'; + +import { TimelineDocument } from '../../pages/bug/TimelineQuery.generated'; +import IfLoggedIn from '../IfLoggedIn/IfLoggedIn'; +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> + <IfLoggedIn> + {() => ( + <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> + )} + </IfLoggedIn> + </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/CommentInput/CommentInput.tsx b/webui/src/components/CommentInput/CommentInput.tsx new file mode 100644 index 00000000..86cc7dbb --- /dev/null +++ b/webui/src/components/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/layout/CurrentIdentity.graphql b/webui/src/components/CurrentIdentity/CurrentIdentity.graphql index 2794a40f..2794a40f 100644 --- a/webui/src/layout/CurrentIdentity.graphql +++ b/webui/src/components/CurrentIdentity/CurrentIdentity.graphql diff --git a/webui/src/layout/CurrentIdentity.tsx b/webui/src/components/CurrentIdentity/CurrentIdentity.tsx index 8cd3585b..8cd3585b 100644 --- a/webui/src/layout/CurrentIdentity.tsx +++ b/webui/src/components/CurrentIdentity/CurrentIdentity.tsx diff --git a/webui/src/layout/Header.tsx b/webui/src/components/Header/Header.tsx index b0fae3cc..3e39b5f3 100644 --- a/webui/src/layout/Header.tsx +++ b/webui/src/components/Header/Header.tsx @@ -5,7 +5,7 @@ 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'; +import CurrentIdentity from '../CurrentIdentity/CurrentIdentity'; const useStyles = makeStyles((theme) => ({ offset: { diff --git a/webui/src/layout/index.tsx b/webui/src/components/Header/index.tsx index 42a0cfc1..42a0cfc1 100644 --- a/webui/src/layout/index.tsx +++ b/webui/src/components/Header/index.tsx diff --git a/webui/src/layout/IfLoggedIn.tsx b/webui/src/components/IfLoggedIn/IfLoggedIn.tsx index 9f4a7576..2476aad8 100644 --- a/webui/src/layout/IfLoggedIn.tsx +++ b/webui/src/components/IfLoggedIn/IfLoggedIn.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; +import { useCurrentIdentityQuery } from '../CurrentIdentity/CurrentIdentity.generated'; type Props = { children: () => React.ReactNode }; const IfLoggedIn = ({ children }: Props) => { diff --git a/webui/src/components/Label.tsx b/webui/src/components/Label.tsx index 4aaa6bb6..111f6d7f 100644 --- a/webui/src/components/Label.tsx +++ b/webui/src/components/Label.tsx @@ -7,10 +7,9 @@ import { darken, } from '@material-ui/core/styles/colorManipulator'; +import { LabelFragment } from '../graphql/fragments.generated'; 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/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/components/fragments.graphql b/webui/src/graphql/fragments.graphql index 03a235f9..03a235f9 100644 --- a/webui/src/components/fragments.graphql +++ b/webui/src/graphql/fragments.graphql diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 8d6d11cc..d85c5296 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -1,32 +1,28 @@ 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 IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; import Label from 'src/components/Label'; -import IfLoggedIn from 'src/layout/IfLoggedIn'; 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..0b97e133 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 '../../components/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..87c21e3c 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'; @@ -11,6 +12,8 @@ import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; import Skeleton from '@material-ui/lab/Skeleton'; +import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn'; + import FilterToolbar from './FilterToolbar'; import List from './List'; import { useListBugsQuery } from './ListQuery.generated'; @@ -40,6 +43,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 +109,13 @@ const useStyles = makeStyles<Theme, StylesProps>((theme) => ({ padding: theme.spacing(2, 3), }, }, + greenButton: { + backgroundColor: '#2ea44fd9', + color: '#fff', + '&:hover': { + backgroundColor: '#2ea44f', + }, + }, })); function editParams( @@ -271,21 +292,37 @@ 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> + <IfLoggedIn> + {() => ( + <Button + className={classes.greenButton} + variant="contained" + href="/new" + > + New issue + </Button> + )} + </IfLoggedIn> </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..c9e268b6 --- /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 '../../components/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; |