From 680dd91c0c0200bd4948173df0b601e16f511e6e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 13 Feb 2020 00:19:22 +0100 Subject: webui: create comment form --- webui/package-lock.json | 5 ++ webui/package.json | 1 + webui/src/Date.tsx | 7 +- webui/src/bug/Bug.tsx | 3 + webui/src/bug/CommentForm.graphql | 5 ++ webui/src/bug/CommentForm.tsx | 145 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 webui/src/bug/CommentForm.graphql create mode 100644 webui/src/bug/CommentForm.tsx (limited to 'webui') 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..a6fd4a58 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", diff --git a/webui/src/Date.tsx b/webui/src/Date.tsx index 9380d2fc..a830546c 100644 --- a/webui/src/Date.tsx +++ b/webui/src/Date.tsx @@ -1,11 +1,16 @@ import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import moment from 'moment'; import React from 'react'; +import Moment from 'react-moment'; + +const HOUR = 1000 * 3600; +const DAY = 24 * HOUR; +const WEEK = 7 * DAY; type Props = { date: string }; const Date = ({ date }: Props) => ( - {moment(date).fromNow()} + ); diff --git a/webui/src/bug/Bug.tsx b/webui/src/bug/Bug.tsx index f4029a5f..114cb8e0 100644 --- a/webui/src/bug/Bug.tsx +++ b/webui/src/bug/Bug.tsx @@ -7,6 +7,7 @@ import Date from '../Date'; import Label from '../Label'; import { BugFragment } from './Bug.generated'; +import CommentForm from './CommentForm'; import TimelineQuery from './TimelineQuery'; const useStyles = makeStyles(theme => ({ @@ -87,6 +88,8 @@ function Bug({ bug }: Props) { + + ); } diff --git a/webui/src/bug/CommentForm.graphql b/webui/src/bug/CommentForm.graphql new file mode 100644 index 00000000..33d21193 --- /dev/null +++ b/webui/src/bug/CommentForm.graphql @@ -0,0 +1,5 @@ +mutation AddComment($input: AddCommentInput!) { + addComment(input: $input) { + operation { id } + } +} diff --git a/webui/src/bug/CommentForm.tsx b/webui/src/bug/CommentForm.tsx new file mode 100644 index 00000000..3aa52b19 --- /dev/null +++ b/webui/src/bug/CommentForm.tsx @@ -0,0 +1,145 @@ +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 React, { useState, useRef } from 'react'; + +import Content from '../Content'; + +import { useAddCommentMutation } from './CommentForm.generated'; +import { TimelineDocument } from './TimelineQuery.generated'; + +type StyleProps = { loading: boolean }; +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', + }, + actions: { + display: 'flex', + justifyContent: 'flex-end', + }, +})); + +type TabPanelProps = { + children: React.ReactNode; + value: number; + index: number; +} & React.HTMLProps; +function TabPanel({ children, value, index, ...props }: TabPanelProps) { + return ( + + ); +} + +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(''); + const [tab, setTab] = useState(0); + const classes = useStyles({ loading }); + const form = useRef(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) => { + e.preventDefault(); + submit(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Submit on cmd/ctrl+enter + if ((e.metaKey || e.altKey) && e.keyCode === 13) { + submit(); + } + }; + + return ( + +
+ setTab(t)}> + + + +
+ + setInput(e.target.value)} + disabled={loading} + /> + + + + +
+
+ +
+
+
+ ); +} + +export default CommentForm; -- cgit From 8b85780d76ad45675582f4478eedb026b7ac25e1 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 13 Feb 2020 00:53:29 +0100 Subject: webui: start reorganizing the component structure --- webui/package.json | 3 +- webui/src/Author.graphql | 8 ---- webui/src/Author.tsx | 32 ---------------- webui/src/Content.tsx | 26 ------------- webui/src/Date.tsx | 17 --------- webui/src/Label.graphql | 8 ---- webui/src/Label.tsx | 55 ---------------------------- webui/src/bug/Bug.graphql | 3 +- webui/src/bug/Bug.tsx | 6 +-- webui/src/bug/CommentForm.tsx | 2 +- webui/src/bug/LabelChange.tsx | 6 +-- webui/src/bug/LabelChangeFragment.graphql | 3 +- webui/src/bug/Message.tsx | 7 ++-- webui/src/bug/MessageCommentFragment.graphql | 2 +- webui/src/bug/MessageCreateFragment.graphql | 2 +- webui/src/bug/SetStatus.tsx | 4 +- webui/src/bug/SetStatusFragment.graphql | 2 +- webui/src/bug/SetTitle.tsx | 4 +- webui/src/bug/SetTitleFragment.graphql | 2 +- webui/src/components/Author.tsx | 32 ++++++++++++++++ webui/src/components/Content/ImageTag.tsx | 22 +++++++++++ webui/src/components/Content/PreTag.tsx | 16 ++++++++ webui/src/components/Content/index.tsx | 26 +++++++++++++ webui/src/components/Date.tsx | 17 +++++++++ webui/src/components/Label.tsx | 55 ++++++++++++++++++++++++++++ webui/src/components/fragments.graphql | 19 ++++++++++ webui/src/list/BugRow.graphql | 3 +- webui/src/list/BugRow.tsx | 4 +- webui/src/tag/ImageTag.tsx | 22 ----------- webui/src/tag/PreTag.tsx | 16 -------- 30 files changed, 212 insertions(+), 212 deletions(-) delete mode 100644 webui/src/Author.graphql delete mode 100644 webui/src/Author.tsx delete mode 100644 webui/src/Content.tsx delete mode 100644 webui/src/Date.tsx delete mode 100644 webui/src/Label.graphql delete mode 100644 webui/src/Label.tsx create mode 100644 webui/src/components/Author.tsx create mode 100644 webui/src/components/Content/ImageTag.tsx create mode 100644 webui/src/components/Content/PreTag.tsx create mode 100644 webui/src/components/Content/index.tsx create mode 100644 webui/src/components/Date.tsx create mode 100644 webui/src/components/Label.tsx create mode 100644 webui/src/components/fragments.graphql delete mode 100644 webui/src/tag/ImageTag.tsx delete mode 100644 webui/src/tag/PreTag.tsx (limited to 'webui') diff --git a/webui/package.json b/webui/package.json index a6fd4a58..902ed1db 100644 --- a/webui/package.json +++ b/webui/package.json @@ -49,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/Author.graphql b/webui/src/Author.graphql deleted file mode 100644 index 76d66b91..00000000 --- a/webui/src/Author.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment authored on Authored { - author { - name - email - displayName - avatarUrl - } -} diff --git a/webui/src/Author.tsx b/webui/src/Author.tsx deleted file mode 100644 index 852cd2b7..00000000 --- a/webui/src/Author.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import MAvatar from '@material-ui/core/Avatar'; -import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import React from 'react'; - -import { AuthoredFragment } from './Author.generated'; - -type Props = AuthoredFragment & { - className?: string; - bold?: boolean; -}; - -const Author = ({ author, ...props }: Props) => { - if (!author.email) { - return {author.displayName}; - } - - return ( - - {author.displayName} - - ); -}; - -export const Avatar = ({ author, ...props }: Props) => { - if (author.avatarUrl) { - return ; - } - - return {author.displayName[0]}; -}; - -export default Author; diff --git a/webui/src/Content.tsx b/webui/src/Content.tsx deleted file mode 100644 index 3a7af2f8..00000000 --- a/webui/src/Content.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import html from 'remark-html'; -import parse from 'remark-parse'; -import remark2react from 'remark-react'; -import unified from 'unified'; - -import ImageTag from './tag/ImageTag'; -import PreTag from './tag/PreTag'; - -type Props = { markdown: string }; -const Content: React.FC = ({ markdown }: Props) => { - const processor = unified() - .use(parse) - .use(html) - .use(remark2react, { - remarkReactComponents: { - img: ImageTag, - pre: PreTag, - }, - }); - - const contents: React.ReactNode = processor.processSync(markdown).contents; - return <>{contents}; -}; - -export default Content; diff --git a/webui/src/Date.tsx b/webui/src/Date.tsx deleted file mode 100644 index a830546c..00000000 --- a/webui/src/Date.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Tooltip from '@material-ui/core/Tooltip/Tooltip'; -import moment from 'moment'; -import React from 'react'; -import Moment from 'react-moment'; - -const HOUR = 1000 * 3600; -const DAY = 24 * HOUR; -const WEEK = 7 * DAY; - -type Props = { date: string }; -const Date = ({ date }: Props) => ( - - - -); - -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/Label.tsx b/webui/src/Label.tsx deleted file mode 100644 index a33b4c2c..00000000 --- a/webui/src/Label.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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'; - -// Minimum contrast between the background and the text color -const contrastThreshold = 2.5; - -// Guess the text color based on the background color -const getTextColor = (background: string) => - getContrastRatio(background, common.white) >= contrastThreshold - ? common.white // White on dark backgrounds - : common.black; // And black on light ones - -const _rgb = (color: Color) => - 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; - -// Create a style object from the label RGB colors -const createStyle = (color: Color) => ({ - backgroundColor: _rgb(color), - color: getTextColor(_rgb(color)), - borderBottomColor: darken(_rgb(color), 0.2), -}); - -const useStyles = makeStyles(theme => ({ - label: { - ...theme.typography.body1, - padding: '1px 6px 0.5px', - fontSize: '0.9em', - fontWeight: 500, - margin: '0.05em 1px calc(-1.5px + 0.05em)', - borderRadius: '3px', - display: 'inline-block', - borderBottom: 'solid 1.5px', - verticalAlign: 'bottom', - }, -})); - -type Props = { label: LabelFragment }; -function Label({ label }: Props) { - const classes = useStyles(); - return ( - - {label.name} - - ); -} - -export default Label; diff --git a/webui/src/bug/Bug.graphql b/webui/src/bug/Bug.graphql index 112024aa..498242c0 100644 --- a/webui/src/bug/Bug.graphql +++ b/webui/src/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/bug/Bug.tsx index 114cb8e0..0e53e447 100644 --- a/webui/src/bug/Bug.tsx +++ b/webui/src/bug/Bug.tsx @@ -2,9 +2,9 @@ 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 '../components/Author'; +import Date from '../components/Date'; +import Label from '../components/Label'; import { BugFragment } from './Bug.generated'; import CommentForm from './CommentForm'; diff --git a/webui/src/bug/CommentForm.tsx b/webui/src/bug/CommentForm.tsx index 3aa52b19..a915ecf0 100644 --- a/webui/src/bug/CommentForm.tsx +++ b/webui/src/bug/CommentForm.tsx @@ -6,7 +6,7 @@ import TextField from '@material-ui/core/TextField'; import { makeStyles, Theme } from '@material-ui/core/styles'; import React, { useState, useRef } from 'react'; -import Content from '../Content'; +import Content from '../components/Content'; import { useAddCommentMutation } from './CommentForm.generated'; import { TimelineDocument } from './TimelineQuery.generated'; diff --git a/webui/src/bug/LabelChange.tsx b/webui/src/bug/LabelChange.tsx index 572579bd..a3950524 100644 --- a/webui/src/bug/LabelChange.tsx +++ b/webui/src/bug/LabelChange.tsx @@ -1,9 +1,9 @@ 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 '../components/Author'; +import Date from '../components/Date'; +import Label from '../components/Label'; import { LabelChangeFragment } from './LabelChangeFragment.generated'; diff --git a/webui/src/bug/LabelChangeFragment.graphql b/webui/src/bug/LabelChangeFragment.graphql index 631de70c..01b94a98 100644 --- a/webui/src/bug/LabelChangeFragment.graphql +++ b/webui/src/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/bug/Message.tsx index c8d0710d..a61ed3f2 100644 --- a/webui/src/bug/Message.tsx +++ b/webui/src/bug/Message.tsx @@ -2,10 +2,9 @@ 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 '../components/Author'; +import Date from '../components/Date'; +import Content from '../components/Content'; import { AddCommentFragment } from './MessageCommentFragment.generated'; import { CreateFragment } from './MessageCreateFragment.generated'; diff --git a/webui/src/bug/MessageCommentFragment.graphql b/webui/src/bug/MessageCommentFragment.graphql index 38d626d0..61156fee 100644 --- a/webui/src/bug/MessageCommentFragment.graphql +++ b/webui/src/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/bug/MessageCreateFragment.graphql index 08477470..e371b9dc 100644 --- a/webui/src/bug/MessageCreateFragment.graphql +++ b/webui/src/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/bug/SetStatus.tsx index 3e1a7989..86105c8a 100644 --- a/webui/src/bug/SetStatus.tsx +++ b/webui/src/bug/SetStatus.tsx @@ -1,8 +1,8 @@ import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import Author from '../Author'; -import Date from '../Date'; +import Author from '../components/Author'; +import Date from '../components/Date'; import { SetStatusFragment } from './SetStatusFragment.generated'; diff --git a/webui/src/bug/SetStatusFragment.graphql b/webui/src/bug/SetStatusFragment.graphql index 0fdea01b..5a3986d0 100644 --- a/webui/src/bug/SetStatusFragment.graphql +++ b/webui/src/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/bug/SetTitle.tsx index 0b088e0b..e57aaafb 100644 --- a/webui/src/bug/SetTitle.tsx +++ b/webui/src/bug/SetTitle.tsx @@ -1,8 +1,8 @@ import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import Author from '../Author'; -import Date from '../Date'; +import Author from '../components/Author'; +import Date from '../components/Date'; import { SetTitleFragment } from './SetTitleFragment.generated'; diff --git a/webui/src/bug/SetTitleFragment.graphql b/webui/src/bug/SetTitleFragment.graphql index 432c4449..22d2185c 100644 --- a/webui/src/bug/SetTitleFragment.graphql +++ b/webui/src/bug/SetTitleFragment.graphql @@ -1,4 +1,4 @@ -#import "../Author.graphql" +#import "../components/fragments.graphql" fragment SetTitle on SetTitleTimelineItem { date diff --git a/webui/src/components/Author.tsx b/webui/src/components/Author.tsx new file mode 100644 index 00000000..43fd108e --- /dev/null +++ b/webui/src/components/Author.tsx @@ -0,0 +1,32 @@ +import MAvatar from '@material-ui/core/Avatar'; +import Tooltip from '@material-ui/core/Tooltip/Tooltip'; +import React from 'react'; + +import { AuthoredFragment } from './fragments.generated'; + +type Props = AuthoredFragment & { + className?: string; + bold?: boolean; +}; + +const Author = ({ author, ...props }: Props) => { + if (!author.email) { + return {author.displayName}; + } + + return ( + + {author.displayName} + + ); +}; + +export const Avatar = ({ author, ...props }: Props) => { + if (author.avatarUrl) { + return ; + } + + return {author.displayName[0]}; +}; + +export default Author; diff --git a/webui/src/components/Content/ImageTag.tsx b/webui/src/components/Content/ImageTag.tsx new file mode 100644 index 00000000..bdb36873 --- /dev/null +++ b/webui/src/components/Content/ImageTag.tsx @@ -0,0 +1,22 @@ +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +const useStyles = makeStyles({ + tag: { + maxWidth: '100%', + }, +}); + +const ImageTag = ({ + alt, + ...props +}: React.ImgHTMLAttributes) => { + const classes = useStyles(); + return ( + + {alt} + + ); +}; + +export default ImageTag; diff --git a/webui/src/components/Content/PreTag.tsx b/webui/src/components/Content/PreTag.tsx new file mode 100644 index 00000000..d3b4c273 --- /dev/null +++ b/webui/src/components/Content/PreTag.tsx @@ -0,0 +1,16 @@ +import { makeStyles } from '@material-ui/styles'; +import React from 'react'; + +const useStyles = makeStyles({ + tag: { + maxWidth: '100%', + overflowX: 'auto', + }, +}); + +const PreTag = (props: React.HTMLProps) => { + const classes = useStyles(); + return
;
+};
+
+export default PreTag;
diff --git a/webui/src/components/Content/index.tsx b/webui/src/components/Content/index.tsx
new file mode 100644
index 00000000..56e52e1e
--- /dev/null
+++ b/webui/src/components/Content/index.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import html from 'remark-html';
+import parse from 'remark-parse';
+import remark2react from 'remark-react';
+import unified from 'unified';
+
+import ImageTag from './ImageTag';
+import PreTag from './PreTag';
+
+type Props = { markdown: string };
+const Content: React.FC = ({ markdown }: Props) => {
+  const processor = unified()
+    .use(parse)
+    .use(html)
+    .use(remark2react, {
+      remarkReactComponents: {
+        img: ImageTag,
+        pre: PreTag,
+      },
+    });
+
+  const contents: React.ReactNode = processor.processSync(markdown).contents;
+  return <>{contents};
+};
+
+export default Content;
diff --git a/webui/src/components/Date.tsx b/webui/src/components/Date.tsx
new file mode 100644
index 00000000..a830546c
--- /dev/null
+++ b/webui/src/components/Date.tsx
@@ -0,0 +1,17 @@
+import Tooltip from '@material-ui/core/Tooltip/Tooltip';
+import moment from 'moment';
+import React from 'react';
+import Moment from 'react-moment';
+
+const HOUR = 1000 * 3600;
+const DAY = 24 * HOUR;
+const WEEK = 7 * DAY;
+
+type Props = { date: string };
+const Date = ({ date }: Props) => (
+  
+    
+  
+);
+
+export default Date;
diff --git a/webui/src/components/Label.tsx b/webui/src/components/Label.tsx
new file mode 100644
index 00000000..48c20096
--- /dev/null
+++ b/webui/src/components/Label.tsx
@@ -0,0 +1,55 @@
+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 './fragments.generated';
+import { Color } from '../gqlTypes';
+
+// Minimum contrast between the background and the text color
+const contrastThreshold = 2.5;
+
+// Guess the text color based on the background color
+const getTextColor = (background: string) =>
+  getContrastRatio(background, common.white) >= contrastThreshold
+    ? common.white // White on dark backgrounds
+    : common.black; // And black on light ones
+
+const _rgb = (color: Color) =>
+  'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
+
+// Create a style object from the label RGB colors
+const createStyle = (color: Color) => ({
+  backgroundColor: _rgb(color),
+  color: getTextColor(_rgb(color)),
+  borderBottomColor: darken(_rgb(color), 0.2),
+});
+
+const useStyles = makeStyles(theme => ({
+  label: {
+    ...theme.typography.body1,
+    padding: '1px 6px 0.5px',
+    fontSize: '0.9em',
+    fontWeight: 500,
+    margin: '0.05em 1px calc(-1.5px + 0.05em)',
+    borderRadius: '3px',
+    display: 'inline-block',
+    borderBottom: 'solid 1.5px',
+    verticalAlign: 'bottom',
+  },
+}));
+
+type Props = { label: LabelFragment };
+function Label({ label }: Props) {
+  const classes = useStyles();
+  return (
+    
+      {label.name}
+    
+  );
+}
+
+export default Label;
diff --git a/webui/src/components/fragments.graphql b/webui/src/components/fragments.graphql
new file mode 100644
index 00000000..03a235f9
--- /dev/null
+++ b/webui/src/components/fragments.graphql
@@ -0,0 +1,19 @@
+# Label.tsx
+fragment Label on Label {
+  name
+  color {
+    R
+    G
+    B
+  }
+}
+
+# Author.tsx
+fragment authored on Authored {
+  author {
+    name
+    email
+    displayName
+    avatarUrl
+  }
+}
diff --git a/webui/src/list/BugRow.graphql b/webui/src/list/BugRow.graphql
index 3f9a1ef6..c2966f10 100644
--- a/webui/src/list/BugRow.graphql
+++ b/webui/src/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/list/BugRow.tsx
index f94538a7..181aec2e 100644
--- a/webui/src/list/BugRow.tsx
+++ b/webui/src/list/BugRow.tsx
@@ -7,8 +7,8 @@ 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 Date from '../components/Date';
+import Label from '../components/Label';
 import { Status } from '../gqlTypes';
 
 import { BugRowFragment } from './BugRow.generated';
