diff --git a/api/src/auth/authDirectives.js b/api/src/auth/authDirectives.js index 471be4692..0ba153fce 100644 --- a/api/src/auth/authDirectives.js +++ b/api/src/auth/authDirectives.js @@ -71,8 +71,8 @@ const isLoggedIn = async ctx => { const isRequestingMemberAlsoOwner = ({ ctx, memberId, type, typeId }) => - ctx.db.exists[type]({ id: typeId, owner: { auth0id: memberId } }) -const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ auth0id: memberId }) + ctx.db.exists[type]({ slug: typeId, owner: memberId }) +const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ authId: memberId }) const directiveResolvers = { isAuthenticated: async (next, source, args, ctx) => { @@ -89,12 +89,14 @@ const directiveResolvers = { throw new Error(`Unauthorized, incorrect role`) }, isOwner: async (next, source, { type }, ctx) => { - const { id: typeId } = + console.log('directive isOwner: ', source, type, ctx) + const { slug: typeId } = source && source.id ? source : ctx.request.body.variables ? ctx.request.body.variables : { id: null } - const { auth0id: memberId } = await isLoggedIn(ctx) - console.log('directive isOwner 0: ', type, typeId, memberId) + const user = await isLoggedIn(ctx) + console.log('directive isOwner 0: ', typeId, user.sub) + const memberId = user.sub const isOwner = type === `Member` ? memberId === typeId @@ -106,14 +108,14 @@ const directiveResolvers = { throw new Error(`Unauthorized, must be owner`) }, isOwnerOrHasRole: async (next, source, { roles, type }, ctx, ...p) => { - const { auth0id: memberId } = await isLoggedIn(ctx) + const { authId: memberId } = await isLoggedIn(ctx) console.log('directive isOwnerOrHasRole 1: ', memberId, roles, type) if(memberId === undefined && role=== undefined) throw new Error(`Not logged in`) if (roles.includes(role)) { return next() } - const { auth0id: typeId } = ctx.request.body.variables + const { authId: typeId } = ctx.request.body.variables const isOwner = await isRequestingMemberAlsoOwner({ ctx, memberId, diff --git a/client/src/graphql/teams/DeleteTeamMutation.js b/client/src/graphql/teams/DeleteTeamMutation.js index 998e5c643..8e349c58a 100644 --- a/client/src/graphql/teams/DeleteTeamMutation.js +++ b/client/src/graphql/teams/DeleteTeamMutation.js @@ -2,8 +2,8 @@ import gql from 'graphql-tag' export const DELETE_TEAM = gql` - mutation deleteTeam($id: ID!) { - deleteTeam(id: $id) { + mutation deleteTeam($slug: String!) { + deleteTeam(slug: $slug) { id } } diff --git a/client/src/views/common/MessageCard.js b/client/src/views/common/MessageCard.js new file mode 100644 index 000000000..60668fd62 --- /dev/null +++ b/client/src/views/common/MessageCard.js @@ -0,0 +1,43 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import Typography from '@material-ui/core/Typography' +import { withStyles } from '@material-ui/core/styles' + +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' + +const styles = theme => ({ + card: { + display: 'flex', + width: '100%', + margin: '1.5em', + padding: '3em' + }, + cardDetails: { + display: 'flex', + flex: 1 + }, + cardContent: { + flex: 1 + } +}) + +export const MessageCard = ({ classes, message, children }) => ( + +
+ + {message} + {children} + +
+
+) + +MessageCard.propTypes = { + children: PropTypes.node, + classes: PropTypes.object.isRequired, + message: PropTypes.string.isRequired +} + +export default withStyles(styles)(MessageCard) diff --git a/client/src/views/common/index.js b/client/src/views/common/index.js new file mode 100644 index 000000000..fe07e8f49 --- /dev/null +++ b/client/src/views/common/index.js @@ -0,0 +1,3 @@ +import MessageCard from './MessageCard' + +export { MessageCard } diff --git a/client/src/views/dashboard/Overview.js b/client/src/views/dashboard/Overview.js index c79b2eec9..87aa217bf 100644 --- a/client/src/views/dashboard/Overview.js +++ b/client/src/views/dashboard/Overview.js @@ -34,6 +34,13 @@ const styles = theme => ({ aawebMedia: { width: '100%', height: 320 + }, + dashLink: { + float: 'right', + color: '#111', + fontSize: 16, + textDecoration: 'none', + paddingRight: '1em' } }) diff --git a/client/src/views/dashboard/components/CreateTeamDialog.js b/client/src/views/dashboard/components/CreateTeamDialog.js new file mode 100644 index 000000000..66e0f00e5 --- /dev/null +++ b/client/src/views/dashboard/components/CreateTeamDialog.js @@ -0,0 +1,196 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { Mutation } from 'react-apollo' +import { withStyles } from '@material-ui/core/styles' + +import Button from '@material-ui/core/Button' +import AddIcon from '@material-ui/icons/Add' +import TextField from '@material-ui/core/TextField' +import FormControl from '@material-ui/core/FormControl' +import FormHelperText from '@material-ui/core/FormHelperText' +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' +import { isEmpty } from '../../../utils/js-helpers' + +import CREATE_TEAM from '../../../graphql/teams/CreateTeamMutation' +import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery' + +// import { checkGraphQLError } from '../../../utils/graphql-errors' + +const styles = theme => ({ + dialogStyle: { + padding: '3em' + }, + buttonRight: { + // position: 'absolute', + // right: '3em' + marginTop: '1em', + marginRight: 'auto', + marginLeft: 'auto', + display: 'block', + lineHeight: '1em' + } +}) + +export class CreateTeamDialog extends Component { + constructor (props) { + super(props) + + this.handleClickOpen = this.handleClickOpen.bind(this) + this.handleClose = this.handleClose.bind(this) + this.handleChange = this.handleChange.bind(this) + this.handleSubmit = this.handleSubmit.bind(this) + this.validate = this.validate.bind(this) + this.slugify = this.slugify.bind(this) + + this.state = { + open: false, + name: '', + errors: { + name: '' + } + } + } + + render () { + const { classes, authId } = this.props + return ( + + {(createTeam, { loading, error, data }) => { + let errors + let loader = null + if (loading) { + loader = Submitting team... + } + if (error) { + errors = error.graphQLErrors.map(({ message }, i) => { + // const displayMessage = checkGraphQLError(message) + return ( + + {message} + + ) + }) + } + return ( +
+ + + Create a Team + + Team Name: + (this.state.errors.name !== '' || error)}> + this.state.errors.name !== '' || error} + /> + {this.state.errors.name !== '' && ({this.state.errors.name})} + {error && ({errors})} + {loader} + + + + + + + +
+ ) + }} +
+ ) + } + + handleClickOpen () { + this.setState({ open: true }) + } + + handleClose () { + this.setState({ open: false }) + } + + handleChange (e) { + const errors = this.validate(e.target.value) + if (isEmpty(errors)) { + this.setState({ errors: { name: '' } }) + this.setState({ name: e.target.value }) + } else { + this.setState({ errors: errors }) + this.setState({ name: e.target.value }) + } + } + + async handleSubmit (e, createTeam, name) { + e.preventDefault() + const currentUser = await getItem('user') + let authId = JSON.parse(currentUser) + authId = authId.authId + console.log('createTeam submit: ', authId, name) + const slug = this.slugify(name) + await createTeam({ variables: { name, slug, owner: authId } }) + this.setState({ name: '' }) + } + + validate (data) { + const errors = {} + if (data === '') errors.name = 'Please enter a team name' + return errors + } + + 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 + } +} + +CreateTeamDialog.propTypes = { + classes: PropTypes.object.isRequired, + authId: PropTypes.string +} + +export default withStyles(styles)(CreateTeamDialog) diff --git a/client/src/views/dashboard/components/DashTeam.js b/client/src/views/dashboard/components/DashTeam.js index cad20bf57..8201e9d4d 100644 --- a/client/src/views/dashboard/components/DashTeam.js +++ b/client/src/views/dashboard/components/DashTeam.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { Query } from 'react-apollo' import { withStateHandlers, lifecycle, compose } from 'recompose' +import { Link } from 'react-router-dom' import Typography from '@material-ui/core/Typography' import Grid from '@material-ui/core/Grid' @@ -9,7 +10,8 @@ import Grid from '@material-ui/core/Grid' import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery' import DashTeamItem from './DashTeamItem' -import CreateTeamWrapper from './CreateTeamWrapper' +import CreateTeamDialog from './CreateTeamDialog' +import { MessageCard } from '../../common/' import { isDefined, isString } from '../../../utils/js-helpers' import { getItem } from '../../../utils/local-storage' @@ -38,11 +40,10 @@ export const DashTeam = ({ classes, user = null }) => { ) } return ( - + - Teams + Teams Manage Teams - {({ loading, error, data }) => { console.log('GET_TEAMS_BY_OWNER: ', loading, error, data) @@ -70,6 +71,16 @@ export const DashTeam = ({ classes, user = null }) => { {loader} ) + } else { + return ( + + + + + + + + ) } } else { return ( diff --git a/client/src/views/dashboard/components/ManageTeamDelete.js b/client/src/views/dashboard/components/ManageTeamDelete.js new file mode 100644 index 000000000..231dbab14 --- /dev/null +++ b/client/src/views/dashboard/components/ManageTeamDelete.js @@ -0,0 +1,130 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { withRouter } from 'react-router-dom' +import { Mutation } from 'react-apollo' + +import Button from '@material-ui/core/Button' +import DeleteIcon from '@material-ui/icons/Delete' +import Dialog from '@material-ui/core/Dialog' +import DialogActions from '@material-ui/core/DialogActions' +import DialogContent from '@material-ui/core/DialogContent' +import DialogTitle from '@material-ui/core/DialogTitle' +import Typography from '@material-ui/core/Typography' + +import DELETE_TEAM from '../../../graphql/teams/DeleteTeamMutation' +import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery' + +import { checkGraphQLError } from '../../../utils/graphql-errors' + +export class ManageTeamDelete extends Component { + constructor (props) { + super(props) + + this.handleClickOpen = this.handleClickOpen.bind(this) + this.handleClose = this.handleClose.bind(this) + this.handleChange = this.handleChange.bind(this) + this.handleSubmit = this.handleSubmit.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.slug) + const authId = this.props.authId + return ( + { + const data = cache.readQuery({ query: GET_TEAMS_BY_OWNER }) + console.log('Mutation cache update: ', deleteTeam, data) + data.getTeamsByOwner.push(deleteTeam) + cache.writeQuery({ query: GET_TEAMS_BY_OWNER, data }) + }} + > + {(deleteTeam, { loading, error, data }) => { + let errors + let loader + if (loading) { + loader = Submitting team... + } + if (error) { + errors = error.graphQLErrors.map(({ message }, i) => { + const displayMessage = checkGraphQLError(message) + console.log('createTeam error:', displayMessage) + return ( + + {message} + + ) + }) + } + return ( +
+ + + Delete Team Team + + DANGER - Deleting your team cannot be undone + Are you sure you want to delete this team? + {loader} + {errors} + + + + + + +
+ ) + }} +
+ ) + } + + async handleSubmit (e, deleteTeam, slug, authId) { + e.preventDefault() + await deleteTeam({ variables: { slug, owner: authId } }) + } +} + +ManageTeamDelete.propTypes = { + classes: PropTypes.object.isRequired, + slug: PropTypes.string.isRequired, + authId: PropTypes.string.isRequired +} + +export default withRouter(ManageTeamDelete) diff --git a/client/src/views/dashboard/components/ManageTeamEdit.js b/client/src/views/dashboard/components/ManageTeamEdit.js index d10dd5728..6937cc7f9 100644 --- a/client/src/views/dashboard/components/ManageTeamEdit.js +++ b/client/src/views/dashboard/components/ManageTeamEdit.js @@ -1,8 +1,10 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { withRouter } from 'react-router-dom' +import { Mutation } from 'react-apollo' + import Button from '@material-ui/core/Button' -import AddIcon from '@material-ui/icons/Add' +import EditIcon from '@material-ui/icons/Edit' import TextField from '@material-ui/core/TextField' import Dialog from '@material-ui/core/Dialog' import DialogActions from '@material-ui/core/DialogActions' @@ -13,15 +15,19 @@ import Typography from '@material-ui/core/Typography' import { getItem } from '../../../utils/local-storage' +import UPDATE_TEAM_PROFILE from '../../../graphql/teams/UpdateTeamProfileMutation' +import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery' + +import { checkGraphQLError } from '../../../utils/graphql-errors' + 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.handleSubmit = this.handleSubmit.bind(this) this.state = { open: false, @@ -30,6 +36,101 @@ export class ManageTeamEdit extends Component { } } + render () { + console.log(this.props, this.props.slug) + return ( + { + const data = cache.readQuery({ query: GET_TEAMS_BY_OWNER }) + console.log('Mutation cache update: ', deleteTeam, data) + data.getTeamsByOwner.push(deleteTeam) + cache.writeQuery({ query: GET_TEAMS_BY_OWNER, data }) + }} + > + {(updateTeamProfile, { loading, error, data }) => { + let errors + let loader + let description + let motto + if (loading) { + loader = Submitting team... + } + if (error) { + errors = error.graphQLErrors.map(({ message }, i) => { + const displayMessage = checkGraphQLError(message) + console.log('updateTeamProfile error:', displayMessage) + return ( + + {message} + + ) + }) + } + return ( +
+ + + Edit Team Details + + Team Creation + Team Motto: + { + motto = node + }} + /> + Team description: + { + description = node + }} + /> + {loader} + {errors} + + + + + + +
+ ) + }} +
+ ) + } + handleClickOpen () { this.setState({ open: true }) } @@ -39,95 +140,29 @@ export class ManageTeamEdit extends Component { } handleChange (e) { - this.setState({ name: e.target.value }) + switch (e.target.id) { + case 'motto': + this.setState({ motto: e.target.value }) + break + case 'description': + this.setState({ description: e.target.value }) + break + default: + } } - render () { - console.log(this.props, this.props.team) - return ( -
- - - Edit Team Details - - Team Creation - Team Motto: - - - - - - - -
- ) - } - - 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) { + async handleSubmit (e, updateTeamProfile, slug, description, motto) { 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 }) + authId = authId.authId + await updateTeamProfile({ variables: { slug, owner: authId, description: description, motto: motto } }) } } ManageTeamEdit.propTypes = { classes: PropTypes.object.isRequired, - team: PropTypes.object.isRequired, - createTeamMutation: PropTypes.function + slug: PropTypes.string.isRequired } export default withRouter(ManageTeamEdit) diff --git a/client/src/views/dashboard/components/ManageTeamsItem.js b/client/src/views/dashboard/components/ManageTeamsItem.js index fdb6534f4..f84fe3d40 100644 --- a/client/src/views/dashboard/components/ManageTeamsItem.js +++ b/client/src/views/dashboard/components/ManageTeamsItem.js @@ -10,8 +10,9 @@ import Button from '@material-ui/core/Button' import Typography from '@material-ui/core/Typography' import ManageTeamDelete from './ManageTeamDelete' +import ManageTeamEdit from './ManageTeamEdit' -export const ManageTeamsItem = ({ classes, team }) => ( +export const ManageTeamsItem = ({ classes, team, authId }) => (
@@ -32,10 +33,8 @@ export const ManageTeamsItem = ({ classes, team }) => ( - - + +
@@ -44,7 +43,8 @@ export const ManageTeamsItem = ({ classes, team }) => ( ManageTeamsItem.propTypes = { classes: PropTypes.object.isRequired, - team: PropTypes.object.isRequired + team: PropTypes.object.isRequired, + authId: PropTypes.string.isRequired } export default ManageTeamsItem diff --git a/client/src/views/dashboard/components/ManageTeamsList.js b/client/src/views/dashboard/components/ManageTeamsList.js index c15316e60..6a6bebde4 100644 --- a/client/src/views/dashboard/components/ManageTeamsList.js +++ b/client/src/views/dashboard/components/ManageTeamsList.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { Query } from 'react-apollo' import { withStateHandlers, lifecycle, compose } from 'recompose' -import Typography from '@material-ui/core/Typography' import Grid from '@material-ui/core/Grid' import { withStyles } from '@material-ui/core/styles' @@ -13,6 +12,8 @@ import { getItem } from '../../../utils/local-storage' import GET_TEAMS_BY_OWNER from '../../../graphql/teams/GetTeamsByOwnerQuery' import ManageTeamsItem from './ManageTeamsItem' +import CreateTeamDialog from './CreateTeamDialog' +import { MessageCard } from '../../common/' const styles = theme => ({ heroContent: { @@ -55,53 +56,74 @@ export const ManageTeamsList = ({ classes, user = null }) => { if (isDefined(owner.authId)) authId = owner.authId } console.log('ManageTeamsList 2: ', owner, authId) + if (authId === undefined || authId === null) { + return ( + + + + + + ) + } else { + return ( + + {({ loading, error, data }) => { + console.log('GET_TEAMS_BY_OWNER: ', loading, error, data) - return ( - - {({ loading, error, data }) => { - console.log('GET_TEAMS_BY_OWNER: ', loading, error, data) - - let errors - if (error) { - errors = error.graphQLErrors.map(({ message }, i) => { - return ( - - {message} - - ) - }) - } - if (!loading && !error) { - if (data.teamsByOwner.length > 0) { - return ( - + let errors = null + if (error) { + errors = error.graphQLErrors.map(({ message }, i) => { + return ( + + ) + }) + } + if (!loading && !error) { + if (data.teamsByOwner.length > 0) { + return ( + + + {!loading && + data.teamsByOwner.map(team => ( + + ))} + + + + {errors} + + + ) + } else { + return ( - {!loading && - data.teamsByOwner.map(team => ( - - ))} - {errors} + + + + + - + ) + } + } else { + return ( + + + + if (errors !== null) () + + ) } - } else { - return ( - - - Loading... - - {errors} - - ) - } - }} - - ) + }} + + ) + } } ManageTeamsList.propTypes = {