Use profile page for each identity

Sascha created

Authorcomponent links to the authors profile page.
Replace pofile buglist with statistics

Change summary

api/graphql/schema/repository.graphql                        |   4 
webui/src/App.tsx                                            |   4 
webui/src/components/Author.tsx                              |  12 
webui/src/components/BugTitleForm/BugTitleForm.tsx           |   6 
webui/src/components/CurrentIdentity/CurrentIdentity.graphql |  14 
webui/src/components/Header/Header.tsx                       |   2 
webui/src/components/Identity/CurrentIdentity.graphql        |   9 
webui/src/components/Identity/CurrentIdentity.tsx            |   4 
webui/src/components/Identity/IdentityFragment.graphql       |  10 
webui/src/components/Identity/UserIdentity.graphql           |   9 
webui/src/components/IfLoggedIn/IfLoggedIn.tsx               |   2 
webui/src/graphql/fragments.graphql                          |   1 
webui/src/pages/bug/LabelChange.tsx                          |   8 
webui/src/pages/bug/Message.tsx                              |   1 
webui/src/pages/bug/SetStatus.tsx                            |   8 
webui/src/pages/bug/SetTitle.tsx                             |   8 
webui/src/pages/identity/BugList.tsx                         |   4 
webui/src/pages/identity/GetUserStatistic.graphql            |  13 
webui/src/pages/identity/Identity.tsx                        | 207 +++--
webui/src/pages/identity/IdentityQuery.tsx                   |  24 
webui/src/pages/identity/index.tsx                           |   1 
webui/src/pages/list/BugRow.tsx                              |   4 
webui/src/pages/list/ListQuery.tsx                           |   2 
23 files changed, 222 insertions(+), 135 deletions(-)

Detailed changes

api/graphql/schema/repository.graphql 🔗

@@ -17,10 +17,6 @@ type Repository {
         query: String
     ): BugConnection!
 
-    allBugsDetail(
-        query: String
-    ): [Bug]!
-
     bug(prefix: String!): Bug
 
     """All the identities"""

webui/src/App.tsx 🔗

@@ -3,7 +3,7 @@ import { Route, Switch } from 'react-router';
 
 import Layout from './components/Header';
 import BugPage from './pages/bug';
-import IdentityPage from './pages/identity/Identity';
+import IdentityPage from './pages/identity';
 import ListPage from './pages/list';
 import NewBugPage from './pages/new/NewBugPage';
 import NotFoundPage from './pages/notfound/NotFoundPage';