diff --git a/webui/src/tag/ImageTag.tsx b/webui/src/tag/ImageTag.tsx
deleted file mode 100644
index bdb36873..00000000
--- a/webui/src/tag/ImageTag.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { makeStyles } from '@material-ui/styles';
-import React from 'react';
-
-const useStyles = makeStyles({
-  tag: {
-    maxWidth: '100%',
-  },
-});
-
-const ImageTag = ({
-  alt,
-  ...props
-}: React.ImgHTMLAttributes) => {
-  const classes = useStyles();
-  return (
-    
-      {alt}
-    
-  );
-};
-
-export default ImageTag;
diff --git a/webui/src/tag/PreTag.tsx b/webui/src/tag/PreTag.tsx
deleted file mode 100644
index d3b4c273..00000000
--- a/webui/src/tag/PreTag.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { makeStyles } from '@material-ui/styles';
-import React from 'react';
-
-const useStyles = makeStyles({
-  tag: {
-    maxWidth: '100%',
-    overflowX: 'auto',
-  },
-});
-
-const PreTag = (props: React.HTMLProps) => {
-  const classes = useStyles();
-  return 
;
-};
-
-export default PreTag;
-- 
cgit 


From ce6f6a984b374b189141116433ced80dfa0c2aae Mon Sep 17 00:00:00 2001
From: Quentin Gliech 
Date: Thu, 13 Feb 2020 20:00:03 +0100
Subject: webui: move pages components

