diff --git a/server/package-lock.json b/server/package-lock.json
index 215ff09..bfde3d1 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -223,6 +223,11 @@
"resolved": "https://registry.npmjs.org/auto-bind2/-/auto-bind2-1.0.3.tgz",
"integrity": "sha512-+br9nya9M8ayHjai7m9rdpRxuEr8xcYRDrIp7HybNe0ixUHbc1kDiWXKMb0ldsfWb9Zi+SqJ9JfjW8nTkYD0QQ=="
},
+ "autobind-decorator": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/autobind-decorator/-/autobind-decorator-2.1.0.tgz",
+ "integrity": "sha512-bgyxeRi1R2Q8kWpHsb1c+lXCulbIAHsyZRddaS+agAUX3hFUVZMociwvRgeZi1zWvfqEEjybSv4zxWvFV8ydQQ=="
+ },
"aws-sdk": {
"version": "2.197.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.197.0.tgz",
diff --git a/server/package.json b/server/package.json
index cf36377..cbdb303 100644
--- a/server/package.json
+++ b/server/package.json
@@ -22,6 +22,7 @@
"amqplib": "^0.5.1",
"app-root-path": "^2.0.1",
"auto-bind2": "^1.0.3",
+ "autobind-decorator": "^2.1.0",
"aws-sdk": "^2.98.0",
"body-parser": "^1.17.1",
"canvas": "^1.6.7",
diff --git a/website/src/API.js b/website/src/API.js
index 81aa944..fba0264 100644
--- a/website/src/API.js
+++ b/website/src/API.js
@@ -237,15 +237,12 @@ class API extends EventEmitter {
return this.post('/auth/password/reset', passwords)
}
- getUser(_id) {
- return this.get('/users/' + _id)
+ getUser(id) {
+ return this.get('/users/' + id)
}
listUsers() {
return this.get('/users')
}
- listBrokerUsers() {
- return this.get('/users/brokers')
- }
createUser(user) {
return this.post('/users', user)
}
@@ -263,11 +260,8 @@ class API extends EventEmitter {
})
})
}
- deleteUser(_id) {
- return this.delete('/users/' + _id)
- }
- setUserImage(details) {
- return this.put('/users/set-image', details)
+ deleteUser(id) {
+ return this.delete('/users/' + id)
}
enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || ''))
@@ -276,6 +270,22 @@ class API extends EventEmitter {
return this.put('/users/leave-room')
}
+ getTeam(id) {
+ return this.get('/teams/' + id)
+ }
+ listTeams() {
+ return this.get('/teams')
+ }
+ createTeam(team) {
+ return this.post('/teams', team)
+ }
+ updateTeam(team) {
+ this.put('/teams', team)
+ }
+ deleteTeam(id) {
+ return this.delete('/teams/' + id)
+ }
+
upload(file, progressCallback) {
return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024
diff --git a/website/src/App.js b/website/src/App.js
index fc6bea7..0f165c0 100644
--- a/website/src/App.js
+++ b/website/src/App.js
@@ -3,6 +3,7 @@ import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRo
import { Home } from './Home'
import { Profile } from './Profile'
import { Users } from './Users'
+import { Teams } from './Teams'
import { HeaderButton, HeaderText, Column, Row, Text, Box } from 'ui'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import logoImage from 'images/logo.png'
@@ -99,7 +100,7 @@ export class App extends Component {
()} />
()} />
-
+ ()} />
()} />
diff --git a/website/src/Home/Home.js b/website/src/Home/Home.js
index e6ce9ad..37fe67a 100644
--- a/website/src/Home/Home.js
+++ b/website/src/Home/Home.js
@@ -29,7 +29,7 @@ export class Home extends Component {
-
+ (this.props.history.push('/teams'))} />
diff --git a/website/src/Teams/TeamForm.js b/website/src/Teams/TeamForm.js
new file mode 100644
index 0000000..a928662
--- /dev/null
+++ b/website/src/Teams/TeamForm.js
@@ -0,0 +1,214 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import autobind from 'autobind-decorator'
+import { regExpPattern } from 'regexp-pattern'
+import { api } from 'src/API'
+import { Row, Column, BoundInput, BoundButton, BoundCheckbox, BoundEmailIcon, DropdownList } from 'ui'
+import { FormBinder } from 'react-form-binder'
+import { sizeInfo } from 'ui/style'
+
+export class TeamForm extends React.Component {
+ static propTypes = {
+ user: PropTypes.object,
+ onSave: PropTypes.func,
+ onRemove: PropTypes.func,
+ onModifiedChanged: PropTypes.func,
+ onChangeEmail: PropTypes.func,
+ onResendEmail: PropTypes.func
+ }
+
+ static bindings = {
+ email: {
+ isValid: (r, v) => (regExpPattern.email.test(v)),
+ isDisabled: (r) => (r._id)
+ },
+ emailValidated: {
+ initValue: false,
+ isDisabled: (r) => (!r._id)
+ },
+ changeEmail: {
+ noValue: true,
+ isDisabled: (r) => (!r._id)
+ },
+ resendEmail: {
+ noValue: true,
+ isDisabled: (r) => (!r._id || !!r.getFieldValue('emailValidated'))
+ },
+ firstName: {
+ isValid: (r, v) => (v !== '')
+ },
+ lastName: {
+ isValid: (r, v) => (v !== '')
+ },
+ administrator: {
+ isValid: (r, v) => true,
+ initValue: false,
+ isDisabled: (r) => (api.loggedInTeam._id === r._id), // Adding a new user
+ alwaysGet: true,
+ },
+ remove: {
+ noValue: true,
+ isVisible: (r) => (r._id),
+ isDisabled: (r) => (api.loggedInTeam._id === r._id)
+ },
+ reset: {
+ noValue: true,
+ isDisabled: (r) => {
+ return !r.anyModified
+ }
+ },
+ submit: {
+ noValue: true,
+ isDisabled: (r) => (!r.anyModified || !r.allValid),
+ },
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ binder: new FormBinder(this.props.user, TeamForm.bindings, this.props.onModifiedChanged)
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.user !== this.props.user) {
+ this.setState({
+ binder: new FormBinder(nextProps.user, TeamForm.bindings, nextProps.onModifiedChanged)
+ })
+ }
+ }
+
+ @autobind
+ handleSubmit(e) {
+ e.preventDefault()
+
+ let obj = this.state.binder.getModifiedFieldValues()
+
+ if (obj) {
+ this.props.onSave(obj)
+ }
+ }
+
+ @autobind
+ handleReset() {
+ const { user, onModifiedChanged } = this.props
+
+ this.setState({ binder: new FormBinder(user, TeamForm.bindings, onModifiedChanged) })
+
+ if (onModifiedChanged) {
+ onModifiedChanged(false)
+ }
+ }
+
+ @autobind
+ handleChangeEmail() {
+ this.props.onChangeEmail()
+ }
+
+ @autobind
+ handleResendEmail() {
+ this.props.onResendEmail()
+ }
+
+ render() {
+ const { binder } = this.state
+ const teams = [
+ { id: 1, name: 'Sign of the Times' },
+ { id: 2, name: 'Trash Monsters' },
+ { id: 3, name: 'The Bigger Picker Uppers' },
+ { id: 4, name: 'Carcass Masters' },
+ { id: 5, name: 'Dust Bunnies' },
+ { id: 6, name: 'Pavement Busters' },
+ { id: 7, name: 'Don\'t Hug That Tree' },
+ { id: 8, name: 'Broken Swingers' },
+ ]
+
+ return (
+
+ )
+ }
+}
diff --git a/website/src/Teams/TeamFormPlaceholder.js b/website/src/Teams/TeamFormPlaceholder.js
new file mode 100644
index 0000000..3f54941
--- /dev/null
+++ b/website/src/Teams/TeamFormPlaceholder.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Column, Text } from 'ui'
+
+export const TeamFormPlaceholder = () => (
+
+
+
+ Select a team to view details here
+
+ Or 'Add New Team'
+
+
+
+)
diff --git a/website/src/Teams/TeamList.js b/website/src/Teams/TeamList.js
new file mode 100644
index 0000000..8871976
--- /dev/null
+++ b/website/src/Teams/TeamList.js
@@ -0,0 +1,55 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Column, List, Button } from 'ui'
+import { sizeInfo } from 'ui/style'
+
+export class TeamList extends React.Component {
+ static propTypes = {
+ users: PropTypes.array,
+ onUserListClick: PropTypes.func,
+ selectedUser: PropTypes.object,
+ selectionModified: PropTypes.bool,
+ onAddNewUser: PropTypes.func
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ users: null
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.users !== this.props.users) {
+ this.setState({ users: nextProps.users })
+ }
+ }
+
+ render() {
+ const { selectedUser, selectionModified } = this.props
+ const { users } = this.state
+
+ return (
+
+
+ {
+ return (
+ (this.props.onUserListClick(e, index))}
+ active={user === this.props.selectedUser}>
+
+
+ { user._id ? user.firstName + ' ' + user.lastName : '[New User]' }
+
+ { user === selectedUser && selectionModified ? : null }
+
+ )
+ }} />
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/website/src/Teams/Teams.js b/website/src/Teams/Teams.js
new file mode 100644
index 0000000..ebe8261
--- /dev/null
+++ b/website/src/Teams/Teams.js
@@ -0,0 +1,310 @@
+import React, { Component, Fragment } from 'react'
+import PropTypes from 'prop-types'
+import autobind from 'autobind-decorator'
+import { TeamList } from './TeamList'
+import { TeamForm } from './TeamForm'
+import { TeamFormPlaceholder } from './TeamFormPlaceholder'
+import { api } from 'src/API'
+import { Row, Column, Box } from 'ui'
+import { YesNoMessageModal, MessageModal, ChangeEmailModal, WaitModal } from '../Modal'
+import { sizeInfo, colorInfo } from 'ui/style'
+
+export class Teams extends Component {
+ static propTypes = {
+ changeTitle: PropTypes.func.isRequired,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ modified: false,
+ selectedTeam: null,
+ users: [],
+ yesNoModal: null,
+ messageModal: null,
+ waitModal: null,
+ changeEmailModal: null,
+ }
+ }
+
+ componentDidMount() {
+ this.props.changeTitle('Teams')
+
+ api.listTeams().then((list) => {
+ list.items.sort((userA, userB) => (userA.lastName.localeCompare(userB.lastName)))
+ this.setState({ users: list.items, selectedTeam: list.items[0] }) // TODO: <- Remove
+ }).catch((error) => {
+ this.setState({
+ messageModal: {
+ icon: 'hand',
+ message: 'Unable to get the list of teams.',
+ detail: error.message,
+ }
+ })
+ })
+ }
+
+ componentWillUnmount() {
+ this.props.changeTitle('')
+ }
+
+ removeUnfinishedNewTeam() {
+ let users = this.state.users
+
+ if (users.length > 0 && !users[0]._id) {
+ this.setState({ users: this.state.users.slice(1) })
+ }
+ }
+
+ @autobind
+ handleTeamListClick(e, index) {
+ let user = this.state.users[index]
+
+ if (this.state.modified) {
+ this.nextSelectedTeam = user
+ this.setState({
+ yesNoModal: {
+ question: 'This user has been modified. Are you sure you would like to navigate away?',
+ onDismiss: this.handleModifiedModalDismiss
+ }
+ })
+ } else {
+ this.setState({ selectedTeam: user })
+ this.removeUnfinishedNewTeam()
+ }
+ }
+
+ @autobind
+ handleSave(user) {
+ if (user._id) {
+ this.setState({ waitModal: { message: 'Updating Team' } })
+ api.updateTeam(user).then((updatedTeam) => {
+ this.setState({
+ waitModal: null,
+ users: this.state.users.map((user) => (user._id === updatedTeam._id ? updatedTeam : user)),
+ modified: false,
+ selectedTeam: updatedTeam
+ })
+ }).catch((error) => {
+ this.setState({
+ waitModal: null,
+ messageModal: {
+ icon: 'hand',
+ message: 'Unable to save the user changes',
+ detail: error.message,
+ }
+ })
+ })
+ } else {
+ this.setState({ waitModal: { message: 'Creating Team' } })
+ api.createTeam(user).then((createdTeam) => {
+ this.setState({
+ waitModal: false,
+ users: this.state.users.map((user) => (!user._id ? createdTeam : user)).sort((a, b) => (
+ a.lastName < b.lastName ? -1 : a.lastName > b.lastName : 0
+ )),
+ modified: false,
+ selectedTeam: createdTeam
+ })
+ }).catch((error) => {
+ this.setState({
+ waitModal: null,
+ messageModal: {
+ icon: 'hand',
+ message: 'Unable to create the user.',
+ detail: error.message,
+ }
+ })
+ })
+ }
+ }
+
+ @autobind
+ handleChangeEmail() {
+ this.setState({ changeEmailModal: { oldEmail: this.state.selectedTeam.email } })
+ }
+
+ @autobind
+ handleResendEmail() {
+ this.setState({
+ waitModal: { message: 'Resending Email...' }
+ })
+ api.sendConfirmEmail({ existingEmail: this.state.selectedTeam.email }).then(() => {
+ this.setState({
+ waitModal: null,
+ messageModal: {
+ icon: 'thumb',
+ message: `An email has been sent to '${this.state.selectedTeam.email}' with further instructions.`
+ }
+ })
+ }).catch((error) => {
+ this.setState({
+ error: true,
+ waitModal: null,
+ messageModal: {
+ icon: 'hand',
+ message: 'Unable to request email change.',
+ detail: error.message,
+ }
+ })
+ })
+ }
+
+ @autobind
+ handleChangeEmailDismiss(newEmail) {
+ this.setState({ changeEmailModal: null })
+ if (!newEmail) {
+ return
+ }
+ this.setState({
+ waitModal: { message: 'Requesting Email Change...' }
+ })
+ if (this.state.selectedTeam) {
+ api.sendConfirmEmail({ existingEmail: this.state.selectedTeam.email, newEmail }).then(() => {
+ this.setState({
+ waitModal: null,
+ messageModal: {
+ icon: 'hand',
+ message: `An email has been sent to '${newEmail}' to confirm this email.`
+ }
+ })
+ }).catch((error) => {
+ this.setState({
+ error: true,
+ waitModal: null,
+ messageModal: {
+ icon: 'hand',
+ message: 'Unable to request email change.',
+ detail: error.message,
+ }
+ })
+ })
+ }
+ }
+
+ @autobind
+ handleRemove() {
+ this.setState({
+ yesNoModal: {
+ question: 'Are you sure you want to remove this user? This will also remove them from any teams they belong to.',
+ onDismiss: this.handleRemoveModalDismiss
+ }
+ })
+ }
+
+ @autobind
+ handleRemoveModalDismiss(yes) {
+ if (yes) {
+ const selectedTeamId = this.state.selectedTeam._id
+ const selectedIndex = this.state.users.findIndex((user) => (user._id === selectedTeamId))
+
+ if (selectedIndex >= 0) {
+ this.setState({ waitModal: { message: 'Removing Team' } })
+ api.deleteTeam(selectedTeamId).then(() => {
+ this.setState({
+ waitModal: null,
+ users: [...this.state.users.slice(0, selectedIndex), ...this.state.users.slice(selectedIndex + 1)],
+ selectedTeam: null
+ })
+ }).catch((error) => {
+ this.setState({
+ waitModal: null,
+ messageModal: {
+ icon: 'hand',
+ message: 'Unable to remove the user.',
+ detail: error.message,
+ }
+ })
+ })
+ }
+ }
+
+ this.setState({
+ yesNoModal: null
+ })
+ }
+
+ @autobind
+ handleModifiedModalDismiss(yes) {
+ if (yes) {
+ this.setState({
+ selectedTeam: this.nextSelectedTeam,
+ modified: false
+ })
+ this.removeUnfinishedNewTeam()
+ delete this.nextSelectedTeam
+ }
+
+ this.setState({
+ yesNoModal: null
+ })
+ }
+
+ @autobind
+ handleMessageModalDismiss() {
+ this.setState({ messageModal: null })
+ }
+
+ @autobind
+ handleModifiedChanged(modified) {
+ this.setState({ modified: modified })
+ }
+
+ @autobind
+ handleAddNewTeam() {
+ let newTeam = {}
+ let newTeams = [newTeam].concat(this.state.users)
+ this.setState({ users: newTeams, selectedTeam: newTeam })
+ }
+
+ render() {
+ const { messageModal, yesNoModal, changeEmailModal } = this.state
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {
+ this.state.selectedTeam
+ ?
+ :
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/website/src/Teams/index.js b/website/src/Teams/index.js
new file mode 100644
index 0000000..0adea39
--- /dev/null
+++ b/website/src/Teams/index.js
@@ -0,0 +1 @@
+export { Teams } from './Teams'