Add basic Team Management
This commit is contained in:
1 parent
5b90eb8923
commit
f2aae81f6e
10 files changed
+487
-35
No files matched your search
@@ -0,0 +1,33 @@
|
||||
const gql = require('graphql-tag')
|
||||
|
||||
const TEAMS_CONNECTIONS_FRAGMENT = gql`{
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
slug
|
||||
owner
|
||||
status {
|
||||
status
|
||||
reason
|
||||
createdAt
|
||||
}
|
||||
createdAt
|
||||
profile {
|
||||
avatar
|
||||
description
|
||||
motto
|
||||
updatedAt
|
||||
}
|
||||
members {
|
||||
role
|
||||
member {
|
||||
alias
|
||||
authId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
module.exports = TEAMS_CONNECTIONS_FRAGMENT
|
||||
+9
-30
@@ -14,6 +14,7 @@ const { validateIdToken } = require('./auth/validateIdToken')
|
||||
const { directiveResolvers } = require('./auth/authDirectives')
|
||||
|
||||
const TEAMS_FRAGMENT = require('./graphql/TeamsFragment')
|
||||
const TEAMS_CONNECTIONS_FRAGMENT = require('./graphql/TeamsConnectionsFragment')
|
||||
|
||||
const createMember = async function (ctx, info, idToken) {
|
||||
console.log('createMember', idToken)
|
||||
@@ -47,36 +48,7 @@ const resolvers = {
|
||||
return ctx.db.query.member({ where: { authId } }, info)
|
||||
},
|
||||
teams(parent, args, ctx, info) {
|
||||
return ctx.db.query.teamsConnection({},
|
||||
`{
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
slug
|
||||
owner
|
||||
status {
|
||||
status
|
||||
reason
|
||||
createdAt
|
||||
}
|
||||
createdAt
|
||||
profile {
|
||||
avatar
|
||||
description
|
||||
motto
|
||||
updatedAt
|
||||
}
|
||||
members {
|
||||
role
|
||||
member {
|
||||
alias
|
||||
authId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
return ctx.db.query.teamsConnection({}, TEAMS_CONNECTIONS_FRAGMENT)
|
||||
},
|
||||
async teamById(parent, { id }, ctx, info) {
|
||||
return ctx.db.query.team({ where: { id } }, info)
|
||||
@@ -128,6 +100,13 @@ const resolvers = {
|
||||
return err
|
||||
})
|
||||
},
|
||||
async updateTeamProfile(parent, { slug, owner, description, motto, avatar }, ctx, info) {
|
||||
return ctx.db.mutation.updateTeam({data:{profile: {update: {description: description, motto: motto, avatar: avatar}}}, where:{slug: slug}}), TEAMS_FRAGMENT)
|
||||
.catch((err) => {
|
||||
console.log('createTeam error: ', err)
|
||||
return err
|
||||
})
|
||||
},
|
||||
async deleteTeam(parent, { id }, ctx, info) {
|
||||
return ctx.db.mutation.deleteTeam({ where: { id } }, info)
|
||||
.catch((res) => {
|
||||
|
||||
@@ -18,7 +18,8 @@ type Query {
|
||||
|
||||
type Mutation {
|
||||
authenticate(idToken: String!): Member
|
||||
createTeam(name: String!, slug: String, owner: String): Team @isAuthenticated
|
||||
createTeam(name: String!, slug: String!, owner: String!): Team @isAuthenticated
|
||||
updateTeamProfile(slug: String!, owner: String!, description: String, motto: String, avatar: String): Team @isAuthenticated
|
||||
deleteTeam(id: ID!): Team @isAuthenticated @isOwner(type: "Team")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const CREATE_TEAM = gql`
|
||||
mutation UpdateTeamMutation($slug: String!, $owner: String!, $description: String, $motto: String) {
|
||||
createTeam(slug: $slug, owner: $owner, description: $description, motto: $motto) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
owner
|
||||
status {
|
||||
status
|
||||
reason
|
||||
createdAt
|
||||
}
|
||||
createdAt
|
||||
profile {
|
||||
avatar
|
||||
description
|
||||
motto
|
||||
updatedAt
|
||||
}
|
||||
members {
|
||||
role
|
||||
member {
|
||||
alias
|
||||
authId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default CREATE_TEAM
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
|
||||
import TeamTable from './components/TeamTable'
|
||||
import ManageTeamsList from './components/ManageTeamsList'
|
||||
|
||||
const styles = theme => ({
|
||||
tableContainer: {
|
||||
@@ -15,10 +15,10 @@ const styles = theme => ({
|
||||
const ManageTeams = ({ classes }) => (
|
||||
<div>
|
||||
<Typography variant='display1' gutterBottom>
|
||||
Teams
|
||||
Manage Your Teams
|
||||
</Typography>
|
||||
<div className={classes.tableContainer}>
|
||||
<TeamTable />
|
||||
<ManageTeamsList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import AddIcon from '@material-ui/icons/Add'
|
||||
import TextField from '@material-ui/core/TextField'
|
||||
import Dialog from '@material-ui/core/Dialog'
|
||||
import DialogActions from '@material-ui/core/DialogActions'
|
||||
import DialogContent from '@material-ui/core/DialogContent'
|
||||
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
|
||||
import { getItem } from '../../../utils/local-storage'
|
||||
|
||||
export class ManageTeamEdit extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.handleClickOpen = this.handleClickOpen.bind(this)
|
||||
this.handleClose = this.handleClose.bind(this)
|
||||
this.handlePost = this.handlePost.bind(this)
|
||||
this.slugify = this.slugify.bind(this)
|
||||
this.handleChange = this.handleChange.bind(this)
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
motto: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOpen () {
|
||||
this.setState({ open: true })
|
||||
}
|
||||
|
||||
handleClose () {
|
||||
this.setState({ open: false })
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.setState({ name: e.target.value })
|
||||
}
|
||||
|
||||
render () {
|
||||
console.log(this.props, this.props.team)
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant='extendedFab'
|
||||
aria-label='AddTeam'
|
||||
className={this.props.classes.button}
|
||||
onClick={this.handleClickOpen}
|
||||
>
|
||||
<AddIcon className={this.props.classes.extendedIcon} />
|
||||
<Typography variant='subheading'>Create A Team</Typography>
|
||||
</Button>
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
onClose={this.handleClose}
|
||||
aria-labelledby='form-dialog-title'
|
||||
>
|
||||
<DialogTitle id='form-dialog-title'>Edit Team Details</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant='subheading'>Team Creation</Typography>
|
||||
<DialogContentText>Team Motto:</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin='dense'
|
||||
id='teammotto'
|
||||
label='Team Motto'
|
||||
type='text'
|
||||
fullWidth
|
||||
value={this.state.mottoe}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleClose} color='primary'>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={this.handlePost} color='primary'>
|
||||
Create Team
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
slugify (string) {
|
||||
const a = 'àáäâãåèéëêìíïîòóöôùúüûñçßÿœæŕśńṕẃǵǹḿǘẍźḧ·/_,:;'
|
||||
const b = 'aaaaaaeeeeiiiioooouuuuncsyoarsnpwgnmuxzh------'
|
||||
const p = new RegExp(a.split('').join('|'), 'g')
|
||||
|
||||
return string
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace spaces with -
|
||||
.replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
|
||||
.replace(/&/g, '-and-') // Replace & with 'and'
|
||||
.replace(/[^\w-]+/g, '') // Remove all non-word characters
|
||||
.replace(/--+/g, '-') // Replace multiple - with single -
|
||||
.replace(/^-+/, '') // Trim - from start of text
|
||||
.replace(/-+$/, '') // Trim - from end of text
|
||||
}
|
||||
|
||||
async handlePost (e) {
|
||||
e.preventDefault()
|
||||
const { name } = this.state
|
||||
const slug = this.slugify(name)
|
||||
const currentUser = await getItem('user')
|
||||
let authId = JSON.parse(currentUser)
|
||||
authId = authId.sub
|
||||
console.log('createPage cu:', JSON.parse(currentUser))
|
||||
await this.props.createTeamMutation({
|
||||
variables: {
|
||||
name,
|
||||
slug,
|
||||
owner: authId
|
||||
}
|
||||
})
|
||||
this.setState({ open: false })
|
||||
}
|
||||
}
|
||||
|
||||
ManageTeamEdit.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
team: PropTypes.object.isRequired,
|
||||
createTeamMutation: PropTypes.function
|
||||
}
|
||||
|
||||
export default withRouter(ManageTeamEdit)
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexGrow: 1
|
||||
},
|
||||
'@global': {
|
||||
body: {
|
||||
backgroundColor: theme.palette.common.white
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
width: 'auto',
|
||||
marginLeft: theme.spacing.unit * 3,
|
||||
marginRight: theme.spacing.unit * 3,
|
||||
marginBottom: theme.spacing.unit * 6,
|
||||
[theme.breakpoints.up(900 + theme.spacing.unit * 3 * 2)]: {
|
||||
width: 900,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto'
|
||||
}
|
||||
},
|
||||
paper: {
|
||||
marginTop: theme.spacing.unit * 3,
|
||||
marginBottom: theme.spacing.unit * 3,
|
||||
padding: theme.spacing.unit * 2,
|
||||
[theme.breakpoints.up(600 + theme.spacing.unit * 3 * 2)]: {
|
||||
marginTop: theme.spacing.unit * 6,
|
||||
marginBottom: theme.spacing.unit * 6,
|
||||
padding: theme.spacing.unit * 3
|
||||
}
|
||||
},
|
||||
heroContent: {
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
padding: `${theme.spacing.unit * 8}px 0 ${theme.spacing.unit * 6}px`
|
||||
}
|
||||
})
|
||||
|
||||
const ManageTeamsDetails = ({ classes, team }) => (
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<main className={classes.layout}>
|
||||
<Paper className={classes.paper}>
|
||||
<div className={classes.heroContent}>
|
||||
<Link to='/teams'>← Back to all teams</Link>
|
||||
<Typography
|
||||
variant='display3'
|
||||
align='center'
|
||||
color='textPrimary'
|
||||
gutterBottom
|
||||
>
|
||||
{team.name}
|
||||
</Typography>
|
||||
<Typography variant='subheading' color='textSecondary'>
|
||||
{team.createdAt}
|
||||
</Typography>
|
||||
<Typography variant='subheading' paragraph gutterBottom>
|
||||
Members: {team.members.length}
|
||||
</Typography>
|
||||
<Typography variant='subheading' color='primary'>
|
||||
Team Admin:
|
||||
{
|
||||
team.members.map(member => {
|
||||
if (member.role === 'OWNER' || member.role === 'ADMIN') {
|
||||
return member.member.alias
|
||||
}
|
||||
})
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
</Paper>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
)
|
||||
ManageTeamsDetails.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
team: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ManageTeamsDetails)
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import Card from '@material-ui/core/Card'
|
||||
import CardActionArea from '@material-ui/core/CardActionArea'
|
||||
import CardActions from '@material-ui/core/CardActions'
|
||||
import CardContent from '@material-ui/core/CardContent'
|
||||
import CardMedia from '@material-ui/core/CardMedia'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export const ManageTeamsItem = ({ classes, team }) => (
|
||||
<Grid item sm={6} md={4} lg={3}>
|
||||
<Card className={classes.card}>
|
||||
<CardActionArea>
|
||||
<CardMedia
|
||||
className={classes.cardMedia}
|
||||
image='data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22288%22%20height%3D%22225%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20288%20225%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_164edaf95ee%20text%20%7B%20fill%3A%23eceeef%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A14pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_164edaf95ee%22%3E%3Crect%20width%3D%22288%22%20height%3D%22225%22%20fill%3D%22%2355595c%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%2296.32500076293945%22%20y%3D%22118.8%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E' // eslint-disable-line max-len
|
||||
title='Image title'
|
||||
/>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<Typography gutterBottom variant='headline' component='h2'>
|
||||
{team.name}
|
||||
</Typography>
|
||||
<Typography variant='caption' color='textSecondary'>
|
||||
{team.createdAt}
|
||||
</Typography>
|
||||
<Typography variant='caption' paragraph gutterBottom>
|
||||
Members: 1
|
||||
</Typography>
|
||||
<Typography variant='subheading' color='primary'>
|
||||
Now recruiting!
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<CardActions>
|
||||
<Link to={`/teams/${team.slug}`} >
|
||||
<Button size='small' color='primary' className={classes.buttonRight}>
|
||||
Details
|
||||
</Button>
|
||||
</Link>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
ManageTeamsItem.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
team: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
export default ManageTeamsItem
|
||||
@@ -0,0 +1,128 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Query } from 'react-apollo'
|
||||
|
||||
import Typography from '@material-ui/core/Typography'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
|
||||
import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery'
|
||||
|
||||
import { isEmpty } from '../../../utils/js-helpers'
|
||||
|
||||
import ManageTeamsItem from './ManageTeamsItem'
|
||||
import ManageTeamsDetails from './ManageTeamsDetails'
|
||||
|
||||
const styles = theme => ({
|
||||
heroContent: {
|
||||
maxWidth: 600,
|
||||
margin: '0 auto',
|
||||
padding: `${theme.spacing.unit * 8}px 0 ${theme.spacing.unit * 6}px`
|
||||
},
|
||||
cardGrid: {
|
||||
padding: `${theme.spacing.unit * 8}px 0`
|
||||
},
|
||||
card: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
cardMedia: {
|
||||
paddingTop: '56.25%' // 16:9
|
||||
},
|
||||
cardContent: {
|
||||
flexGrow: 1
|
||||
},
|
||||
buttonRight: {
|
||||
justifyContent: 'flex-end'
|
||||
}
|
||||
})
|
||||
|
||||
export const ManageTeamsList = ({ classes, match }) => (
|
||||
<Query query={GET_TEAMS_BY_OWNER} fetchPolicy='cache-and-network' >
|
||||
{({ loading, error, data }) => {
|
||||
console.log('GET_TEAMS_BY_OWNER: ', loading, error, data, match)
|
||||
let slug = null
|
||||
if (!isEmpty(match.params) && match.params.slug) slug = match.params.slug
|
||||
|
||||
let errors
|
||||
let queryLoader
|
||||
if (error) {
|
||||
errors = error.graphQLErrors.map(({ message }, i) => {
|
||||
return (
|
||||
<Typography key={i} variant='caption'>
|
||||
{message}
|
||||
</Typography>
|
||||
)
|
||||
})
|
||||
}
|
||||
if (!loading && !error) {
|
||||
if (data.teams.edges.length > 0) {
|
||||
if (slug !== null) {
|
||||
return (
|
||||
<Grid container spacing={40}>
|
||||
{!loading &&
|
||||
data.teams.edges.map(team => {
|
||||
if (team.node.slug === slug) {
|
||||
return (
|
||||
<ManageTeamsDetails
|
||||
key={team.node.id}
|
||||
team={team.node}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
{queryLoader}
|
||||
{errors}
|
||||
</Grid>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className={classes.heroContent}>
|
||||
<Typography
|
||||
variant='display3'
|
||||
align='center'
|
||||
color='textPrimary'
|
||||
gutterBottom
|
||||
>
|
||||
Teams
|
||||
</Typography>
|
||||
</div>
|
||||
<Grid container spacing={40}>
|
||||
{!loading &&
|
||||
data.teams.edges.map(team => (
|
||||
<ManageTeamsItem
|
||||
key={team.node.id}
|
||||
team={team.node}
|
||||
classes={classes}
|
||||
/>
|
||||
))}
|
||||
{queryLoader}
|
||||
{errors}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<Grid container spacing={40}>
|
||||
<Typography variant='subheading' gutterBottom>
|
||||
Loading...
|
||||
</Typography>
|
||||
{queryLoader}
|
||||
{errors}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
|
||||
ManageTeamsList.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
match: PropTypes.object
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ManageTeamsList)
|
||||
@@ -41,7 +41,7 @@ const styles = theme => ({
|
||||
export const TeamsList = ({ classes, match }) => (
|
||||
<Query query={GET_ALL_TEAMS_QUERY} fetchPolicy='cache-and-network' >
|
||||
{({ loading, error, data }) => {
|
||||
console.log('GET_TEAMS_BY_OWNER: ', loading, error, data, match)
|
||||
console.log('GET_ALL_TEAMS: ', loading, error, data, match)
|
||||
let slug = null
|
||||
if (!isEmpty(match.params) && match.params.slug) slug = match.params.slug
|
||||
|
||||
|
||||
Reference in new issue
Block a user