---
 webui/.eslintrc.js                                 |   6 +-
 webui/src/App.tsx                                  |  65 +----
 webui/src/CurrentIdentity.graphql                  |   8 -
 webui/src/CurrentIdentity.tsx                      |  30 --
 webui/src/__tests__/query.ts                       |   2 +-
 webui/src/apollo.ts                                |  18 ++
 webui/src/bug/Bug.graphql                          |  13 -
 webui/src/bug/Bug.tsx                              |  97 -------
 webui/src/bug/BugQuery.graphql                     |   9 -
 webui/src/bug/BugQuery.tsx                         |  22 --
 webui/src/bug/CommentForm.graphql                  |   5 -
 webui/src/bug/CommentForm.tsx                      | 145 ----------
 webui/src/bug/LabelChange.tsx                      |  49 ----
 webui/src/bug/LabelChangeFragment.graphql          |  12 -
 webui/src/bug/Message.tsx                          |  78 -----
 webui/src/bug/MessageCommentFragment.graphql       |   8 -
 webui/src/bug/MessageCreateFragment.graphql        |   8 -
 webui/src/bug/SetStatus.tsx                        |  31 --
 webui/src/bug/SetStatusFragment.graphql            |   7 -
 webui/src/bug/SetTitle.tsx                         |  37 ---
 webui/src/bug/SetTitleFragment.graphql             |   8 -
 webui/src/bug/Timeline.tsx                         |  48 ----
 webui/src/bug/TimelineQuery.graphql                |  39 ---
 webui/src/bug/TimelineQuery.tsx                    |  30 --
 webui/src/components/Author.tsx                    |   3 +-
 webui/src/components/Content/ImageTag.tsx          |   3 +-
 webui/src/components/Content/PreTag.tsx            |   3 +-
 webui/src/components/Date.tsx                      |   3 +-
 webui/src/components/Label.tsx                     |   6 +-
 webui/src/index.tsx                                |  29 +-
 webui/src/layout/CurrentIdentity.graphql           |   8 +
 webui/src/layout/CurrentIdentity.tsx               |  31 ++
 webui/src/layout/Header.tsx                        |  50 ++++
 webui/src/layout/index.tsx                         |  18 ++
 webui/src/list/BugRow.graphql                      |  13 -
 webui/src/list/BugRow.tsx                          | 112 --------
 webui/src/list/Filter.tsx                          | 189 -------------
 webui/src/list/FilterToolbar.graphql               |   7 -
 webui/src/list/FilterToolbar.tsx                   | 128 ---------
 webui/src/list/List.tsx                            |  21 --
 webui/src/list/ListQuery.graphql                   |  37 ---
 webui/src/list/ListQuery.tsx                       | 314 --------------------
 webui/src/pages/bug/Bug.graphql                    |  13 +
 webui/src/pages/bug/Bug.tsx                        |  98 +++++++
 webui/src/pages/bug/BugQuery.graphql               |   9 +
 webui/src/pages/bug/BugQuery.tsx                   |  23 ++
 webui/src/pages/bug/CommentForm.graphql            |   5 +
 webui/src/pages/bug/CommentForm.tsx                | 146 ++++++++++
 webui/src/pages/bug/LabelChange.tsx                |  50 ++++
 webui/src/pages/bug/LabelChangeFragment.graphql    |  12 +
 webui/src/pages/bug/Message.tsx                    |  79 ++++++
 webui/src/pages/bug/MessageCommentFragment.graphql |   8 +
 webui/src/pages/bug/MessageCreateFragment.graphql  |   8 +
 webui/src/pages/bug/SetStatus.tsx                  |  32 +++
 webui/src/pages/bug/SetStatusFragment.graphql      |   7 +
 webui/src/pages/bug/SetTitle.tsx                   |  38 +++
 webui/src/pages/bug/SetTitleFragment.graphql       |   8 +
 webui/src/pages/bug/Timeline.tsx                   |  49 ++++
 webui/src/pages/bug/TimelineQuery.graphql          |  39 +++
 webui/src/pages/bug/TimelineQuery.tsx              |  31 ++
 webui/src/pages/bug/index.tsx                      |   1 +
 webui/src/pages/list/BugRow.graphql                |  13 +
 webui/src/pages/list/BugRow.tsx                    | 113 ++++++++
 webui/src/pages/list/Filter.tsx                    | 190 +++++++++++++
 webui/src/pages/list/FilterToolbar.graphql         |   7 +
 webui/src/pages/list/FilterToolbar.tsx             | 129 +++++++++
 webui/src/pages/list/List.tsx                      |  22 ++
 webui/src/pages/list/ListQuery.graphql             |  37 +++
 webui/src/pages/list/ListQuery.tsx                 | 315 +++++++++++++++++++++
 webui/src/pages/list/index.ts                      |   1 +
 webui/src/theme.ts                                 |  11 +
 webui/tsconfig.json                                |  16 +-
 72 files changed, 1663 insertions(+), 1597 deletions(-)
 delete mode 100644 webui/src/CurrentIdentity.graphql
 delete mode 100644 webui/src/CurrentIdentity.tsx
 create mode 100644 webui/src/apollo.ts
 delete mode 100644 webui/src/bug/Bug.graphql
 delete mode 100644 webui/src/bug/Bug.tsx
 delete mode 100644 webui/src/bug/BugQuery.graphql
 delete mode 100644 webui/src/bug/BugQuery.tsx
 delete mode 100644 webui/src/bug/CommentForm.graphql
 delete mode 100644 webui/src/bug/CommentForm.tsx
 delete mode 100644 webui/src/bug/LabelChange.tsx
 delete mode 100644 webui/src/bug/LabelChangeFragment.graphql
 delete mode 100644 webui/src/bug/Message.tsx
 delete mode 100644 webui/src/bug/MessageCommentFragment.graphql
 delete mode 100644 webui/src/bug/MessageCreateFragment.graphql
 delete mode 100644 webui/src/bug/SetStatus.tsx
 delete mode 100644 webui/src/bug/SetStatusFragment.graphql
 delete mode 100644 webui/src/bug/SetTitle.tsx
 delete mode 100644 webui/src/bug/SetTitleFragment.graphql
 delete mode 100644 webui/src/bug/Timeline.tsx
 delete mode 100644 webui/src/bug/TimelineQuery.graphql
 delete mode 100644 webui/src/bug/TimelineQuery.tsx
 create mode 100644 webui/src/layout/CurrentIdentity.graphql
 create mode 100644 webui/src/layout/CurrentIdentity.tsx
 create mode 100644 webui/src/layout/Header.tsx
 create mode 100644 webui/src/layout/index.tsx
 delete mode 100644 webui/src/list/BugRow.graphql
 delete mode 100644 webui/src/list/BugRow.tsx
 delete mode 100644 webui/src/list/Filter.tsx
 delete mode 100644 webui/src/list/FilterToolbar.graphql
 delete mode 100644 webui/src/list/FilterToolbar.tsx
 delete mode 100644 webui/src/list/List.tsx
 delete mode 100644 webui/src/list/ListQuery.graphql
 delete mode 100644 webui/src/list/ListQuery.tsx
 create mode 100644 webui/src/pages/bug/Bug.graphql
 create mode 100644 webui/src/pages/bug/Bug.tsx
 create mode 100644 webui/src/pages/bug/BugQuery.graphql
 create mode 100644 webui/src/pages/bug/BugQuery.tsx
 create mode 100644 webui/src/pages/bug/CommentForm.graphql
 create mode 100644 webui/src/pages/bug/CommentForm.tsx
 create mode 100644 webui/src/pages/bug/LabelChange.tsx
 create mode 100644 webui/src/pages/bug/LabelChangeFragment.graphql
 create mode 100644 webui/src/pages/bug/Message.tsx
 create mode 100644 webui/src/pages/bug/MessageCommentFragment.graphql
 create mode 100644 webui/src/pages/bug/MessageCreateFragment.graphql
 create mode 100644 webui/src/pages/bug/SetStatus.tsx
 create mode 100644 webui/src/pages/bug/SetStatusFragment.graphql
 create mode 100644 webui/src/pages/bug/SetTitle.tsx
 create mode 100644 webui/src/pages/bug/SetTitleFragment.graphql
 create mode 100644 webui/src/pages/bug/Timeline.tsx
 create mode 100644 webui/src/pages/bug/TimelineQuery.graphql
 create mode 100644 webui/src/pages/bug/TimelineQuery.tsx
 create mode 100644 webui/src/pages/bug/index.tsx
 create mode 100644 webui/src/pages/list/BugRow.graphql
 create mode 100644 webui/src/pages/list/BugRow.tsx
 create mode 100644 webui/src/pages/list/Filter.tsx
 create mode 100644 webui/src/pages/list/FilterToolbar.graphql
 create mode 100644 webui/src/pages/list/FilterToolbar.tsx
 create mode 100644 webui/src/pages/list/List.tsx
 create mode 100644 webui/src/pages/list/ListQuery.graphql
 create mode 100644 webui/src/pages/list/ListQuery.tsx
 create mode 100644 webui/src/pages/list/index.ts
 create mode 100644 webui/src/theme.ts

(limited to 'webui')

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/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 (
-    
-      
-      
-        
-          
-            git-bug
-            git-bug
-          
-          
- -
-
-
+ - - + + - + ); } diff --git a/webui/src/CurrentIdentity.graphql b/webui/src/CurrentIdentity.graphql deleted file mode 100644 index 2794a40f..00000000 --- a/webui/src/CurrentIdentity.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query CurrentIdentity { - repository { - userIdentity { - displayName - avatarUrl - } - } -} diff --git a/webui/src/CurrentIdentity.tsx b/webui/src/CurrentIdentity.tsx deleted file mode 100644 index 256f44c4..00000000 --- a/webui/src/CurrentIdentity.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import Avatar from '@material-ui/core/Avatar'; -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; - -const useStyles = makeStyles(theme => ({ - displayName: { - marginLeft: theme.spacing(2), - }, -})); - -const CurrentIdentity = () => { - const classes = useStyles(); - const { loading, error, data } = useCurrentIdentityQuery(); - - if (error || loading || !data?.repository?.userIdentity) return null; - - const user = data.repository.userIdentity; - return ( - <> - - {user.displayName.charAt(0).toUpperCase()} - -
{user.displayName}
- - ); -}; - -export default CurrentIdentity; 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/bug/Bug.graphql b/webui/src/bug/Bug.graphql deleted file mode 100644 index 498242c0..00000000 --- a/webui/src/bug/Bug.graphql +++ /dev/null @@ -1,13 +0,0 @@ -#import "../components/fragments.graphql" - -fragment Bug on Bug { - id - humanId - status - title - labels { - ...Label - } - createdAt - ...authored -} diff --git a/webui/src/bug/Bug.tsx b/webui/src/bug/Bug.tsx deleted file mode 100644 index 0e53e447..00000000 --- a/webui/src/bug/Bug.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import Typography from '@material-ui/core/Typography/Typography'; -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import Author from '../components/Author'; -import Date from '../components/Date'; -import Label from '../components/Label'; - -import { BugFragment } from './Bug.generated'; -import CommentForm from './CommentForm'; -import TimelineQuery from './TimelineQuery'; - -const useStyles = makeStyles(theme => ({ - main: { - maxWidth: 800, - margin: 'auto', - marginTop: theme.spacing(4), - }, - header: { - marginLeft: theme.spacing(1) + 40, - }, - title: { - ...theme.typography.h5, - }, - id: { - ...theme.typography.subtitle1, - marginLeft: theme.spacing(1), - }, - container: { - display: 'flex', - marginBottom: theme.spacing(1), - }, - timeline: { - flex: 1, - marginTop: theme.spacing(2), - marginRight: theme.spacing(2), - minWidth: 0, - }, - sidebar: { - marginTop: theme.spacing(2), - flex: '0 0 200px', - }, - labelList: { - listStyle: 'none', - padding: 0, - margin: 0, - }, - label: { - marginTop: theme.spacing(1), - marginBottom: theme.spacing(1), - '& > *': { - display: 'block', - }, - }, -})); - -type Props = { - bug: BugFragment; -}; - -function Bug({ bug }: Props) { - const classes = useStyles(); - return ( -
-
- {bug.title} - {bug.humanId} - - - - {' opened this bug '} - - -
- -
-
- -
-
- Labels -
    - {bug.labels.map(l => ( -
  • -
  • - ))} -
-
-
- - -
- ); -} - -export default Bug; diff --git a/webui/src/bug/BugQuery.graphql b/webui/src/bug/BugQuery.graphql deleted file mode 100644 index cdc4723f..00000000 --- a/webui/src/bug/BugQuery.graphql +++ /dev/null @@ -1,9 +0,0 @@ -#import "./Bug.graphql" - -query GetBug($id: String!) { - repository { - bug(prefix: $id) { - ...Bug - } - } -} diff --git a/webui/src/bug/BugQuery.tsx b/webui/src/bug/BugQuery.tsx deleted file mode 100644 index 2ecf718c..00000000 --- a/webui/src/bug/BugQuery.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import CircularProgress from '@material-ui/core/CircularProgress'; -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; - -import Bug from './Bug'; -import { useGetBugQuery } from './BugQuery.generated'; - -type Props = RouteComponentProps<{ - id: string; -}>; - -const BugQuery: React.FC = ({ match }: Props) => { - const { loading, error, data } = useGetBugQuery({ - variables: { id: match.params.id }, - }); - if (loading) return ; - if (error) return

Error: {error}

; - if (!data?.repository?.bug) return

404.