@@ -15,7 +15,7 @@ export default function App() {
         <Route path="/" exact component={ListPage} />
         <Route path="/new" exact component={NewBugPage} />
         <Route path="/bug/:id" exact component={BugPage} />
-        <Route path="/user" exact component={IdentityPage} />
+        <Route path="/user/:id" exact component={IdentityPage} />
         <Route component={NotFoundPage} />
       </Switch>
     </Layout>

webui/src/components/Author.tsx 🔗

@@ -1,6 +1,8 @@
 import React from 'react';
+import { Link as RouterLink } from 'react-router-dom';
 
 import MAvatar from '@material-ui/core/Avatar';
+import Link from '@material-ui/core/Link';
 import Tooltip from '@material-ui/core/Tooltip/Tooltip';
 
 import { AuthoredFragment } from '../graphql/fragments.generated';
@@ -11,13 +13,11 @@ type Props = AuthoredFragment & {
 };
 
 const Author = ({ author, ...props }: Props) => {
-  if (!author.email) {
-    return <span {...props}>{author.displayName}</span>;
-  }
-
   return (
-    <Tooltip title={author.email}>
-      <span {...props}>{author.displayName}</span>
+    <Tooltip title={`Goto the ${author.displayName}'s profile.`}>
+      <Link {...props} component={RouterLink} to={`/user/${author.humanId}`}>
+        {author.displayName}
+      </Link>
     </Tooltip>
   );
 };

webui/src/components/BugTitleForm/BugTitleForm.tsx 🔗

@@ -52,6 +52,10 @@ const useStyles = makeStyles((theme) => ({
   saveButton: {
     marginRight: theme.spacing(1),
   },
+  author: {
+    fontWeight: 'bold',
+    color: theme.palette.text.secondary,
+  },
 }));
 
 interface Props {
@@ -182,7 +186,7 @@ function BugTitleForm({ bug }: Props) {
       {bugTitleEdition ? editableBugTitle() : readonlyBugTitle()}
       <div className="classes.headerSubtitle">
         <Typography color={'textSecondary'}>
-          <Author author={bug.author} />
+          <Author author={bug.author} className={classes.author} />
           {' opened this bug '}
           <Date date={bug.createdAt} />
         </Typography>

webui/src/components/Header/Header.tsx 🔗

@@ -8,7 +8,7 @@ import Toolbar from '@material-ui/core/Toolbar';
 import Tooltip from '@material-ui/core/Tooltip/Tooltip';
 import { makeStyles } from '@material-ui/core/styles';
 
-import CurrentIdentity from '../CurrentIdentity/CurrentIdentity';
+import CurrentIdentity from '../Identity/CurrentIdentity';
 import { LightSwitch } from '../Themer';
 
 const useStyles = makeStyles((theme) => ({

webui/src/components/CurrentIdentity/CurrentIdentity.tsx → webui/src/components/Identity/CurrentIdentity.tsx 🔗

@@ -1,4 +1,5 @@
 import React from 'react';
+import { Link as RouterLink } from 'react-router-dom';
 
 import {
   Button,
@@ -94,7 +95,8 @@ const CurrentIdentity = () => {
                     <Link
                       color="inherit"
                       className={classes.profileLink}
-                      href="/user"
+                      component={RouterLink}
+                      to={`/user/${user.humanId}`}
                     >
                       Open profile
                     </Link>

webui/src/components/IfLoggedIn/IfLoggedIn.tsx 🔗

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { useCurrentIdentityQuery } from '../CurrentIdentity/CurrentIdentity.generated';
+import { useCurrentIdentityQuery } from '../Identity/CurrentIdentity.generated';
 
 type Props = { children: () => React.ReactNode };
 const IfLoggedIn = ({ children }: Props) => {

webui/src/pages/bug/LabelChange.tsx 🔗

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { Typography } from '@material-ui/core';
 import { makeStyles } from '@material-ui/core/styles';
 
 import Author from 'src/components/Author';
@@ -10,11 +11,12 @@ import { LabelChangeFragment } from './LabelChangeFragment.generated';
 
 const useStyles = makeStyles((theme) => ({
   main: {
-    ...theme.typography.body2,
+    color: theme.palette.text.secondary,
     marginLeft: theme.spacing(1) + 40,
   },
   author: {
     fontWeight: 'bold',
+    color: theme.palette.text.secondary,
   },
   label: {
     maxWidth: '50ch',
@@ -31,7 +33,7 @@ function LabelChange({ op }: Props) {
   const { added, removed } = op;
   const classes = useStyles();
   return (
-    <div className={classes.main}>
+    <Typography className={classes.main}>
       <Author author={op.author} className={classes.author} />
       {added.length > 0 && <span> added the </span>}
       {added.map((label, index) => (
@@ -48,7 +50,7 @@ function LabelChange({ op }: Props) {
         {added.length + removed.length > 1 && 's'}{' '}
       </span>
       <Date date={op.date} />
-    </div>
+    </Typography>
   );
 }
 

webui/src/pages/bug/Message.tsx 🔗

@@ -21,6 +21,7 @@ import MessageHistoryDialog from './MessageHistoryDialog';
 const useStyles = makeStyles((theme) => ({
   author: {
     fontWeight: 'bold',
+    color: theme.palette.info.contrastText,
   },
   container: {
     display: 'flex',

webui/src/pages/bug/SetStatus.tsx 🔗

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { Typography } from '@material-ui/core';
 import { makeStyles } from '@material-ui/core/styles';
 
 import { Status } from '../../gqlTypes';
@@ -10,11 +11,12 @@ import { SetStatusFragment } from './SetStatusFragment.generated';
 
 const useStyles = makeStyles((theme) => ({
   main: {
-    ...theme.typography.body2,
+    color: theme.palette.text.secondary,
     marginLeft: theme.spacing(1) + 40,
   },
   author: {
     fontWeight: 'bold',
+    color: theme.palette.text.secondary,
   },
 }));
 
@@ -29,11 +31,11 @@ function SetStatus({ op }: Props) {
   ];
 
   return (
-    <div className={classes.main}>
+    <Typography className={classes.main}>
       <Author author={op.author} className={classes.author} />
       <span> {status} this </span>
       <Date date={op.date} />
-    </div>
+    </Typography>
   );
 }
 

webui/src/pages/bug/SetTitle.tsx 🔗

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { Typography } from '@material-ui/core';
 import { makeStyles } from '@material-ui/core/styles';
 
 import Author from 'src/components/Author';
@@ -9,11 +10,12 @@ import { SetTitleFragment } from './SetTitleFragment.generated';
 
 const useStyles = makeStyles((theme) => ({
   main: {
-    ...theme.typography.body2,
+    color: theme.palette.text.secondary,
     marginLeft: theme.spacing(1) + 40,
   },
   author: {
     fontWeight: 'bold',
+    color: theme.palette.text.secondary,
   },
   before: {
     fontWeight: 'bold',
@@ -31,14 +33,14 @@ type Props = {
 function SetTitle({ op }: Props) {
   const classes = useStyles();
   return (
-    <div className={classes.main}>
+    <Typography className={classes.main}>
       <Author author={op.author} className={classes.author} />
       <span> changed the title from </span>
       <span className={classes.before}>{op.was}</span>
       <span> to </span>
       <span className={classes.after}>{op.title}</span>&nbsp;
       <Date date={op.date} />
-    </div>
+    </Typography>
   );
 }
 

webui/src/pages/identity/BugList.tsx 🔗

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { Card, Link, Typography } from '@material-ui/core';
+import { Card, Divider, Link, Typography } from '@material-ui/core';
 import CircularProgress from '@material-ui/core/CircularProgress';
 import { makeStyles } from '@material-ui/core/styles';
 
@@ -53,6 +53,7 @@ function BugList({ humanId }: Props) {
                 {bug.title}
               </Link>
             </Typography>
+            <Divider />
             <Typography variant="subtitle2">
               Created&nbsp;
               <Date date={bug.createdAt} />
@@ -64,6 +65,7 @@ function BugList({ humanId }: Props) {
           </Card>
         );
       })}
+      {bugs?.length === 0 && <p>No authored bugs by this user found.</p>}
     </div>
   );
 }

webui/src/pages/identity/GetUserStatistic.graphql 🔗

@@ -0,0 +1,13 @@
+query GetUserStatistic($authorQuery: String!, $participantQuery: String!, $actionQuery: String!) {
+  repository {
+    authored: allBugs(query: $authorQuery) {
+      totalCount
+    },
+    participated: allBugs(query: $participantQuery) {
+      totalCount
+    }
+    actions: allBugs(query: $actionQuery) {
+      totalCount
+    }
+  }
+}

webui/src/pages/identity/Identity.tsx 🔗

@@ -1,124 +1,145 @@
 import React from 'react';
+import { Link as RouterLink } from 'react-router-dom';
 
-import {
-  Checkbox,
-  FormControlLabel,
-  Link,
-  Paper,
-  Typography,
-} from '@material-ui/core';
+import { Link, Paper, Typography } from '@material-ui/core';
 import Avatar from '@material-ui/core/Avatar';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import Grid from '@material-ui/core/Grid';
 import { makeStyles } from '@material-ui/core/styles';
 import InfoIcon from '@material-ui/icons/Info';
 import MailOutlineIcon from '@material-ui/icons/MailOutline';
 
-import { useCurrentIdentityQuery } from '../../components/CurrentIdentity/CurrentIdentity.generated';
+import { IdentityFragment } from '../../components/Identity/IdentityFragment.generated';
 
-import BugList from './BugList';
+import { useGetUserStatisticQuery } from './GetUserStatistic.generated';
 
 const useStyles = makeStyles((theme) => ({
   main: {
     maxWidth: 1000,
     margin: 'auto',
-    marginTop: theme.spacing(4),
-    padding: theme.spacing(3, 2),
-    display: 'flex',
-  },
-  container: {
-    display: 'flex',
-    marginBottom: theme.spacing(1),
-  },
-  leftSidebar: {
-    marginTop: theme.spacing(2),
-    flex: '0 0 200px',
+    marginTop: theme.spacing(3),
   },
   content: {
-    marginTop: theme.spacing(5),
-    padding: theme.spacing(3, 2),
-    minWidth: 800,
-    backgroundColor: theme.palette.background.paper,
-  },
-  rightSidebar: {
-    marginTop: theme.spacing(5),
-    flex: '0 0 200px',
+    padding: theme.spacing(0.5, 2, 2, 2),
+    wordWrap: 'break-word',
   },
   large: {
-    width: theme.spacing(20),
-    height: theme.spacing(20),
+    minWidth: 200,
+    minHeight: 200,
+    margin: 'auto',
+    maxWidth: '100%',
+    maxHeight: '100%',
   },
-  control: {
-    paddingBottom: theme.spacing(3),
+  heading: {
+    marginTop: theme.spacing(3),
   },
   header: {
     ...theme.typography.h4,
+    wordBreak: 'break-word',
+  },
+  infoIcon: {
+    verticalAlign: 'bottom',
   },
 }));
 
-const Identity = () => {
+type Props = {
+  identity: IdentityFragment;
+};
+const Identity = ({ identity }: Props) => {
   const classes = useStyles();
-  const { data } = useCurrentIdentityQuery();
-  const user = data?.repository?.userIdentity;
-  console.log(user);
+  const user = identity;
+
+  const { loading, error, data } = useGetUserStatisticQuery({
+    variables: {
+      authorQuery: 'author:' + user?.humanId,
+      participantQuery: 'participant:' + user?.humanId,
+      actionQuery: 'actor:' + user?.humanId,
+    },
+  });
+
+  if (loading) return <CircularProgress />;
+  if (error) return <p>Error: {error}</p>;
+  const statistic = data?.repository;
+  const authoredCount = statistic?.authored?.totalCount;
+  const participatedCount = statistic?.participated?.totalCount;
+  const actionCount = statistic?.actions?.totalCount;
 
   return (
     <main className={classes.main}>
-      <div className={classes.container}>
-        <div className={classes.leftSidebar}>
-          <h1 className={classes.header}>
-            {user?.displayName ? user?.displayName : 'none'}
-          </h1>
-          <Avatar
-            src={user?.avatarUrl ? user.avatarUrl : undefined}
-            className={classes.large}
-          >
-            {user?.displayName.charAt(0).toUpperCase()}
-          </Avatar>
-          <Typography variant="h5" component="h2">
-            Your account
-          </Typography>
-          <Typography variant="subtitle2" component="h2">
-            Name: {user?.name ? user?.name : '---'}
-          </Typography>
-          <Typography variant="subtitle2" component="h3">
-            Id (truncated): {user?.humanId ? user?.humanId : '---'}
-            <InfoIcon
-              fontSize={'small'}
-              titleAccess={user?.id ? user?.id : '---'}
-            />
-          </Typography>
-          <Typography variant="subtitle2" component="h3">
-            Login: {user?.login ? user?.login : '---'}
-          </Typography>
-          <Typography
-            variant="subtitle2"
-            component="h3"
-            style={{
-              display: 'flex',
-              alignItems: 'center',
-              flexWrap: 'wrap',
-            }}
-          >
-            <MailOutlineIcon />
-            <Link href={'mailto:' + user?.email} color={'inherit'}>
-              {user?.email ? user?.email : '---'}
-            </Link>
-          </Typography>
-          <FormControlLabel
-            className={classes.control}
-            label="Protected"
-            labelPlacement="end"
-            value={user?.isProtected}
-            control={<Checkbox color="secondary" indeterminate />}
-          />
-        </div>
-        <Paper className={classes.content}>
-          <Typography variant="h5" component="h2">
-            Bugs authored by {user?.displayName}
-          </Typography>
-          <BugList humanId={user?.humanId ? user?.humanId : ''} />
-        </Paper>
-        <div className={classes.rightSidebar}></div>
-      </div>
+      <Paper elevation={3} className={classes.content}>
+        <Grid spacing={2} container direction="row">
+          <Grid xs={12} sm={4} className={classes.heading} item>
+            <Avatar
+              src={user?.avatarUrl ? user.avatarUrl : undefined}
+              className={classes.large}
+            >
+              {user?.displayName.charAt(0).toUpperCase()}
+            </Avatar>
+          </Grid>
+          <Grid xs={12} sm={4} item>
+            <section>
+              <h1 className={classes.header}>{user?.name}</h1>
+              <Typography variant="subtitle1">
+                Name: {user?.displayName ? user?.displayName : '---'}
+              </Typography>
+              <Typography variant="subtitle1">
+                Id (truncated): {user?.humanId ? user?.humanId : '---'}
+                <InfoIcon
+                  titleAccess={user?.id ? user?.id : '---'}
+                  className={classes.infoIcon}
+                />
+              </Typography>
+              {user?.email && (
+                <Typography
+                  variant="subtitle1"
+                  style={{
+                    display: 'flex',
+                    alignItems: 'center',
+                    flexWrap: 'wrap',
+                  }}
+                >
+                  <MailOutlineIcon />
+                  <Link href={'mailto:' + user?.email} color={'inherit'}>
+                    {user?.email}
+                  </Link>
+                </Typography>
+              )}
+            </section>
+          </Grid>
+          <Grid xs={12} sm={4} item>
+            <section>
+              <h1 className={classes.header}>Statistics</h1>
+              <Link
+                component={RouterLink}
+                to={`/?q=author%3A${user?.humanId}+sort%3Acreation`}
+                color={'inherit'}
+              >
+                <Typography variant="subtitle1">
+                  Created {authoredCount} bugs.
+                </Typography>
+              </Link>
+              <Link
+                component={RouterLink}
+                to={`/?q=participant%3A${user?.humanId}+sort%3Acreation`}
+                color={'inherit'}
+              >
+                <Typography variant="subtitle1">
+                  Participated to {participatedCount} bugs.
+                </Typography>
+              </Link>
+              <Link
+                component={RouterLink}
+                to={`/?q=actor%3A${user?.humanId}+sort%3Acreation`}
+                color={'inherit'}
+              >
+                <Typography variant="subtitle1">
+                  Interacted with {actionCount} bugs.
+                </Typography>
+              </Link>
+            </section>
+          </Grid>
+        </Grid>
+      </Paper>
     </main>
   );
 };

webui/src/pages/identity/IdentityQuery.tsx 🔗

@@ -0,0 +1,24 @@
+import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+
+import CircularProgress from '@material-ui/core/CircularProgress';
+
+import { useGetUserByIdQuery } from '../../components/Identity/UserIdentity.generated';
+
+import Identity from './Identity';
+
+type Props = RouteComponentProps<{
+  id: string;
+}>;
+
+const UserQuery: React.FC<Props> = ({ match }: Props) => {
+  const { loading, error, data } = useGetUserByIdQuery({
+    variables: { userId: match.params.id },
+  });
+  if (loading) return <CircularProgress />;
+  if (error) return <p>Error: {error}</p>;
+  if (!data?.repository?.identity) return <p>404.</p>;
+  return <Identity identity={data.repository.identity} />;
+};
+
+export default UserQuery;

webui/src/pages/list/BugRow.tsx 🔗

@@ -9,6 +9,7 @@ import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
 import CommentOutlinedIcon from '@material-ui/icons/CommentOutlined';
 import ErrorOutline from '@material-ui/icons/ErrorOutline';
 
+import Author from 'src/components/Author';
 import Date from 'src/components/Date';
 import Label from 'src/components/Label';
 import { Status } from 'src/gqlTypes';
@@ -117,7 +118,8 @@ function BugRow({ bug }: Props) {
           <div className={classes.details}>
             {bug.humanId} opened&nbsp;
             <Date date={bug.createdAt} />
-            &nbsp;by {bug.author.displayName}
+            &nbsp;by&nbsp;
+            <Author className={classes.details} author={bug.author} />
           </div>
         </div>
         <span className={classes.commentCountCell}>

webui/src/pages/list/ListQuery.tsx 🔗

@@ -14,7 +14,7 @@ import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
 import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
 import Skeleton from '@material-ui/lab/Skeleton';
 
-import { useCurrentIdentityQuery } from '../../components/CurrentIdentity/CurrentIdentity.generated';
+import { useCurrentIdentityQuery } from '../../components/Identity/CurrentIdentity.generated';
 import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn';
 
 import { parse, Query, stringify } from './Filter';