Hook up team dropdowns and lists

This commit is contained in:
John Lyon-Smith
2018-03-30 10:37:06 -07:00
parent b301bf17eb
commit bddf717d37
20 changed files with 437 additions and 321 deletions

Binary file not shown.

View File

@@ -24,11 +24,11 @@ export class ActivityRoutes {
listActivitys(req, res, next) { listActivitys(req, res, next) {
const Activity = this.db.Activity const Activity = this.db.Activity
let limit = req.params.limit || 20 const limit = req.query.limit || 20
let skip = req.params.skip || 0 const skip = req.query.skip || 0
let partial = !!req.params.partial const partial = !!req.query.partial
let branch = req.params.branch const branch = req.query.branch
let query = {} const query = {}
if (branch) { if (branch) {
query.branch = branch query.branch = branch

View File

@@ -22,19 +22,15 @@ export class TeamRoutes {
.delete(passport.authenticate('bearer', { session: false }), this.deleteTeam) .delete(passport.authenticate('bearer', { session: false }), this.deleteTeam)
} }
listTeams(req, res, next) { async listTeams(req, res, next) {
const Team = this.db.Team const Team = this.db.Team
let limit = req.params.limit || 20 let limit = req.query.limit || 20
let skip = req.params.skip || 0 let skip = req.query.skip || 0
let partial = !!req.params.partial let partial = !!req.query.partial
let branch = req.params.branch
let query = {} let query = {}
if (branch) { try {
query.branch = branch const total = await Team.count({})
}
Team.count({}).then((total) => {
let teams = [] let teams = []
let cursor = Team.find(query).limit(limit).skip(skip).cursor().map((doc) => { let cursor = Team.find(query).limit(limit).skip(skip).cursor().map((doc) => {
return doc.toClient(partial) return doc.toClient(partial)
@@ -52,93 +48,120 @@ export class TeamRoutes {
}) })
}) })
cursor.on('error', (err) => { cursor.on('error', (err) => {
next(createError.InternalServerError(err.message)) throw err
}) })
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
createTeam(req, res, next) { async createTeam(req, res, next) {
if (!req.user.administrator) {
return next(new createError.Forbidden())
}
// Create a new Team template then assign it to a value in the req.body
const Team = this.db.Team
let team = new Team(req.body)
// Save the team (with promise) - If it doesnt, catch and throw error
team.save().then((newTeam) => {
res.json(newTeam.toClient())
}).catch((err) => {
next(createError.InternalServerError(err.message))
})
}
updateTeam(req, res, next) {
if (!req.user.administrator) {
return next(new createError.Forbidden())
}
// Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) {
return next(createError.BadRequest('No _id given in body'))
}
let Team = this.db.Team
let teamUpdates = null
try { try {
teamUpdates = new Team(req.body) if (!req.user.administrator) {
} catch (err) { throw createError.Forbidden()
return next(createError.BadRequest('Invalid data')) }
}
// Create a new Team template then assign it to a value in the req.body
const Team = this.db.Team
let team = new Team(req.body)
const newTeam = await team.save()
res.json(newTeam.toClient())
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
}
async updateTeam(req, res, next) {
try {
if (!req.user.administrator) {
throw createError.Forbidden()
}
// Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) {
throw createError.BadRequest('No _id given in body')
}
let Team = this.db.Team
let teamUpdates = null
try {
teamUpdates = new Team(req.body)
} catch (err) {
throw createError.BadRequest('Invalid data')
}
const foundTeam = await Team.findById(teamUpdates._id)
Team.findById(teamUpdates._id).then((foundTeam) => {
if (!foundTeam) { if (!foundTeam) {
return next(createError.NotFound(`Team with _id ${_id} was not found`)) throw createError.NotFound(`Team with _id ${_id} was not found`)
} }
foundTeam.merge(teamUpdates) foundTeam.merge(teamUpdates)
return foundTeam.save() const savedTeam = await foundTeam.save()
}).then((savedTeam) => {
res.json(savedTeam.toClient()) res.json(savedTeam.toClient())
}).catch((err) => { } catch (err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
getTeam(req, res, next) { async getTeam(req, res, next) {
const Team = this.db.Team const Team = this.db.Team
const _id = req.params._id const _id = req.params._id
Team.findById(_id).then((team) => { try {
const team = await Team.findById(_id)
if (!team) { if (!team) {
return next(createError.NotFound(`Team with _id ${_id} not found`)) throw createError.NotFound(`Team with _id ${_id} not found`)
} }
res.json(team.toClient()) res.json(team.toClient())
}).catch((err) => { } catch (err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
deleteTeam(req, res, next) { async deleteTeam(req, res, next) {
if (!req.user.administrator) { try {
return next(new createError.Forbidden()) if (!req.user.administrator) {
} throw createError.Forbidden()
}
const Team = this.db.Team const Team = this.db.Team
const _id = req.params._id const _id = req.params._id
Team.remove({ _id }).then((team) => { const removedTeam = await Team.remove({ _id })
if (!project) {
return next(createError.NotFound(`Team with _id ${_id} not found`)) if (!removedTeam) {
throw createError.NotFound(`Team with _id ${_id} not found`)
} }
res.json({}) res.json({})
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
} }

View File

@@ -28,9 +28,6 @@ export class UserRoutes {
.get(passport.authenticate('bearer', { session: false }), this.getUser) .get(passport.authenticate('bearer', { session: false }), this.getUser)
.delete(passport.authenticate('bearer', { session: false }), this.deleteUser) .delete(passport.authenticate('bearer', { session: false }), this.deleteUser)
app.route('/users/set-image')
.put(passport.authenticate('bearer', { session: false }), this.setImage)
app.route('/users/enter-room/:roomName') app.route('/users/enter-room/:roomName')
.put(passport.authenticate('bearer', { session: false }), this.enterRoom) .put(passport.authenticate('bearer', { session: false }), this.enterRoom)
@@ -38,19 +35,27 @@ export class UserRoutes {
.put(passport.authenticate('bearer', { session: false }), this.leaveRoom) .put(passport.authenticate('bearer', { session: false }), this.leaveRoom)
} }
listUsers(req, res, next) { async listUsers(req, res, next) {
const User = this.db.User const User = this.db.User
const limit = req.params.limit || 20 const limit = req.query.limit || 20
const skip = req.params.skip || 0 const skip = req.query.skip || 0
const partial = !!req.query.partial
const isAdmin = !!req.user.administrator const isAdmin = !!req.user.administrator
const team = req.query.team
let query = {}
if (!isAdmin) { try {
return next(new createError.Forbidden()) if (team) {
} query = { team }
}
User.count({}).then((total) => { if (!isAdmin) {
throw createError.Forbidden()
}
const total = await User.count({})
let users = [] let users = []
let cursor = User.find({}).limit(limit).skip(skip).cursor().map((doc) => { let cursor = User.find(query).limit(limit).skip(skip).cursor().map((doc) => {
return doc.toClient(req.user) return doc.toClient(req.user)
}) })
@@ -66,56 +71,64 @@ export class UserRoutes {
}) })
}) })
cursor.on('error', (err) => { cursor.on('error', (err) => {
next(createError.InternalServerError(err.message)) throw err
}) })
}).catch((err) => { } catch(err) {
next(createError.InternalServerError(err.message))
})
}
getUser(req, res, next) {
let User = this.db.User
const _id = req.params._id
const isSelf = (_id === req.user._id)
const isAdmin = req.user.administrator
// User can see themselves, otherwise must be super user
if (!isSelf && !isAdmin) {
return next(new createError.Forbidden())
}
User.findById(_id).then((user) => {
if (!user) {
return Promise.reject(createError.NotFound(`User with _id ${_id} was not found`))
}
res.json(user.toClient(req.user))
}).catch((err) => {
if (err instanceof createError.HttpError) { if (err instanceof createError.HttpError) {
next(err) next(err)
} else { } else {
next(createError.InternalServerError(err.message)) next(createError.InternalServerError(err.message))
} }
}) }
} }
createUser(req, res, next) { async getUser(req, res, next) {
let User = this.db.User
const _id = req.params._id
const isSelf = (_id === req.user._id)
const isAdmin = req.user.administrator const isAdmin = req.user.administrator
if (!isAdmin) { try {
return next(new createError.Forbidden()) // User can see themselves, otherwise must be super user
} if (!isSelf && !isAdmin) {
throw createError.Forbidden()
}
const user = await User.findById(_id)
if (!user) {
return Promise.reject(createError.NotFound(`User with _id ${_id} was not found`))
}
res.json(user.toClient(req.user))
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
}
async createUser(req, res, next) {
const isAdmin = req.user.administrator
try {
if (!isAdmin) {
throw new createError.Forbidden()
}
let User = this.db.User
let user = new User(req.body)
// Add email confirmation required token
const buf = await util.promisify(crypto.randomBytes)(32)
let User = this.db.User
let user = new User(req.body)
// Add email confirmation required token
util.promisify(crypto.randomBytes)(32).then((buf) => {
user.emailToken = { user.emailToken = {
value: urlSafeBase64.encode(buf), value: urlSafeBase64.encode(buf),
created: new Date() created: new Date()
} }
return user.save() const savedUser = await user.save()
}).then((savedUser) => {
const userFullName = `${savedUser.firstName} ${savedUser.lastName}` const userFullName = `${savedUser.firstName} ${savedUser.lastName}`
const senderFullName = `${req.user.firstName} ${req.user.lastName}` const senderFullName = `${req.user.firstName} ${req.user.lastName}`
const siteUrl = url.parse(req.headers.referer) const siteUrl = url.parse(req.headers.referer)
@@ -130,137 +143,70 @@ export class UserRoutes {
supportEmail: this.supportEmail supportEmail: this.supportEmail
} }
} }
res.json(savedUser.toClient()) res.json(savedUser.toClient())
return this.sendEmail ? this.mq.request('dar-email', 'sendEmail', msg) : Promise.resolve()
}).catch((err) => { if (this.sendEmail) {
next(createError.InternalServerError(err.message)) await this.mq.request('dar-email', 'sendEmail', msg)
}) }
} catch(err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
updateUser(req, res, next) { async updateUser(req, res, next) {
const isAdmin = req.user.administrator const isAdmin = req.user.administrator
// Do this here because Mongoose will add it automatically otherwise
if (!req.body._id) {
return next(createError.BadRequest('No user _id given in body'))
}
const isSelf = (req.body._id === req.user._id.toString())
// User can change themselves, otherwise must be super user
if (!isSelf && !isAdmin) {
return next(new createError.Forbidden())
}
const User = this.db.User
let userUpdates = null
try { try {
userUpdates = new User(req.body) // Do this here because Mongoose will add it automatically otherwise
} catch (err) { if (!req.body._id) {
return next(createError.BadRequest('Invalid data')) throw createError.BadRequest('No user _id given in body')
} }
if (isSelf && !isAdmin) { const isSelf = (req.body._id === req.user._id.toString())
return next(createError.BadRequest('Cannot modify own administrator level'))
} // User can change themselves, otherwise must be super user
if (!isSelf && !isAdmin) {
throw createError.Forbidden()
}
const User = this.db.User
let userUpdates = null
try {
userUpdates = new User(req.body)
} catch (err) {
throw createError.BadRequest('Invalid data')
}
if (isSelf && !isAdmin) {
throw createError.BadRequest('Cannot modify own administrator level')
}
const foundUser = await User.findById(userUpdates._id)
User.findById(userUpdates._id).then((foundUser) => {
if (!foundUser) { if (!foundUser) {
return Promise.reject(createError.NotFound(`User with _id ${user._id} was not found`)) throw createError.NotFound(`User with _id ${user._id} was not found`)
} }
// We don't allow direct updates to the email field so remove it if present // We don't allow direct updates to the email field so remove it if present
delete userUpdates.email delete userUpdates.email
foundUser.merge(userUpdates) foundUser.merge(userUpdates)
return foundUser.save() const savedUser = await foundUser.save()
}).then((savedUser) => {
res.json(savedUser.toClient(req.user)) res.json(savedUser.toClient(req.user))
}).catch((err) => { } catch (err) {
next(createError.InternalServerError(err.message)) if (err instanceof createError.HttpError) {
}) next(err)
} } else {
next(createError.InternalServerError(err.message))
setImage(req, res, next) {
const isAdmin = req.user.administrator
const { _id, imageId } = req.body
if (!_id || !imageId) {
return next(createError.BadRequest('Must specify _id and imageId'))
}
const isSelf = (_id === req.user._id.toString())
// User can change themselves, otherwise must be super user
if (!isSelf && !isAdmin) {
return next(new createError.Forbidden())
}
const { bigSize = {}, smallSize = {} } = req.body
Promise.all([
this.mq.request('dar-image', 'scaleImage', {
newWidth: bigSize.width || 200,
newHeight: bigSize.height || 200,
scaleMode: 'aspectFill',
inputAssetId: imageId,
passback: {
userId: _id,
rooms: [ req.user._id ],
routerName: 'userRoutes',
funcName: 'completeSetBigImage'
}
}),
this.mq.request('dar-image', 'scaleImage', {
newWidth: smallSize.width || 25,
newHeight: smallSize.height || 25,
scaleMode: 'aspectFill',
inputAssetId: imageId,
passback: {
userId: _id,
rooms: [ req.user._id, 'users' ],
routerName: 'userRoutes',
funcName: 'completeSetSmallImage'
}
})
]).then((correlationIds) => {
res.json({ correlationIds })
}).catch((err) => {
next(createError.InternalServerError('Unable to scale user images'))
})
}
completeSetBigImage(passback, error, data) {
if (error || !passback.userId || !passback.rooms) {
return
}
const User = this.db.User
User.findByIdAndUpdate(passback.userId, { imageId: data.outputAssetId }).then((foundUser) => {
if (foundUser) {
this.ws.notify(passback.rooms, 'newProfileImage', { imageId: data.outputAssetId })
} }
}).catch((err) => {
this.log.error(`Unable to notify [${passback.rooms.join(', ')}] of new image'`)
})
}
completeSetSmallImage(passback, error, data) {
if (error || !passback.userId || !passback.rooms) {
return
} }
const User = this.db.User
User.findByIdAndUpdate(passback.userId, { thumbnailImageId: data.outputAssetId }).then((foundUser) => {
if (foundUser) {
this.ws.notify(passback.rooms, 'newThumbnailImage', { imageId: data.outputAssetId })
}
}).catch((err) => {
this.log.error(`Unable to notify [${passback.rooms.join(', ')}] of new thumbnail image'`)
})
} }
enterRoom(req, res, next) { enterRoom(req, res, next) {
@@ -273,19 +219,20 @@ export class UserRoutes {
res.json({}) res.json({})
} }
deleteUser(req, res, next) { async deleteUser(req, res, next) {
const isAdmin = req.user.administrator const isAdmin = req.user.administrator
if (!isAdmin) { try {
return new createError.Forbidden() if (!isAdmin) {
} throw createError.Forbidden()
}
let User = this.db.User let User = this.db.User
const _id = req.params._id const _id = req.params._id
const deletedUser = await User.remove({ _id })
User.remove({ _id }).then((deletedUser) => {
if (!deletedUser) { if (!deletedUser) {
return next(createError.NotFound(`User with _id ${_id} was not found`)) throw createError.NotFound(`User with _id ${_id} was not found`)
} }
const userFullName = `${deletedUser.firstName} ${deletedUser.lastName}` const userFullName = `${deletedUser.firstName} ${deletedUser.lastName}`
@@ -299,11 +246,17 @@ export class UserRoutes {
supportEmail: this.supportEmail supportEmail: this.supportEmail
} }
} }
return this.sendEmail ? this.mq.request('dar-email', 'sendEmail', msg) : Promise.resolve()
}).then(() => {
res.json({}) res.json({})
}).catch((err) => {
next(createError.InternalServerError(err.message)) if (this.sendEmail) {
}) await this.mq.request('dar-email', 'sendEmail', msg)
}
} catch (err) {
if (err instanceof createError.HttpError) {
next(err)
} else {
next(createError.InternalServerError(err.message))
}
}
} }
} }

View File

@@ -24,16 +24,11 @@ export class WorkItemRoutes {
listWorkItems(req, res, next) { listWorkItems(req, res, next) {
const WorkItem = this.db.WorkItem const WorkItem = this.db.WorkItem
let limit = req.params.limit || 20 const limit = req.query.limit || 20
let skip = req.params.skip || 0 const skip = req.query.skip || 0
let partial = !!req.params.partial const partial = !!req.query.partial
let branch = req.params.branch
let query = {} let query = {}
if (branch) {
query.branch = branch
}
WorkItem.count({}).then((total) => { WorkItem.count({}).then((total) => {
let workItems = [] let workItems = []
let cursor = WorkItem.find(query).limit(limit).skip(skip).cursor().map((doc) => { let cursor = WorkItem.find(query).limit(limit).skip(skip).cursor().map((doc) => {

View File

@@ -243,6 +243,9 @@ class API extends EventEmitter {
listUsers() { listUsers() {
return this.get('/users') return this.get('/users')
} }
listUsersForTeam(teamId) {
return this.get(`/users?team=${teamId}`)
}
createUser(user) { createUser(user) {
return this.post('/users', user) return this.post('/users', user)
} }
@@ -280,7 +283,7 @@ class API extends EventEmitter {
return this.post('/teams', team) return this.post('/teams', team)
} }
updateTeam(team) { updateTeam(team) {
this.put('/teams', team) return this.put('/teams', team)
} }
deleteTeam(id) { deleteTeam(id) {
return this.delete('/teams/' + id) return this.delete('/teams/' + id)

View File

@@ -4,6 +4,7 @@ import autobind from 'autobind-decorator'
import { Row, Column, BoundInput, BoundButton, List } from 'ui' import { Row, Column, BoundInput, BoundButton, List } from 'ui'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import { sizeInfo } from 'ui/style' import { sizeInfo } from 'ui/style'
import { api } from 'src/API'
export class TeamForm extends React.Component { export class TeamForm extends React.Component {
static propTypes = { static propTypes = {
@@ -36,14 +37,23 @@ export class TeamForm extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.users = [
{ id: 0, name: 'Meryll Streep' },
{ id: 1, name: 'Scarlett Johansenn' },
{ id: 2, name: 'John Lyon-Smith' },
]
this.state = { this.state = {
binder: new FormBinder(props.team, TeamForm.bindings, this.props.onModifiedChanged) binder: new FormBinder(props.team, TeamForm.bindings, this.props.onModifiedChanged),
users: []
}
this.getUsersForTeam(props.team._id)
}
@autobind
getUsersForTeam(teamId) {
if (teamId) {
api.listUsersForTeam(teamId).then((list) => {
list.items.sort((a, b) => (a.lastName.localeCompare(b.lastName)))
this.setState({ users: list.items })
}).catch(() => {
this.setState({ users: [] })
})
} }
} }
@@ -52,6 +62,8 @@ export class TeamForm extends React.Component {
this.setState({ this.setState({
binder: new FormBinder(nextProps.team, TeamForm.bindings, nextProps.onModifiedChanged) binder: new FormBinder(nextProps.team, TeamForm.bindings, nextProps.onModifiedChanged)
}) })
this.getUsersForTeam(nextProps.team._id)
} }
} }
@@ -78,7 +90,7 @@ export class TeamForm extends React.Component {
} }
render() { render() {
const { binder } = this.state const { binder, users } = this.state
return ( return (
<form style={{ width: '100%', height: '100%', overflow: 'scroll' }} id='teamForm' onSubmit={this.handleSubmit}> <form style={{ width: '100%', height: '100%', overflow: 'scroll' }} id='teamForm' onSubmit={this.handleSubmit}>
@@ -93,10 +105,10 @@ export class TeamForm extends React.Component {
<BoundInput label='Name' name='name' message='Must not be empty' binder={binder} /> <BoundInput label='Name' name='name' message='Must not be empty' binder={binder} />
</Column.Item> </Column.Item>
<Column.Item height={300}> <Column.Item height={300}>
<List items={this.users} render={(item) => ( <List items={users} render={(item) => (
<List.Item key={item.id}> <List.Item key={item._id}>
<List.Icon name='profile' size={sizeInfo.dropdownIconSize} /> <List.Icon name='profile' size={sizeInfo.dropdownIconSize} />
<List.Text>{item.name}</List.Text> <List.Text>{item.firstName + ' ' + item.lastName}</List.Text>
</List.Item> </List.Item>
)} /> )} />
</Column.Item> </Column.Item>

View File

@@ -31,8 +31,8 @@ export class Teams extends Component {
this.props.changeTitle('Teams') this.props.changeTitle('Teams')
api.listTeams().then((list) => { api.listTeams().then((list) => {
list.items.sort((teamA, teamB) => (teamA.lastName.localeCompare(teamB.lastName))) list.items.sort((teamA, teamB) => (teamA.name.localeCompare(teamB.name)))
this.setState({ teams: list.items }) // TODO: <- Remove this.setState({ teams: list.items })
}).catch((error) => { }).catch((error) => {
this.setState({ this.setState({
messageModal: { messageModal: {
@@ -101,7 +101,7 @@ export class Teams extends Component {
this.setState({ this.setState({
waitModal: false, waitModal: false,
teams: this.state.teams.map((team) => (!team._id ? createdTeam : team)).sort((a, b) => ( teams: this.state.teams.map((team) => (!team._id ? createdTeam : team)).sort((a, b) => (
a.lastName < b.lastName ? -1 : a.lastName > b.lastName : 0 a.name < b.name ? -1 : a.name > b.name ? 1 : 0
)), )),
modified: false, modified: false,
selectedTeam: createdTeam selectedTeam: createdTeam

View File

@@ -3,7 +3,9 @@ import PropTypes from 'prop-types'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
import { regExpPattern } from 'regexp-pattern' import { regExpPattern } from 'regexp-pattern'
import { api } from 'src/API' import { api } from 'src/API'
import { Row, Column, BoundInput, BoundButton, BoundCheckbox, BoundEmailIcon, DropdownList } from 'ui' import {
Row, Column, BoundInput, BoundButton, BoundCheckbox, BoundEmailIcon, BoundDropdown,
} from 'ui'
import { FormBinder } from 'react-form-binder' import { FormBinder } from 'react-form-binder'
import { sizeInfo } from 'ui/style' import { sizeInfo } from 'ui/style'
@@ -40,6 +42,9 @@ export class UserForm extends React.Component {
lastName: { lastName: {
isValid: (r, v) => (v !== '') isValid: (r, v) => (v !== '')
}, },
team: {
isValid: true
},
administrator: { administrator: {
isValid: (r, v) => true, isValid: (r, v) => true,
initValue: false, initValue: false,
@@ -66,8 +71,21 @@ export class UserForm extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
binder: new FormBinder(this.props.user, UserForm.bindings, this.props.onModifiedChanged) binder: new FormBinder(props.user, UserForm.bindings, props.onModifiedChanged),
teams: []
} }
this.getTeams()
}
// TODO: This is not very efficient. Better to get the teams in User.js and pass them in
// This however will always be up-to-date. Need to use the WebSocket to refresh.
getTeams() {
api.listTeams().then((list) => {
this.setState({ teams: list.items.map((item) => ({ value: item._id, text: item.name, icon: 'team' })).sort((a, b) => (a.text.localeCompare(b.text))) })
}).catch(() => {
this.setState({ teams: [] })
})
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@@ -75,6 +93,8 @@ export class UserForm extends React.Component {
this.setState({ this.setState({
binder: new FormBinder(nextProps.user, UserForm.bindings, nextProps.onModifiedChanged) binder: new FormBinder(nextProps.user, UserForm.bindings, nextProps.onModifiedChanged)
}) })
this.getTeams()
} }
} }
@@ -111,17 +131,7 @@ export class UserForm extends React.Component {
} }
render() { render() {
const { binder } = this.state const { binder, teams } = 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 ( return (
<form style={{ width: '100%', height: '100%', overflow: 'scroll' }} id='userForm' onSubmit={this.handleSubmit}> <form style={{ width: '100%', height: '100%', overflow: 'scroll' }} id='userForm' onSubmit={this.handleSubmit}>
@@ -155,12 +165,7 @@ export class UserForm extends React.Component {
</Row> </Row>
</Column.Item> </Column.Item>
<Column.Item> <Column.Item>
<DropdownList items={teams} render={(item) => ( <BoundDropdown name='team' label='Team' icon='team' items={teams} binder={binder} />
<DropdownList.Item key={item.id}>
<DropdownList.Icon name='team' size={sizeInfo.dropdownIconSize} />
<DropdownList.Text>{item.name}</DropdownList.Text>
</DropdownList.Item>
)} />
</Column.Item> </Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} /> <Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item minHeight={sizeInfo.buttonHeight}> <Column.Item minHeight={sizeInfo.buttonHeight}>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,9 +1,17 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 129 129"> <svg width="55px" height="52px" viewBox="0 0 55 52" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g> <!-- Generator: Sketch 49.2 (51160) - http://www.bohemiancoding.com/sketch -->
<g> <title>profile</title>
<path d="m64.3,71.6c18,0 32.6-14.6 32.6-32.6s-14.6-32.5-32.6-32.5-32.6,14.6-32.6,32.5 14.6,32.6 32.6,32.6zm0-56.6c13.2,0 24,10.8 24,24s-10.8,24-24,24-24-10.8-24-24 10.8-24 24-24z"/> <desc>Created with Sketch.</desc>
<path d="m7.9,122.5h113.2c2.4,0 4.3-1.9 4.3-4.3 0-22.5-18.3-40.9-40.9-40.9h-40c-22.5,0-40.9,18.3-40.9,40.9-1.33227e-15,2.4 1.9,4.3 4.3,4.3zm36.6-36.6h40c16.4,0 29.9,12.2 32,28h-104c2.1-15.7 15.6-28 32-28z"/> <defs></defs>
<g id="Login" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Home" transform="translate(-147.000000, -479.000000)">
<g id="profile" transform="translate(147.000000, 479.000000)">
<path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Path" fill="#FFFFFF"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Path" fill="#FFFFFF"></path>
</g>
</g>
</g> </g>
</g> </svg>
</svg>

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,24 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="240px" height="240px" viewBox="0 0 240 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="240px" height="240px" viewBox="0 0 240 240" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49 (51002) - http://www.bohemiancoding.com/sketch --> <!-- Generator: Sketch 49.2 (51160) - http://www.bohemiancoding.com/sketch -->
<title>users</title> <title>users</title>
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <defs></defs>
<g id="Login" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Login" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Home" transform="translate(-158.000000, -105.000000)" fill="#000000" fill-rule="nonzero"> <g id="Home" transform="translate(-158.000000, -105.000000)">
<g id="Users" transform="translate(122.000000, 94.000000)"> <g id="Users" transform="translate(122.000000, 94.000000)">
<g id="users" transform="translate(36.000000, 11.000000)"> <g id="users" transform="translate(36.000000, 11.000000)">
<g id="profile" transform="translate(16.000000, 85.000000)"> <g id="profile" transform="translate(93.000000, 85.000000)">
<path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Shape"></path> <path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Shape"></path> <path d="M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Path" fill="#FFFFFF"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Path" fill="#FFFFFF"></path>
</g> </g>
<g id="profile-copy" transform="translate(170.000000, 85.000000)"> <g id="profile-copy" transform="translate(31.000000, 85.000000)">
<path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Shape"></path> <path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Shape"></path> <path d="M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Path" fill="#FFFFFF"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Path" fill="#FFFFFF"></path>
</g> </g>
<g id="profile-copy-2" transform="translate(93.000000, 85.000000)"> <g id="profile-copy-2" transform="translate(156.000000, 85.000000)">
<path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Shape"></path> <path d="M28,29 C36.2822086,29 43,22.4961598 43,14.4777266 C43,6.45929339 36.2822086,0 28,0 C19.7177914,0 13,6.50384025 13,14.4777266 C13,22.4516129 19.7177914,29 28,29 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Shape"></path> <path d="M28,3.78648233 C34.0736196,3.78648233 39.0429448,8.59754224 39.0429448,14.4777266 C39.0429448,20.3579109 34.0736196,25.1689708 28,25.1689708 C21.9263804,25.1689708 16.9570552,20.3579109 16.9570552,14.4777266 C16.9570552,8.59754224 21.9263804,3.78648233 28,3.78648233 Z" id="Path" fill="#FFFFFF"></path>
<path d="M1.94170772,52 L53.0582923,52 C54.1420361,52 55,51.159292 55,50.0973451 C55,40.1415929 46.7364532,32 36.5311987,32 L18.4688013,32 C8.30870279,32 0,40.0973451 0,50.0973451 C-6.01598683e-16,51.159292 0.857963875,52 1.94170772,52 Z" id="Shape" fill="#000000" fill-rule="nonzero"></path>
<path d="M18.4688013,35.8053097 L36.5311987,35.8053097 C43.9367816,35.8053097 50.0328407,41.2035398 50.9811166,48.1946903 L4.01888342,48.1946903 C4.96715928,41.2477876 11.0632184,35.8053097 18.4688013,35.8053097 Z" id="Path" fill="#FFFFFF"></path>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -41,9 +41,13 @@ export class BoundButton extends React.Component {
const { name, text, submit, width, onClick } = this.props const { name, text, submit, width, onClick } = this.props
const { visible, disabled } = this.state const { visible, disabled } = this.state
if (!visible) {
return null
}
return ( return (
<Button disabled={disabled} submit={submit} onClick={onClick} <Button disabled={disabled} submit={submit} onClick={onClick}
name={name} visible={visible} text={text} width={width} /> name={name} text={text} width={width} />
) )
} }
} }

View File

@@ -36,8 +36,12 @@ export class BoundCheckbox extends React.Component {
const { name, label } = this.props const { name, label } = this.props
const { visible, disabled, value } = this.state const { visible, disabled, value } = this.state
if (!visible) {
return null
}
return ( return (
<div style={visible ? {} : { display: 'none' }}> <div>
<Checkbox id={name} disabled={disabled} value={value} onChange={this.handleChange} /> <Checkbox id={name} disabled={disabled} value={value} onChange={this.handleChange} />
<label <label
htmlFor={name} htmlFor={name}

View File

@@ -0,0 +1,75 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Dropdown } from 'ui'
import autobind from 'autobind-decorator'
import { sizeInfo, fontInfo } from 'ui/style'
export class BoundDropdown extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
binder: PropTypes.object.isRequired,
items: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string, icon: PropTypes.string })),
icon: PropTypes.string,
}
constructor(props) {
super(props)
this.state = props.binder.getFieldState(props.name)
}
@autobind
handleChange(item) {
const { binder, name } = this.props
const state = binder.getFieldState(name)
if (!state.readOnly && !state.disabled) {
this.setState(binder.updateFieldValue(name, item.value))
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.binder !== this.props.binder) {
this.setState(nextProps.binder.getFieldState(nextProps.name))
}
if (nextProps.items !== this.props.items) {
this.forceUpdate()
}
}
render() {
const { name, label, icon, items } = this.props
const { visible, disabled, value } = this.state
if (!visible) {
return null
}
return (
<div>
<label
htmlFor={name}
style={{
fontSize: fontInfo.size.medium,
fontFamily: fontInfo.family,
color: disabled ? fontInfo.color.dimmed : fontInfo.color.normal,
}}>
{label}
<Dropdown
items={items}
value={value}
emptyItem={{ value: null, text: '', icon: 'team' }}
icon={icon}
onChange={this.handleChange}
render={(item) => (
<Dropdown.Item key={item.value}>
<Dropdown.Icon name={item.icon} size={sizeInfo.dropdownIconSize} />
<Dropdown.Text>{item.text}</Dropdown.Text>
</Dropdown.Item>
)} />
</label>
</div>
)
}
}

View File

@@ -38,12 +38,16 @@ export class BoundInput extends React.Component {
const { label, password, name, placeholder, message } = this.props const { label, password, name, placeholder, message } = this.props
const { visible, disabled, value, valid } = this.state const { visible, disabled, value, valid } = this.state
if (!visible) {
return null
}
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Text>{label}</Text> <Text>{label}</Text>
<br /> <br />
<Input value={value} <Input value={value}
visible={visible} visible
disabled={disabled} disabled={disabled}
password={password} password={password}
name={name} name={name}

View File

@@ -8,17 +8,37 @@ import upArrowImage from './images/up-arrow.svg'
import autobind from 'autobind-decorator' import autobind from 'autobind-decorator'
@Radium @Radium
export class DropdownList extends Component { export class Dropdown extends Component {
static propTypes = { static propTypes = {
items: PropTypes.array, items: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string, icon: PropTypes.string })),
value: PropTypes.string,
render: PropTypes.func.isRequired, render: PropTypes.func.isRequired,
emptyItem: PropTypes.shape({ value: PropTypes.string, text: PropTypes.string, icon: PropTypes.string }),
onChange: PropTypes.func,
}
static defaultProps = {
emptyItem: { value: null, text: '', icon: null },
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
open: false, open: false,
selectedItem: (props.items && props.items.length > 0) ? props.items[0] : {}, selectedItem: this.getSelectedFromValue(props.items, props.value)
}
}
@autobind
getSelectedFromValue(items, value) {
return items.find((item) => (item.value === value)) || this.props.emptyItem
}
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value || nextProps.items !== this.props.items) {
this.setState({
selectedItem: this.getSelectedFromValue(nextProps.items, nextProps.value)
})
} }
} }
@@ -30,6 +50,9 @@ export class DropdownList extends Component {
@autobind @autobind
handleItemClick(e, item) { handleItemClick(e, item) {
this.setState({ selectedItem: item, open: false }) this.setState({ selectedItem: item, open: false })
if (this.props.onChange) {
this.props.onChange(item)
}
} }
render() { render() {
@@ -100,7 +123,7 @@ export class DropdownList extends Component {
} }
} }
DropdownList.Item = Radium(class ListItem extends Component { Dropdown.Item = Radium(class ListItem extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
active: PropTypes.bool, active: PropTypes.bool,
@@ -124,7 +147,7 @@ DropdownList.Item = Radium(class ListItem extends Component {
} }
}) })
DropdownList.Icon = Radium(class DropdownIcon extends Component { Dropdown.Icon = Radium(class DropdownIcon extends Component {
static propTypes = { static propTypes = {
name: PropTypes.string, name: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
@@ -149,7 +172,7 @@ DropdownList.Icon = Radium(class DropdownIcon extends Component {
} }
}) })
DropdownList.Text = Radium(class DropdownText extends Component { Dropdown.Text = Radium(class DropdownText extends Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
} }

View File

@@ -11,7 +11,7 @@ export { Text } from './Text'
export { Link } from './Link' export { Link } from './Link'
export { Icon } from './Icon' export { Icon } from './Icon'
export { List } from './List' export { List } from './List'
export { DropdownList } from './DropdownList' export { Dropdown } from './Dropdown'
export { Modal } from './Modal' export { Modal } from './Modal'
export { Dimmer } from './Dimmer' export { Dimmer } from './Dimmer'
export { Loader } from './Loader' export { Loader } from './Loader'
@@ -21,3 +21,4 @@ export { BoundButton } from './BoundButton'
export { BoundCheckbox } from './BoundCheckbox' export { BoundCheckbox } from './BoundCheckbox'
export { BoundInput } from './BoundInput' export { BoundInput } from './BoundInput'
export { BoundEmailIcon } from './BoundEmailIcon' export { BoundEmailIcon } from './BoundEmailIcon'
export { BoundDropdown } from './BoundDropdown'