; - return ; -}; - -export default BugQuery; diff --git a/webui/src/bug/CommentForm.graphql b/webui/src/bug/CommentForm.graphql deleted file mode 100644 index 33d21193..00000000 --- a/webui/src/bug/CommentForm.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation AddComment($input: AddCommentInput!) { - addComment(input: $input) { - operation { id } - } -} diff --git a/webui/src/bug/CommentForm.tsx b/webui/src/bug/CommentForm.tsx deleted file mode 100644 index a915ecf0..00000000 --- a/webui/src/bug/CommentForm.tsx +++ /dev/null @@ -1,145 +0,0 @@ -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 React, { useState, useRef } from 'react'; - -import Content from '../components/Content'; - -import { useAddCommentMutation } from './CommentForm.generated'; -import { TimelineDocument } from './TimelineQuery.generated'; - -type StyleProps = { loading: boolean }; -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', - }, - actions: { - display: 'flex', - justifyContent: 'flex-end', - }, -})); - -type TabPanelProps = { - children: React.ReactNode; - value: number; - index: number; -} & React.HTMLProps; -function TabPanel({ children, value, index, ...props }: TabPanelProps) { - return ( - - ); -} - -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(''); - const [tab, setTab] = useState(0); - const classes = useStyles({ loading }); - const form = useRef(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) => { - e.preventDefault(); - submit(); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - // Submit on cmd/ctrl+enter - if ((e.metaKey || e.altKey) && e.keyCode === 13) { - submit(); - } - }; - - return ( - -
- setTab(t)}> - - - -
- - setInput(e.target.value)} - disabled={loading} - /> - - - - -
-
- -
-
-
- ); -} - -export default CommentForm; diff --git a/webui/src/bug/LabelChange.tsx b/webui/src/bug/LabelChange.tsx deleted file mode 100644 index a3950524..00000000 --- a/webui/src/bug/LabelChange.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import Author from '../components/Author'; -import Date from '../components/Date'; -import Label from '../components/Label'; - -import { LabelChangeFragment } from './LabelChangeFragment.generated'; - -const useStyles = makeStyles(theme => ({ - main: { - ...theme.typography.body1, - marginLeft: theme.spacing(1) + 40, - }, - author: { - fontWeight: 'bold', - }, -})); - -type Props = { - op: LabelChangeFragment; -}; - -function LabelChange({ op }: Props) { - const { added, removed } = op; - const classes = useStyles(); - return ( -
- - {added.length > 0 && added the } - {added.map((label, index) => ( -
- ); -} - -export default LabelChange; diff --git a/webui/src/bug/LabelChangeFragment.graphql b/webui/src/bug/LabelChangeFragment.graphql deleted file mode 100644 index 01b94a98..00000000 --- a/webui/src/bug/LabelChangeFragment.graphql +++ /dev/null @@ -1,12 +0,0 @@ -#import "../components/fragments.graphql" - -fragment LabelChange on LabelChangeTimelineItem { - date - ...authored - added { - ...Label - } - removed { - ...Label - } -} diff --git a/webui/src/bug/Message.tsx b/webui/src/bug/Message.tsx deleted file mode 100644 index a61ed3f2..00000000 --- a/webui/src/bug/Message.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import Paper from '@material-ui/core/Paper'; -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import Author, { Avatar } from '../components/Author'; -import Date from '../components/Date'; -import Content from '../components/Content'; - -import { AddCommentFragment } from './MessageCommentFragment.generated'; -import { CreateFragment } from './MessageCreateFragment.generated'; - -const useStyles = makeStyles(theme => ({ - author: { - fontWeight: 'bold', - }, - container: { - display: 'flex', - }, - avatar: { - marginTop: 2, - }, - bubble: { - flex: 1, - marginLeft: theme.spacing(1), - minWidth: 0, - }, - header: { - ...theme.typography.body1, - color: '#444', - padding: '0.5rem 1rem', - borderBottom: '1px solid #ddd', - display: 'flex', - }, - title: { - flex: 1, - }, - tag: { - ...theme.typography.button, - color: '#888', - border: '#ddd solid 1px', - padding: '0 0.5rem', - fontSize: '0.75rem', - borderRadius: 2, - marginLeft: '0.5rem', - }, - body: { - ...theme.typography.body2, - padding: '0 1rem', - }, -})); - -type Props = { - op: AddCommentFragment | CreateFragment; -}; - -function Message({ op }: Props) { - const classes = useStyles(); - return ( -
- - -
-
- - commented - -
- {op.edited &&
Edited
} -
-
- -
-
-
- ); -} - -export default Message; diff --git a/webui/src/bug/MessageCommentFragment.graphql b/webui/src/bug/MessageCommentFragment.graphql deleted file mode 100644 index 61156fee..00000000 --- a/webui/src/bug/MessageCommentFragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -#import "../components/fragments.graphql" - -fragment AddComment on AddCommentTimelineItem { - createdAt - ...authored - edited - message -} diff --git a/webui/src/bug/MessageCreateFragment.graphql b/webui/src/bug/MessageCreateFragment.graphql deleted file mode 100644 index e371b9dc..00000000 --- a/webui/src/bug/MessageCreateFragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -#import "../components/fragments.graphql" - -fragment Create on CreateTimelineItem { - createdAt - ...authored - edited - message -} diff --git a/webui/src/bug/SetStatus.tsx b/webui/src/bug/SetStatus.tsx deleted file mode 100644 index 86105c8a..00000000 --- a/webui/src/bug/SetStatus.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import Author from '../components/Author'; -import Date from '../components/Date'; - -import { SetStatusFragment } from './SetStatusFragment.generated'; - -const useStyles = makeStyles(theme => ({ - main: { - ...theme.typography.body1, - marginLeft: theme.spacing(1) + 40, - }, -})); - -type Props = { - op: SetStatusFragment; -}; - -function SetStatus({ op }: Props) { - const classes = useStyles(); - return ( -
- - {op.status.toLowerCase()} this - -
- ); -} - -export default SetStatus; diff --git a/webui/src/bug/SetStatusFragment.graphql b/webui/src/bug/SetStatusFragment.graphql deleted file mode 100644 index 5a3986d0..00000000 --- a/webui/src/bug/SetStatusFragment.graphql +++ /dev/null @@ -1,7 +0,0 @@ -#import "../components/fragments.graphql" - -fragment SetStatus on SetStatusTimelineItem { - date - ...authored - status -} diff --git a/webui/src/bug/SetTitle.tsx b/webui/src/bug/SetTitle.tsx deleted file mode 100644 index e57aaafb..00000000 --- a/webui/src/bug/SetTitle.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import Author from '../components/Author'; -import Date from '../components/Date'; - -import { SetTitleFragment } from './SetTitleFragment.generated'; - -const useStyles = makeStyles(theme => ({ - main: { - ...theme.typography.body1, - marginLeft: theme.spacing(1) + 40, - }, - bold: { - fontWeight: 'bold', - }, -})); - -type Props = { - op: SetTitleFragment; -}; - -function SetTitle({ op }: Props) { - const classes = useStyles(); - return ( -
- - changed the title from - {op.was} - to - {op.title} - -
- ); -} - -export default SetTitle; diff --git a/webui/src/bug/SetTitleFragment.graphql b/webui/src/bug/SetTitleFragment.graphql deleted file mode 100644 index 22d2185c..00000000 --- a/webui/src/bug/SetTitleFragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -#import "../components/fragments.graphql" - -fragment SetTitle on SetTitleTimelineItem { - date - ...authored - title - was -} diff --git a/webui/src/bug/Timeline.tsx b/webui/src/bug/Timeline.tsx deleted file mode 100644 index ba0f9fc7..00000000 --- a/webui/src/bug/Timeline.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; - -import LabelChange from './LabelChange'; -import Message from './Message'; -import SetStatus from './SetStatus'; -import SetTitle from './SetTitle'; -import { TimelineItemFragment } from './TimelineQuery.generated'; - -const useStyles = makeStyles(theme => ({ - main: { - '& > *:not(:last-child)': { - marginBottom: theme.spacing(2), - }, - }, -})); - -type Props = { - ops: Array; -}; - -function Timeline({ ops }: Props) { - const classes = useStyles(); - - return ( -
- {ops.map((op, index) => { - switch (op.__typename) { - case 'CreateTimelineItem': - return ; - case 'AddCommentTimelineItem': - return ; - case 'LabelChangeTimelineItem': - return ; - case 'SetTitleTimelineItem': - return ; - case 'SetStatusTimelineItem': - return ; - } - - console.warn('unsupported operation type ' + op.__typename); - return null; - })} -
- ); -} - -export default Timeline; diff --git a/webui/src/bug/TimelineQuery.graphql b/webui/src/bug/TimelineQuery.graphql deleted file mode 100644 index 6d78ab7f..00000000 --- a/webui/src/bug/TimelineQuery.graphql +++ /dev/null @@ -1,39 +0,0 @@ -#import "./MessageCreateFragment.graphql" -#import "./MessageCommentFragment.graphql" -#import "./LabelChangeFragment.graphql" -#import "./SetTitleFragment.graphql" -#import "./SetStatusFragment.graphql" - -query Timeline($id: String!, $first: Int = 10, $after: String) { - repository { - bug(prefix: $id) { - timeline(first: $first, after: $after) { - nodes { - ...TimelineItem - } - pageInfo { - hasNextPage - endCursor - } - } - } - } -} - -fragment TimelineItem on TimelineItem { - ... on LabelChangeTimelineItem { - ...LabelChange - } - ... on SetStatusTimelineItem { - ...SetStatus - } - ... on SetTitleTimelineItem { - ...SetTitle - } - ... on AddCommentTimelineItem { - ...AddComment - } - ... on CreateTimelineItem { - ...Create - } -} diff --git a/webui/src/bug/TimelineQuery.tsx b/webui/src/bug/TimelineQuery.tsx deleted file mode 100644 index 9c4cf183..00000000 --- a/webui/src/bug/TimelineQuery.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import CircularProgress from '@material-ui/core/CircularProgress'; -import React from 'react'; - -import Timeline from './Timeline'; -import { useTimelineQuery } from './TimelineQuery.generated'; - -type Props = { - id: string; -}; - -const TimelineQuery = ({ id }: Props) => { - const { loading, error, data } = useTimelineQuery({ - variables: { - id, - first: 100, - }, - }); - - if (loading) return ; - if (error) return

Error: {error}

; - - const nodes = data?.repository?.bug?.timeline.nodes; - if (!nodes) { - return null; - } - - return ; -}; - -export default TimelineQuery; diff --git a/webui/src/components/Author.tsx b/webui/src/components/Author.tsx index 43fd108e..9ac1da52 100644 --- a/webui/src/components/Author.tsx +++ b/webui/src/components/Author.tsx @@ -1,6 +1,7 @@ +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 './fragments.generated'; diff --git a/webui/src/components/Content/ImageTag.tsx b/webui/src/components/Content/ImageTag.tsx index bdb36873..70ee1bc0 100644 --- a/webui/src/components/Content/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/components/Content/PreTag.tsx b/webui/src/components/Content/PreTag.tsx index d3b4c273..5256ab12 100644 --- a/webui/src/components/Content/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/components/Date.tsx b/webui/src/components/Date.tsx index a830546c..be0f5835 100644 --- a/webui/src/components/Date.tsx +++ b/webui/src/components/Date.tsx @@ -1,8 +1,9 @@ -import Tooltip from '@material-ui/core/Tooltip/Tooltip'; 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; diff --git a/webui/src/components/Label.tsx b/webui/src/components/Label.tsx index 48c20096..1fb8caea 100644 --- a/webui/src/components/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 { Color } from 'src/gqlTypes'; import { LabelFragment } from './fragments.generated'; -import { Color } from '../gqlTypes'; // Minimum contrast between the background and the text color const contrastThreshold = 2.5; 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( - + - - - + , diff --git a/webui/src/layout/CurrentIdentity.graphql b/webui/src/layout/CurrentIdentity.graphql new file mode 100644 index 00000000..2794a40f --- /dev/null +++ b/webui/src/layout/CurrentIdentity.graphql @@ -0,0 +1,8 @@ +query CurrentIdentity { + repository { + userIdentity { + displayName + avatarUrl + } + } +} diff --git a/webui/src/layout/CurrentIdentity.tsx b/webui/src/layout/CurrentIdentity.tsx new file mode 100644 index 00000000..21f489ef --- /dev/null +++ b/webui/src/layout/CurrentIdentity.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import Avatar from '@material-ui/core/Avatar'; +import { makeStyles } from '@material-ui/core/styles'; + +import { useCurrentIdentityQuery } from './CurrentIdentity.generated'; + +const useStyles = makeStyles(theme => ({ + displayName: { + marginLeft: theme.spacing(2), + }, +})); + +const CurrentIdentity = () => { + const classes = useStyles(); + const { loading, error, data } = useCurrentIdentityQuery(); + + if (error || loading || !data?.repository?.userIdentity) return null; + + const user = data.repository.userIdentity; + return ( + <> + + {user.displayName.charAt(0).toUpperCase()} + +
{user.displayName}
+ + ); +}; + +export default CurrentIdentity; 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 ( + <> + + + + git-bug + git-bug + +
+ +
+
+
+ + ); +} + +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 ( + <> + +
+ {children} + + ); +} + +export default Layout; diff --git a/webui/src/list/BugRow.graphql b/webui/src/list/BugRow.graphql deleted file mode 100644 index c2966f10..00000000 --- a/webui/src/list/BugRow.graphql +++ /dev/null @@ -1,13 +0,0 @@ -#import "../components/fragments.graphql" - -fragment BugRow on Bug { - id - humanId - title - status - createdAt - labels { - ...Label - } - ...authored -} diff --git a/webui/src/list/BugRow.tsx b/webui/src/list/BugRow.tsx deleted file mode 100644 index 181aec2e..00000000 --- a/webui/src/list/BugRow.tsx +++ /dev/null @@ -1,112 +0,0 @@ -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 '../components/Date'; -import Label from '../components/Label'; -import { Status } from '../gqlTypes'; - -import { BugRowFragment } from './BugRow.generated'; - -type OpenClosedProps = { className: string }; -const Open = ({ className }: OpenClosedProps) => ( - - - -); - -const Closed = ({ className }: OpenClosedProps) => ( - - - -); - -type StatusProps = { className: string; status: Status }; -const BugStatus: React.FC = ({ - status, - className, -}: StatusProps) => { - switch (status) { - case 'OPEN': - return ; - case 'CLOSED': - return ; - default: - return

{'unknown status ' + status}

; - } -}; - -const useStyles = makeStyles(theme => ({ - cell: { - display: 'flex', - alignItems: 'center', - padding: theme.spacing(1), - '& a': { - textDecoration: 'none', - }, - }, - status: { - margin: theme.spacing(1, 2), - }, - expand: { - width: '100%', - lineHeight: '20px', - }, - title: { - display: 'inline', - color: theme.palette.text.primary, - fontSize: '1.3rem', - fontWeight: 500, - }, - details: { - lineHeight: '1.5rem', - color: theme.palette.text.secondary, - }, - labels: { - paddingLeft: theme.spacing(1), - '& > *': { - display: 'inline-block', - }, - }, -})); - -type Props = { - bug: BugRowFragment; -}; - -function BugRow({ bug }: Props) { - const classes = useStyles(); - return ( - - - -
- -
- {bug.title} - {bug.labels.length > 0 && ( - - {bug.labels.map(l => ( - - )} -
- -
- {bug.humanId} opened - - by {bug.author.displayName} -
-
-
-
- ); -} - -export default BugRow; diff --git a/webui/src/list/Filter.tsx b/webui/src/list/Filter.tsx deleted file mode 100644 index 30b52de8..00000000 --- a/webui/src/list/Filter.tsx +++ /dev/null @@ -1,189 +0,0 @@ -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 }; - -function parse(query: string): Query { - // TODO: extract the rest of the query? - const params: Query = {}; - - // TODO: support escaping without quotes - const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g; - let matches; - while ((matches = re.exec(query)) !== null) { - if (!params[matches[1]]) { - params[matches[1]] = []; - } - - let value; - if (matches[4]) { - value = matches[4]; - } else { - value = matches[2]; - } - value = value.replace(/\\(.)/g, '$1'); - params[matches[1]].push(value); - } - return params; -} - -function quote(value: string): string { - const hasSingle = value.includes("'"); - const hasDouble = value.includes('"'); - const hasSpaces = value.includes(' '); - if (!hasSingle && !hasDouble && !hasSpaces) { - return value; - } - - if (!hasDouble) { - return `"${value}"`; - } - - if (!hasSingle) { - return `'${value}'`; - } - - value = value.replace(/"/g, '\\"'); - return `"${value}"`; -} - -function stringify(params: Query): string { - const parts: string[][] = Object.entries(params).map(([key, values]) => { - return values.map(value => `${key}:${quote(value)}`); - }); - return new Array().concat(...parts).join(' '); -} - -const useStyles = makeStyles(theme => ({ - element: { - ...theme.typography.body2, - color: '#444', - padding: theme.spacing(0, 1), - fontWeight: 400, - textDecoration: 'none', - display: 'flex', - background: 'none', - border: 'none', - }, - itemActive: { - fontWeight: 600, - color: '#333', - }, - icon: { - paddingRight: theme.spacing(0.5), - }, -})); - -type DropdownTuple = [string, string]; - -type FilterDropdownProps = { - children: React.ReactNode; - dropdown: DropdownTuple[]; - itemActive: (key: string) => boolean; - icon?: React.ComponentType; - to: (key: string) => LocationDescriptor; -} & React.ButtonHTMLAttributes; - -function FilterDropdown({ - children, - dropdown, - itemActive, - icon: Icon, - to, - ...props -}: FilterDropdownProps) { - const [open, setOpen] = useState(false); - const buttonRef = useRef(null); - const classes = useStyles({ active: false }); - - const content = ( - <> - {Icon && } -
{children}
- - ); - - return ( - <> - - setOpen(false)} - anchorEl={buttonRef.current} - > - {dropdown.map(([key, value]) => ( - setOpen(false)} - key={key} - > - {value} - - ))} - - - ); -} - -export type FilterProps = { - active: boolean; - to: LocationDescriptor; - icon?: React.ComponentType; - children: React.ReactNode; -}; -function Filter({ active, to, children, icon: Icon }: FilterProps) { - const classes = useStyles(); - - const content = ( - <> - {Icon && } -
{children}
- - ); - - if (to) { - return ( - - {content} - - ); - } - - return ( -
- {content} -
- ); -} - -export default Filter; -export { parse, stringify, quote, FilterDropdown, Filter }; diff --git a/webui/src/list/FilterToolbar.graphql b/webui/src/list/FilterToolbar.graphql deleted file mode 100644 index cd103f44..00000000 --- a/webui/src/list/FilterToolbar.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query BugCount($query: String) { - repository { - bugs: allBugs(query: $query) { - totalCount - } - } -} diff --git a/webui/src/list/FilterToolbar.tsx b/webui/src/list/FilterToolbar.tsx deleted file mode 100644 index b95b10bc..00000000 --- a/webui/src/list/FilterToolbar.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { pipe } from '@arrows/composition'; -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, - FilterProps, - Filter, - parse, - stringify, - Query, -} from './Filter'; -import { useBugCountQuery } from './FilterToolbar.generated'; - -const useStyles = makeStyles(theme => ({ - toolbar: { - backgroundColor: theme.palette.grey['100'], - borderColor: theme.palette.grey['300'], - borderWidth: '1px 0', - borderStyle: 'solid', - margin: theme.spacing(0, -1), - }, - spacer: { - flex: 1, - }, -})); - -// This prepends the filter text with a count -type CountingFilterProps = { - query: string; - children: React.ReactNode; -} & FilterProps; -function CountingFilter({ query, children, ...props }: CountingFilterProps) { - const { data, loading, error } = useBugCountQuery({ - variables: { query }, - }); - - var prefix; - if (loading) prefix = '...'; - else if (error || !data?.repository) prefix = '???'; - // TODO: better prefixes & error handling - else prefix = data.repository.bugs.totalCount; - - return ( - - {prefix} {children} - - ); -} - -type Props = { - query: string; - queryLocation: (query: string) => LocationDescriptor; -}; -function FilterToolbar({ query, queryLocation }: Props) { - const classes = useStyles(); - const params: Query = parse(query); - - const hasKey = (key: string): boolean => - params[key] && params[key].length > 0; - const hasValue = (key: string, value: string): boolean => - hasKey(key) && params[key].includes(value); - const loc = pipe(stringify, queryLocation); - const replaceParam = (key: string, value: string) => ( - params: Query - ): Query => ({ - ...params, - [key]: [value], - }); - const clearParam = (key: string) => (params: Query): Query => ({ - ...params, - [key]: [], - }); - - // TODO: author/label filters - return ( - - - open - - - closed - -
- {/* - Author - Label - */} - hasValue('sort', key)} - to={key => pipe(replaceParam('sort', key), loc)(params)} - > - Sort - - - ); -} - -export default FilterToolbar; diff --git a/webui/src/list/List.tsx b/webui/src/list/List.tsx deleted file mode 100644 index cebd13f2..00000000 --- a/webui/src/list/List.tsx +++ /dev/null @@ -1,21 +0,0 @@ -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'; - -type Props = { bugs: BugListFragment }; -function List({ bugs }: Props) { - return ( - - - {bugs.edges.map(({ cursor, node }) => ( - - ))} - -
- ); -} - -export default List; diff --git a/webui/src/list/ListQuery.graphql b/webui/src/list/ListQuery.graphql deleted file mode 100644 index ded60c8a..00000000 --- a/webui/src/list/ListQuery.graphql +++ /dev/null @@ -1,37 +0,0 @@ -#import "./BugRow.graphql" - -query ListBugs( - $first: Int - $last: Int - $after: String - $before: String - $query: String -) { - repository { - bugs: allBugs( - first: $first - last: $last - after: $after - before: $before - query: $query - ) { - ...BugList - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - } - } -} - -fragment BugList on BugConnection { - totalCount - edges { - cursor - node { - ...BugRow - } - } -} diff --git a/webui/src/list/ListQuery.tsx b/webui/src/list/ListQuery.tsx deleted file mode 100644 index 84b72431..00000000 --- a/webui/src/list/ListQuery.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import IconButton from '@material-ui/core/IconButton'; -import InputBase from '@material-ui/core/InputBase'; -import Paper from '@material-ui/core/Paper'; -import { fade, makeStyles, Theme } from '@material-ui/core/styles'; -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'; -import { useListBugsQuery } from './ListQuery.generated'; - -type StylesProps = { searching?: boolean }; -const useStyles = makeStyles(theme => ({ - main: { - maxWidth: 800, - margin: 'auto', - marginTop: theme.spacing(4), - marginBottom: theme.spacing(4), - overflow: 'hidden', - }, - pagination: { - ...theme.typography.overline, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - header: { - display: 'flex', - padding: theme.spacing(2), - '& > h1': { - ...theme.typography.h6, - margin: theme.spacing(0, 2), - }, - alignItems: 'center', - justifyContent: 'space-between', - }, - search: { - 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, 1), - width: ({ searching }) => (searching ? '20rem' : '15rem'), - transition: theme.transitions.create([ - 'width', - 'borderColor', - 'backgroundColor', - ]), - }, - searchFocused: { - borderColor: fade(theme.palette.primary.main, 0.4), - backgroundColor: theme.palette.background.paper, - width: '20rem!important', - }, - placeholderRow: { - padding: theme.spacing(1), - borderBottomColor: theme.palette.grey['300'], - borderBottomWidth: '1px', - borderBottomStyle: 'solid', - display: 'flex', - alignItems: 'center', - }, - placeholderRowStatus: { - margin: theme.spacing(1, 2), - }, - placeholderRowText: { - flex: 1, - }, - message: { - ...theme.typography.h5, - padding: theme.spacing(8), - textAlign: 'center', - borderBottomColor: theme.palette.grey['300'], - borderBottomWidth: '1px', - borderBottomStyle: 'solid', - '& > p': { - margin: '0', - }, - }, - errorBox: { - color: theme.palette.error.main, - '& > pre': { - fontSize: '1rem', - textAlign: 'left', - backgroundColor: theme.palette.grey['900'], - color: theme.palette.common.white, - marginTop: theme.spacing(4), - padding: theme.spacing(2, 3), - }, - }, -})); - -function editParams( - params: URLSearchParams, - callback: (params: URLSearchParams) => void -) { - const cloned = new URLSearchParams(params.toString()); - callback(cloned); - return cloned; -} - -// TODO: factor this out -type PlaceholderProps = { count: number }; -const Placeholder: React.FC = ({ - count, -}: PlaceholderProps) => { - const classes = useStyles({}); - return ( - <> - {new Array(count).fill(null).map((_, i) => ( -
- -
- - -
-
- ))} - - ); -}; - -// TODO: factor this out -const NoBug = () => { - const classes = useStyles({}); - return ( -
- -

No results matched your search.

-
- ); -}; - -type ErrorProps = { error: ApolloError }; -const Error: React.FC = ({ error }: ErrorProps) => { - const classes = useStyles({}); - return ( -
- -

There was an error while fetching bug.

-

- {error.message} -

-
-        {JSON.stringify(error, null, 2)}
-      
-
- ); -}; - -function ListQuery() { - const location = useLocation(); - const history = useHistory(); - const params = new URLSearchParams(location.search); - const query = params.get('q') || ''; - - const [input, setInput] = useState(query); - - const classes = useStyles({ searching: !!input }); - - // TODO is this the right way to do it? - const lastQuery = useRef(null); - useEffect(() => { - if (query !== lastQuery.current) { - setInput(query); - } - lastQuery.current = query; - }, [query, input, lastQuery]); - - const num = (param: string | null) => (param ? parseInt(param) : null); - const page = { - first: num(params.get('first')), - last: num(params.get('last')), - after: params.get('after'), - before: params.get('before'), - }; - - // If nothing set, show the first 10 items - if (!page.first && !page.last) { - page.first = 10; - } - - const perPage = (page.first || page.last || 10).toString(); - - const { loading, error, data } = useListBugsQuery({ - variables: { - ...page, - query, - }, - }); - - let nextPage = null; - let previousPage = null; - let count = 0; - if (!loading && !error && data?.repository?.bugs) { - const bugs = data.repository.bugs; - count = bugs.totalCount; - // This computes the URL for the next page - if (bugs.pageInfo.hasNextPage) { - nextPage = { - ...location, - search: editParams(params, p => { - p.delete('last'); - p.delete('before'); - p.set('first', perPage); - p.set('after', bugs.pageInfo.endCursor); - }).toString(), - }; - } - // and this for the previous page - if (bugs.pageInfo.hasPreviousPage) { - previousPage = { - ...location, - search: editParams(params, p => { - p.delete('first'); - p.delete('after'); - p.set('last', perPage); - p.set('before', bugs.pageInfo.startCursor); - }).toString(), - }; - } - } - - // Prepare params without paging for editing filters - const paramsWithoutPaging = editParams(params, p => { - p.delete('first'); - p.delete('last'); - p.delete('before'); - p.delete('after'); - }); - // Returns a new location with the `q` param edited - const queryLocation = (query: string) => ({ - ...location, - search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(), - }); - - let content; - if (loading) { - content = ; - } else if (error) { - content = ; - } else if (data?.repository) { - const bugs = data.repository.bugs; - - if (bugs.totalCount === 0) { - content = ; - } else { - content = ; - } - } - - const formSubmit = (e: React.FormEvent) => { - e.preventDefault(); - history.push(queryLocation(input)); - }; - - return ( - -
-

Issues

-
- setInput(e.target.value)} - classes={{ - root: classes.search, - focused: classes.searchFocused, - }} - /> - - -
- - {content} -
- {previousPage ? ( - - - - ) : ( - - - - )} -
{loading ? 'Loading' : `Total: ${count}`}
- {nextPage ? ( - - - - ) : ( - - - - )} -
-
- ); -} - -export default ListQuery; diff --git a/webui/src/pages/bug/Bug.graphql b/webui/src/pages/bug/Bug.graphql new file mode 100644 index 00000000..498242c0 --- /dev/null +++ b/webui/src/pages/bug/Bug.graphql @@ -0,0 +1,13 @@ +#import "../components/fragments.graphql" + +fragment Bug on Bug { + id + humanId + status + title + labels { + ...Label + } + createdAt + ...authored +} diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx new file mode 100644 index 00000000..998c9528 --- /dev/null +++ b/webui/src/pages/bug/Bug.tsx @@ -0,0 +1,98 @@ +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 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, + margin: 'auto', + marginTop: theme.spacing(4), + }, + header: { + marginLeft: theme.spacing(1) + 40, + }, + title: { + ...theme.typography.h5, + }, + id: { + ...theme.typography.subtitle1, + marginLeft: theme.spacing(1), + }, + container: { + display: 'flex', + marginBottom: theme.spacing(1), + }, + timeline: { + flex: 1, + marginTop: theme.spacing(2), + marginRight: theme.spacing(2), + minWidth: 0, + }, + sidebar: { + marginTop: theme.spacing(2), + flex: '0 0 200px', + }, + labelList: { + listStyle: 'none', + padding: 0, + margin: 0, + }, + label: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + '& > *': { + display: 'block', + }, + }, +})); + +type Props = { + bug: BugFragment; +}; + +function Bug({ bug }: Props) { + const classes = useStyles(); + return ( +
+
+ {bug.title} + {bug.humanId} + + + + {' opened this bug '} + + +
+ +
+
+ +
+
+ Labels +
    + {bug.labels.map(l => ( +
  • +
  • + ))} +
+
+
+ + +
+ ); +} + +export default Bug; diff --git a/webui/src/pages/bug/BugQuery.graphql b/webui/src/pages/bug/BugQuery.graphql new file mode 100644 index 00000000..cdc4723f --- /dev/null +++ b/webui/src/pages/bug/BugQuery.graphql @@ -0,0 +1,9 @@ +#import "./Bug.graphql" + +query GetBug($id: String!) { + repository { + bug(prefix: $id) { + ...Bug + } + } +} diff --git a/webui/src/pages/bug/BugQuery.tsx b/webui/src/pages/bug/BugQuery.tsx new file mode 100644 index 00000000..2a70a2f8 --- /dev/null +++ b/webui/src/pages/bug/BugQuery.tsx @@ -0,0 +1,23 @@ +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'; + +type Props = RouteComponentProps<{ + id: string; +}>; + +const BugQuery: React.FC = ({ match }: Props) => { + const { loading, error, data } = useGetBugQuery({ + variables: { id: match.params.id }, + }); + if (loading) return ; + if (error) return

Error: {error}

; + if (!data?.repository?.bug) return

404.

; + return ; +}; + +export default BugQuery; 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 => ({ + 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; +function TabPanel({ children, value, index, ...props }: TabPanelProps) { + return ( + + ); +} + +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(''); + const [tab, setTab] = useState(0); + const classes = useStyles({ loading }); + const form = useRef(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) => { + e.preventDefault(); + submit(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Submit on cmd/ctrl+enter + if ((e.metaKey || e.altKey) && e.keyCode === 13) { + submit(); + } + }; + + return ( + +
+ setTab(t)}> + + + +
+ + setInput(e.target.value)} + disabled={loading} + /> + + + + +
+
+ +
+
+
+ ); +} + +export default CommentForm; diff --git a/webui/src/pages/bug/LabelChange.tsx b/webui/src/pages/bug/LabelChange.tsx new file mode 100644 index 00000000..764947ee --- /dev/null +++ b/webui/src/pages/bug/LabelChange.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +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, + marginLeft: theme.spacing(1) + 40, + }, + author: { + fontWeight: 'bold', + }, +})); + +type Props = { + op: LabelChangeFragment; +}; + +function LabelChange({ op }: Props) { + const { added, removed } = op; + const classes = useStyles(); + return ( +
+ + {added.length > 0 && added the } + {added.map((label, index) => ( +
+ ); +} + +export default LabelChange; diff --git a/webui/src/pages/bug/LabelChangeFragment.graphql b/webui/src/pages/bug/LabelChangeFragment.graphql new file mode 100644 index 00000000..82d41235 --- /dev/null +++ b/webui/src/pages/bug/LabelChangeFragment.graphql @@ -0,0 +1,12 @@ +#import "../../components/fragments.graphql" + +fragment LabelChange on LabelChangeTimelineItem { + date + ...authored + added { + ...Label + } + removed { + ...Label + } +} diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx new file mode 100644 index 00000000..ebb42f6b --- /dev/null +++ b/webui/src/pages/bug/Message.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +import Paper from '@material-ui/core/Paper'; +import { makeStyles } from '@material-ui/core/styles'; + +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'; + +const useStyles = makeStyles(theme => ({ + author: { + fontWeight: 'bold', + }, + container: { + display: 'flex', + }, + avatar: { + marginTop: 2, + }, + bubble: { + flex: 1, + marginLeft: theme.spacing(1), + minWidth: 0, + }, + header: { + ...theme.typography.body1, + color: '#444', + padding: '0.5rem 1rem', + borderBottom: '1px solid #ddd', + display: 'flex', + }, + title: { + flex: 1, + }, + tag: { + ...theme.typography.button, + color: '#888', + border: '#ddd solid 1px', + padding: '0 0.5rem', + fontSize: '0.75rem', + borderRadius: 2, + marginLeft: '0.5rem', + }, + body: { + ...theme.typography.body2, + padding: '0 1rem', + }, +})); + +type Props = { + op: AddCommentFragment | CreateFragment; +}; + +function Message({ op }: Props) { + const classes = useStyles(); + return ( +
+ + +
+
+ + commented + +
+ {op.edited &&
Edited
} +
+
+ +
+
+
+ ); +} + +export default Message; diff --git a/webui/src/pages/bug/MessageCommentFragment.graphql b/webui/src/pages/bug/MessageCommentFragment.graphql new file mode 100644 index 00000000..00f8342d --- /dev/null +++ b/webui/src/pages/bug/MessageCommentFragment.graphql @@ -0,0 +1,8 @@ +#import "../../components/fragments.graphql" + +fragment AddComment on AddCommentTimelineItem { + createdAt + ...authored + edited + message +} diff --git a/webui/src/pages/bug/MessageCreateFragment.graphql b/webui/src/pages/bug/MessageCreateFragment.graphql new file mode 100644 index 00000000..4cae819d --- /dev/null +++ b/webui/src/pages/bug/MessageCreateFragment.graphql @@ -0,0 +1,8 @@ +#import "../../components/fragments.graphql" + +fragment Create on CreateTimelineItem { + createdAt + ...authored + edited + message +} diff --git a/webui/src/pages/bug/SetStatus.tsx b/webui/src/pages/bug/SetStatus.tsx new file mode 100644 index 00000000..251abf69 --- /dev/null +++ b/webui/src/pages/bug/SetStatus.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core/styles'; + +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, + marginLeft: theme.spacing(1) + 40, + }, +})); + +type Props = { + op: SetStatusFragment; +}; + +function SetStatus({ op }: Props) { + const classes = useStyles(); + return ( +
+ + {op.status.toLowerCase()} this + +
+ ); +} + +export default SetStatus; diff --git a/webui/src/pages/bug/SetStatusFragment.graphql b/webui/src/pages/bug/SetStatusFragment.graphql new file mode 100644 index 00000000..d8380409 --- /dev/null +++ b/webui/src/pages/bug/SetStatusFragment.graphql @@ -0,0 +1,7 @@ +#import "../../components/fragments.graphql" + +fragment SetStatus on SetStatusTimelineItem { + date + ...authored + status +} diff --git a/webui/src/pages/bug/SetTitle.tsx b/webui/src/pages/bug/SetTitle.tsx new file mode 100644 index 00000000..304fd2e2 --- /dev/null +++ b/webui/src/pages/bug/SetTitle.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +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, + marginLeft: theme.spacing(1) + 40, + }, + bold: { + fontWeight: 'bold', + }, +})); + +type Props = { + op: SetTitleFragment; +}; + +function SetTitle({ op }: Props) { + const classes = useStyles(); + return ( +
+ + changed the title from + {op.was} + to + {op.title} + +
+ ); +} + +export default SetTitle; diff --git a/webui/src/pages/bug/SetTitleFragment.graphql b/webui/src/pages/bug/SetTitleFragment.graphql new file mode 100644 index 00000000..2225dfd3 --- /dev/null +++ b/webui/src/pages/bug/SetTitleFragment.graphql @@ -0,0 +1,8 @@ +#import "../../components/fragments.graphql" + +fragment SetTitle on SetTitleTimelineItem { + date + ...authored + title + was +} diff --git a/webui/src/pages/bug/Timeline.tsx b/webui/src/pages/bug/Timeline.tsx new file mode 100644 index 00000000..73c88cdf --- /dev/null +++ b/webui/src/pages/bug/Timeline.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core/styles'; + +import LabelChange from './LabelChange'; +import Message from './Message'; +import SetStatus from './SetStatus'; +import SetTitle from './SetTitle'; +import { TimelineItemFragment } from './TimelineQuery.generated'; + +const useStyles = makeStyles(theme => ({ + main: { + '& > *:not(:last-child)': { + marginBottom: theme.spacing(2), + }, + }, +})); + +type Props = { + ops: Array; +}; + +function Timeline({ ops }: Props) { + const classes = useStyles(); + + return ( +
+ {ops.map((op, index) => { + switch (op.__typename) { + case 'CreateTimelineItem': + return ; + case 'AddCommentTimelineItem': + return ; + case 'LabelChangeTimelineItem': + return ; + case 'SetTitleTimelineItem': + return ; + case 'SetStatusTimelineItem': + return ; + } + + console.warn('unsupported operation type ' + op.__typename); + return null; + })} +
+ ); +} + +export default Timeline; diff --git a/webui/src/pages/bug/TimelineQuery.graphql b/webui/src/pages/bug/TimelineQuery.graphql new file mode 100644 index 00000000..6d78ab7f --- /dev/null +++ b/webui/src/pages/bug/TimelineQuery.graphql @@ -0,0 +1,39 @@ +#import "./MessageCreateFragment.graphql" +#import "./MessageCommentFragment.graphql" +#import "./LabelChangeFragment.graphql" +#import "./SetTitleFragment.graphql" +#import "./SetStatusFragment.graphql" + +query Timeline($id: String!, $first: Int = 10, $after: String) { + repository { + bug(prefix: $id) { + timeline(first: $first, after: $after) { + nodes { + ...TimelineItem + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} + +fragment TimelineItem on TimelineItem { + ... on LabelChangeTimelineItem { + ...LabelChange + } + ... on SetStatusTimelineItem { + ...SetStatus + } + ... on SetTitleTimelineItem { + ...SetTitle + } + ... on AddCommentTimelineItem { + ...AddComment + } + ... on CreateTimelineItem { + ...Create + } +} diff --git a/webui/src/pages/bug/TimelineQuery.tsx b/webui/src/pages/bug/TimelineQuery.tsx new file mode 100644 index 00000000..74eed52b --- /dev/null +++ b/webui/src/pages/bug/TimelineQuery.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import CircularProgress from '@material-ui/core/CircularProgress'; + +import Timeline from './Timeline'; +import { useTimelineQuery } from './TimelineQuery.generated'; + +type Props = { + id: string; +}; + +const TimelineQuery = ({ id }: Props) => { + const { loading, error, data } = useTimelineQuery({ + variables: { + id, + first: 100, + }, + }); + + if (loading) return ; + if (error) return

Error: {error}

; + + const nodes = data?.repository?.bug?.timeline.nodes; + if (!nodes) { + return null; + } + + return ; +}; + +export default TimelineQuery; 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/pages/list/BugRow.graphql b/webui/src/pages/list/BugRow.graphql new file mode 100644 index 00000000..547c09d8 --- /dev/null +++ b/webui/src/pages/list/BugRow.graphql @@ -0,0 +1,13 @@ +#import "../../components/fragments.graphql" + +fragment BugRow on Bug { + id + humanId + title + status + createdAt + labels { + ...Label + } + ...authored +} diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx new file mode 100644 index 00000000..829877ef --- /dev/null +++ b/webui/src/pages/list/BugRow.tsx @@ -0,0 +1,113 @@ +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 Date from 'src/components/Date'; +import Label from 'src/components/Label'; +import { Status } from 'src/gqlTypes'; + +import { BugRowFragment } from './BugRow.generated'; + +type OpenClosedProps = { className: string }; +const Open = ({ className }: OpenClosedProps) => ( + + + +); + +const Closed = ({ className }: OpenClosedProps) => ( + + + +); + +type StatusProps = { className: string; status: Status }; +const BugStatus: React.FC = ({ + status, + className, +}: StatusProps) => { + switch (status) { + case 'OPEN': + return ; + case 'CLOSED': + return ; + default: + return

{'unknown status ' + status}

; + } +}; + +const useStyles = makeStyles(theme => ({ + cell: { + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + '& a': { + textDecoration: 'none', + }, + }, + status: { + margin: theme.spacing(1, 2), + }, + expand: { + width: '100%', + lineHeight: '20px', + }, + title: { + display: 'inline', + color: theme.palette.text.primary, + fontSize: '1.3rem', + fontWeight: 500, + }, + details: { + lineHeight: '1.5rem', + color: theme.palette.text.secondary, + }, + labels: { + paddingLeft: theme.spacing(1), + '& > *': { + display: 'inline-block', + }, + }, +})); + +type Props = { + bug: BugRowFragment; +}; + +function BugRow({ bug }: Props) { + const classes = useStyles(); + return ( + + + +
+ +
+ {bug.title} + {bug.labels.length > 0 && ( + + {bug.labels.map(l => ( + + )} +
+ +
+ {bug.humanId} opened + + by {bug.author.displayName} +
+
+
+
+ ); +} + +export default BugRow; diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx new file mode 100644 index 00000000..1a3cdd6b --- /dev/null +++ b/webui/src/pages/list/Filter.tsx @@ -0,0 +1,190 @@ +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'; + +export type Query = { [key: string]: Array }; + +function parse(query: string): Query { + // TODO: extract the rest of the query? + const params: Query = {}; + + // TODO: support escaping without quotes + const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g; + let matches; + while ((matches = re.exec(query)) !== null) { + if (!params[matches[1]]) { + params[matches[1]] = []; + } + + let value; + if (matches[4]) { + value = matches[4]; + } else { + value = matches[2]; + } + value = value.replace(/\\(.)/g, '$1'); + params[matches[1]].push(value); + } + return params; +} + +function quote(value: string): string { + const hasSingle = value.includes("'"); + const hasDouble = value.includes('"'); + const hasSpaces = value.includes(' '); + if (!hasSingle && !hasDouble && !hasSpaces) { + return value; + } + + if (!hasDouble) { + return `"${value}"`; + } + + if (!hasSingle) { + return `'${value}'`; + } + + value = value.replace(/"/g, '\\"'); + return `"${value}"`; +} + +function stringify(params: Query): string { + const parts: string[][] = Object.entries(params).map(([key, values]) => { + return values.map(value => `${key}:${quote(value)}`); + }); + return new Array().concat(...parts).join(' '); +} + +const useStyles = makeStyles(theme => ({ + element: { + ...theme.typography.body2, + color: '#444', + padding: theme.spacing(0, 1), + fontWeight: 400, + textDecoration: 'none', + display: 'flex', + background: 'none', + border: 'none', + }, + itemActive: { + fontWeight: 600, + color: '#333', + }, + icon: { + paddingRight: theme.spacing(0.5), + }, +})); + +type DropdownTuple = [string, string]; + +type FilterDropdownProps = { + children: React.ReactNode; + dropdown: DropdownTuple[]; + itemActive: (key: string) => boolean; + icon?: React.ComponentType; + to: (key: string) => LocationDescriptor; +} & React.ButtonHTMLAttributes; + +function FilterDropdown({ + children, + dropdown, + itemActive, + icon: Icon, + to, + ...props +}: FilterDropdownProps) { + const [open, setOpen] = useState(false); + const buttonRef = useRef(null); + const classes = useStyles({ active: false }); + + const content = ( + <> + {Icon && } +
{children}
+ + ); + + return ( + <> + + setOpen(false)} + anchorEl={buttonRef.current} + > + {dropdown.map(([key, value]) => ( + setOpen(false)} + key={key} + > + {value} + + ))} + + + ); +} + +export type FilterProps = { + active: boolean; + to: LocationDescriptor; + icon?: React.ComponentType; + children: React.ReactNode; +}; +function Filter({ active, to, children, icon: Icon }: FilterProps) { + const classes = useStyles(); + + const content = ( + <> + {Icon && } +
{children}
+ + ); + + if (to) { + return ( + + {content} + + ); + } + + return ( +
+ {content} +
+ ); +} + +export default Filter; +export { parse, stringify, quote, FilterDropdown, Filter }; diff --git a/webui/src/pages/list/FilterToolbar.graphql b/webui/src/pages/list/FilterToolbar.graphql new file mode 100644 index 00000000..cd103f44 --- /dev/null +++ b/webui/src/pages/list/FilterToolbar.graphql @@ -0,0 +1,7 @@ +query BugCount($query: String) { + repository { + bugs: allBugs(query: $query) { + totalCount + } + } +} diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx new file mode 100644 index 00000000..825a9dee --- /dev/null +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -0,0 +1,129 @@ +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 { + FilterDropdown, + FilterProps, + Filter, + parse, + stringify, + Query, +} from './Filter'; +import { useBugCountQuery } from './FilterToolbar.generated'; + +const useStyles = makeStyles(theme => ({ + toolbar: { + backgroundColor: theme.palette.grey['100'], + borderColor: theme.palette.grey['300'], + borderWidth: '1px 0', + borderStyle: 'solid', + margin: theme.spacing(0, -1), + }, + spacer: { + flex: 1, + }, +})); + +// This prepends the filter text with a count +type CountingFilterProps = { + query: string; + children: React.ReactNode; +} & FilterProps; +function CountingFilter({ query, children, ...props }: CountingFilterProps) { + const { data, loading, error } = useBugCountQuery({ + variables: { query }, + }); + + var prefix; + if (loading) prefix = '...'; + else if (error || !data?.repository) prefix = '???'; + // TODO: better prefixes & error handling + else prefix = data.repository.bugs.totalCount; + + return ( + + {prefix} {children} + + ); +} + +type Props = { + query: string; + queryLocation: (query: string) => LocationDescriptor; +}; +function FilterToolbar({ query, queryLocation }: Props) { + const classes = useStyles(); + const params: Query = parse(query); + + const hasKey = (key: string): boolean => + params[key] && params[key].length > 0; + const hasValue = (key: string, value: string): boolean => + hasKey(key) && params[key].includes(value); + const loc = pipe(stringify, queryLocation); + const replaceParam = (key: string, value: string) => ( + params: Query + ): Query => ({ + ...params, + [key]: [value], + }); + const clearParam = (key: string) => (params: Query): Query => ({ + ...params, + [key]: [], + }); + + // TODO: author/label filters + return ( + + + open + + + closed + +
+ {/* + Author + Label + */} + hasValue('sort', key)} + to={key => pipe(replaceParam('sort', key), loc)(params)} + > + Sort + + + ); +} + +export default FilterToolbar; diff --git a/webui/src/pages/list/List.tsx b/webui/src/pages/list/List.tsx new file mode 100644 index 00000000..c1cae122 --- /dev/null +++ b/webui/src/pages/list/List.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import Table from '@material-ui/core/Table/Table'; +import TableBody from '@material-ui/core/TableBody/TableBody'; + +import BugRow from './BugRow'; +import { BugListFragment } from './ListQuery.generated'; + +type Props = { bugs: BugListFragment }; +function List({ bugs }: Props) { + return ( + + + {bugs.edges.map(({ cursor, node }) => ( + + ))} + +
+ ); +} + +export default List; diff --git a/webui/src/pages/list/ListQuery.graphql b/webui/src/pages/list/ListQuery.graphql new file mode 100644 index 00000000..ded60c8a --- /dev/null +++ b/webui/src/pages/list/ListQuery.graphql @@ -0,0 +1,37 @@ +#import "./BugRow.graphql" + +query ListBugs( + $first: Int + $last: Int + $after: String + $before: String + $query: String +) { + repository { + bugs: allBugs( + first: $first + last: $last + after: $after + before: $before + query: $query + ) { + ...BugList + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +} + +fragment BugList on BugConnection { + totalCount + edges { + cursor + node { + ...BugRow + } + } +} diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx new file mode 100644 index 00000000..6858b6c6 --- /dev/null +++ b/webui/src/pages/list/ListQuery.tsx @@ -0,0 +1,315 @@ +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'; +import { fade, makeStyles, Theme } from '@material-ui/core/styles'; +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 FilterToolbar from './FilterToolbar'; +import List from './List'; +import { useListBugsQuery } from './ListQuery.generated'; + +type StylesProps = { searching?: boolean }; +const useStyles = makeStyles(theme => ({ + main: { + maxWidth: 800, + margin: 'auto', + marginTop: theme.spacing(4), + marginBottom: theme.spacing(4), + overflow: 'hidden', + }, + pagination: { + ...theme.typography.overline, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + header: { + display: 'flex', + padding: theme.spacing(2), + '& > h1': { + ...theme.typography.h6, + margin: theme.spacing(0, 2), + }, + alignItems: 'center', + justifyContent: 'space-between', + }, + search: { + 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, 1), + width: ({ searching }) => (searching ? '20rem' : '15rem'), + transition: theme.transitions.create([ + 'width', + 'borderColor', + 'backgroundColor', + ]), + }, + searchFocused: { + borderColor: fade(theme.palette.primary.main, 0.4), + backgroundColor: theme.palette.background.paper, + width: '20rem!important', + }, + placeholderRow: { + padding: theme.spacing(1), + borderBottomColor: theme.palette.grey['300'], + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + display: 'flex', + alignItems: 'center', + }, + placeholderRowStatus: { + margin: theme.spacing(1, 2), + }, + placeholderRowText: { + flex: 1, + }, + message: { + ...theme.typography.h5, + padding: theme.spacing(8), + textAlign: 'center', + borderBottomColor: theme.palette.grey['300'], + borderBottomWidth: '1px', + borderBottomStyle: 'solid', + '& > p': { + margin: '0', + }, + }, + errorBox: { + color: theme.palette.error.main, + '& > pre': { + fontSize: '1rem', + textAlign: 'left', + backgroundColor: theme.palette.grey['900'], + color: theme.palette.common.white, + marginTop: theme.spacing(4), + padding: theme.spacing(2, 3), + }, + }, +})); + +function editParams( + params: URLSearchParams, + callback: (params: URLSearchParams) => void +) { + const cloned = new URLSearchParams(params.toString()); + callback(cloned); + return cloned; +} + +// TODO: factor this out +type PlaceholderProps = { count: number }; +const Placeholder: React.FC = ({ + count, +}: PlaceholderProps) => { + const classes = useStyles({}); + return ( + <> + {new Array(count).fill(null).map((_, i) => ( +
+ +
+ + +
+
+ ))} + + ); +}; + +// TODO: factor this out +const NoBug = () => { + const classes = useStyles({}); + return ( +
+ +

No results matched your search.

+
+ ); +}; + +type ErrorProps = { error: ApolloError }; +const Error: React.FC = ({ error }: ErrorProps) => { + const classes = useStyles({}); + return ( +
+ +

There was an error while fetching bug.

+

+ {error.message} +

+
+        {JSON.stringify(error, null, 2)}
+      
+
+ ); +}; + +function ListQuery() { + const location = useLocation(); + const history = useHistory(); + const params = new URLSearchParams(location.search); + const query = params.get('q') || ''; + + const [input, setInput] = useState(query); + + const classes = useStyles({ searching: !!input }); + + // TODO is this the right way to do it? + const lastQuery = useRef(null); + useEffect(() => { + if (query !== lastQuery.current) { + setInput(query); + } + lastQuery.current = query; + }, [query, input, lastQuery]); + + const num = (param: string | null) => (param ? parseInt(param) : null); + const page = { + first: num(params.get('first')), + last: num(params.get('last')), + after: params.get('after'), + before: params.get('before'), + }; + + // If nothing set, show the first 10 items + if (!page.first && !page.last) { + page.first = 10; + } + + const perPage = (page.first || page.last || 10).toString(); + + const { loading, error, data } = useListBugsQuery({ + variables: { + ...page, + query, + }, + }); + + let nextPage = null; + let previousPage = null; + let count = 0; + if (!loading && !error && data?.repository?.bugs) { + const bugs = data.repository.bugs; + count = bugs.totalCount; + // This computes the URL for the next page + if (bugs.pageInfo.hasNextPage) { + nextPage = { + ...location, + search: editParams(params, p => { + p.delete('last'); + p.delete('before'); + p.set('first', perPage); + p.set('after', bugs.pageInfo.endCursor); + }).toString(), + }; + } + // and this for the previous page + if (bugs.pageInfo.hasPreviousPage) { + previousPage = { + ...location, + search: editParams(params, p => { + p.delete('first'); + p.delete('after'); + p.set('last', perPage); + p.set('before', bugs.pageInfo.startCursor); + }).toString(), + }; + } + } + + // Prepare params without paging for editing filters + const paramsWithoutPaging = editParams(params, p => { + p.delete('first'); + p.delete('last'); + p.delete('before'); + p.delete('after'); + }); + // Returns a new location with the `q` param edited + const queryLocation = (query: string) => ({ + ...location, + search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(), + }); + + let content; + if (loading) { + content = ; + } else if (error) { + content = ; + } else if (data?.repository) { + const bugs = data.repository.bugs; + + if (bugs.totalCount === 0) { + content = ; + } else { + content = ; + } + } + + const formSubmit = (e: React.FormEvent) => { + e.preventDefault(); + history.push(queryLocation(input)); + }; + + return ( + +
+

Issues

+
+ setInput(e.target.value)} + classes={{ + root: classes.search, + focused: classes.searchFocused, + }} + /> + + +
+ + {content} +
+ {previousPage ? ( + + + + ) : ( + + + + )} +
{loading ? 'Loading' : `Total: ${count}`}
+ {nextPage ? ( + + + + ) : ( + + + + )} +
+
+ ); +} + +export default ListQuery; 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" + ] } -- cgit From d052ecf67105b5a65511a335e4c3112c74a662a6 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 16 Feb 2020 01:35:13 +0100 Subject: webui: in the bug list, toggle open and close when clicking --- webui/src/pages/list/Filter.tsx | 2 +- webui/src/pages/list/FilterToolbar.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'webui') diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx index 1a3cdd6b..0635e7f0 100644 --- a/webui/src/pages/list/Filter.tsx +++ b/webui/src/pages/list/Filter.tsx @@ -154,7 +154,7 @@ function FilterDropdown({ export type FilterProps = { active: boolean; - to: LocationDescriptor; + to: LocationDescriptor; // the target on click icon?: React.ComponentType; children: React.ReactNode; }; diff --git a/webui/src/pages/list/FilterToolbar.tsx b/webui/src/pages/list/FilterToolbar.tsx index 825a9dee..c568a9dd 100644 --- a/webui/src/pages/list/FilterToolbar.tsx +++ b/webui/src/pages/list/FilterToolbar.tsx @@ -32,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) { @@ -72,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]: [], @@ -87,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 @@ -99,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 -- cgit From c4f5cae4a44330ae0a8fb063768c4181fd0e83c1 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 16 Feb 2020 01:35:51 +0100 Subject: webui: list by default only open bugs --- webui/src/pages/list/ListQuery.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'webui') diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 6858b6c6..8cf68693 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -164,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.get('q') || 'status:open'; const [input, setInput] = useState(query); -- cgit From 602f91148b853b781d38506cbaadce011972da1d Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 16 Feb 2020 01:43:33 +0100 Subject: webui: fix missing space in the bug preview --- webui/src/pages/list/BugRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'webui') diff --git a/webui/src/pages/list/BugRow.tsx b/webui/src/pages/list/BugRow.tsx index 829877ef..9c1883a3 100644 --- a/webui/src/pages/list/BugRow.tsx +++ b/webui/src/pages/list/BugRow.tsx @@ -100,9 +100,9 @@ function BugRow({ bug }: Props) {
- {bug.humanId} opened + {bug.humanId} opened  - by {bug.author.displayName} +  by {bug.author.displayName}
-- cgit From e408ca8a2851d44ae105bf4e226b05eff609950e Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 16 Feb 2020 02:04:57 +0100 Subject: webui: minor styling of the timeline events --- webui/src/pages/bug/LabelChange.tsx | 2 +- webui/src/pages/bug/SetStatus.tsx | 2 +- webui/src/pages/bug/SetTitle.tsx | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) (limited to 'webui') diff --git a/webui/src/pages/bug/LabelChange.tsx b/webui/src/pages/bug/LabelChange.tsx index 764947ee..93fa8a32 100644 --- a/webui/src/pages/bug/LabelChange.tsx +++ b/webui/src/pages/bug/LabelChange.tsx @@ -10,7 +10,7 @@ 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/pages/bug/SetStatus.tsx b/webui/src/pages/bug/SetStatus.tsx index 251abf69..413f764d 100644 --- a/webui/src/pages/bug/SetStatus.tsx +++ b/webui/src/pages/bug/SetStatus.tsx @@ -9,7 +9,7 @@ import { SetStatusFragment } from './SetStatusFragment.generated'; const useStyles = makeStyles(theme => ({ main: { - ...theme.typography.body1, + ...theme.typography.body2, marginLeft: theme.spacing(1) + 40, }, })); diff --git a/webui/src/pages/bug/SetTitle.tsx b/webui/src/pages/bug/SetTitle.tsx index 304fd2e2..64b97517 100644 --- a/webui/src/pages/bug/SetTitle.tsx +++ b/webui/src/pages/bug/SetTitle.tsx @@ -9,10 +9,17 @@ 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', }, })); @@ -25,11 +32,11 @@ function SetTitle({ op }: Props) { const classes = useStyles(); return (
- + changed the title from - {op.was} + {op.was} to - {op.title} + {op.title} 
); -- cgit From 86a35f182975167ffe836f6d5f63260828dbca58 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 16 Feb 2020 02:51:27 +0100 Subject: webui: more styling on the bug page --- webui/src/pages/bug/Bug.tsx | 21 +++++++++++++++++---- webui/src/pages/bug/Message.tsx | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) (limited to 'webui') diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx index 998c9528..3c4bb63b 100644 --- a/webui/src/pages/bug/Bug.tsx +++ b/webui/src/pages/bug/Bug.tsx @@ -13,7 +13,7 @@ import TimelineQuery from './TimelineQuery'; const useStyles = makeStyles(theme => ({ main: { - maxWidth: 800, + maxWidth: 1000, margin: 'auto', marginTop: theme.spacing(4), }, @@ -41,6 +41,9 @@ const useStyles = makeStyles(theme => ({ marginTop: theme.spacing(2), flex: '0 0 200px', }, + sidebarTitle: { + fontWeight: 'bold', + }, labelList: { listStyle: 'none', padding: 0, @@ -53,6 +56,12 @@ const useStyles = makeStyles(theme => ({ display: 'block', }, }, + noLabel: { + ...theme.typography.body2, + }, + commentForm: { + marginLeft: 48, + }, })); type Props = { @@ -77,10 +86,16 @@ function Bug({ bug }: Props) {
+
+ +
- Labels + Labels
    + {bug.labels.length === 0 && ( + None yet + )} {bug.labels.map(l => (
- - ); } diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index ebb42f6b..4a438b77 100644 --- a/webui/src/pages/bug/Message.tsx +++ b/webui/src/pages/bug/Message.tsx @@ -31,6 +31,7 @@ const useStyles = makeStyles(theme => ({ padding: '0.5rem 1rem', borderBottom: '1px solid #ddd', display: 'flex', + backgroundColor: '#e2f1ff', }, title: { flex: 1, -- cgit From 14e91cb5edb8ab73dbb7c4cf3ae72e41aafdec71 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 17 Feb 2020 00:15:40 +0100 Subject: webui: fix the default query --- webui/src/pages/list/ListQuery.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'webui') diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index 8cf68693..a0db0348 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -164,7 +164,7 @@ function ListQuery() { const location = useLocation(); const history = useHistory(); const params = new URLSearchParams(location.search); - const query = params.get('q') || 'status:open'; + const query = params.has('q') ? (params.get('q') || '') : 'status:open'; const [input, setInput] = useState(query); -- cgit From afd22acd5ddb6c92e77b7660ef7ee65f4192d7ae Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 17 Feb 2020 00:16:07 +0100 Subject: webui: more readable dates, also localized --- webui/src/components/Date.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'webui') diff --git a/webui/src/components/Date.tsx b/webui/src/components/Date.tsx index be0f5835..8438b801 100644 --- a/webui/src/components/Date.tsx +++ b/webui/src/components/Date.tsx @@ -10,8 +10,8 @@ const WEEK = 7 * DAY; type Props = { date: string }; const Date = ({ date }: Props) => ( - - + + on ); -- cgit From 218d460590a29fe84b94c024712a095e5f82f917 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 17 Feb 2020 13:17:44 +0100 Subject: webui: style SetStatus --- webui/src/pages/bug/SetStatus.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'webui') diff --git a/webui/src/pages/bug/SetStatus.tsx b/webui/src/pages/bug/SetStatus.tsx index 413f764d..56fde6f9 100644 --- a/webui/src/pages/bug/SetStatus.tsx +++ b/webui/src/pages/bug/SetStatus.tsx @@ -6,12 +6,16 @@ import Author from 'src/components/Author'; import Date from 'src/components/Date'; import { SetStatusFragment } from './SetStatusFragment.generated'; +import { Status } from '../../gqlTypes' const useStyles = makeStyles(theme => ({ main: { ...theme.typography.body2, marginLeft: theme.spacing(1) + 40, }, + author: { + fontWeight: 'bold', + }, })); type Props = { @@ -20,10 +24,12 @@ type Props = { function SetStatus({ op }: Props) { const classes = useStyles(); + const status = { [Status.Open]: 'reopened', [Status.Closed]: 'closed' }[op.status] + return (
- - {op.status.toLowerCase()} this + + {status} this
); -- cgit From f96484391ae817a18f503b5c31cd3bd2211553df Mon Sep 17 00:00:00 2001 From: ludovicm67 Date: Sat, 22 Feb 2020 19:25:37 +0100 Subject: webui: run linter fix --- webui/src/components/Date.tsx | 4 +++- webui/src/pages/bug/SetStatus.tsx | 6 ++++-- webui/src/pages/list/ListQuery.tsx | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) (limited to 'webui') diff --git a/webui/src/components/Date.tsx b/webui/src/components/Date.tsx index 8438b801..146a3496 100644 --- a/webui/src/components/Date.tsx +++ b/webui/src/components/Date.tsx @@ -11,7 +11,9 @@ const WEEK = 7 * DAY; type Props = { date: string }; const Date = ({ date }: Props) => ( - on + + on + ); diff --git a/webui/src/pages/bug/SetStatus.tsx b/webui/src/pages/bug/SetStatus.tsx index 56fde6f9..e9674424 100644 --- a/webui/src/pages/bug/SetStatus.tsx +++ b/webui/src/pages/bug/SetStatus.tsx @@ -2,11 +2,11 @@ import React from 'react'; 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'; -import { Status } from '../../gqlTypes' const useStyles = makeStyles(theme => ({ main: { @@ -24,7 +24,9 @@ type Props = { function SetStatus({ op }: Props) { const classes = useStyles(); - const status = { [Status.Open]: 'reopened', [Status.Closed]: 'closed' }[op.status] + const status = { [Status.Open]: 'reopened', [Status.Closed]: 'closed' }[ + op.status + ]; return (
diff --git a/webui/src/pages/list/ListQuery.tsx b/webui/src/pages/list/ListQuery.tsx index a0db0348..2d8c698a 100644 --- a/webui/src/pages/list/ListQuery.tsx +++ b/webui/src/pages/list/ListQuery.tsx @@ -164,7 +164,7 @@ function ListQuery() { const location = useLocation(); const history = useHistory(); const params = new URLSearchParams(location.search); - const query = params.has('q') ? (params.get('q') || '') : 'status:open'; + const query = params.has('q') ? params.get('q') || '' : 'status:open'; const [input, setInput] = useState(query); -- cgit