Integrated master/detail, refactor Icon, add base router

This commit is contained in:
John Lyon-Smith committed 2018-05-12 12:36:39 -07:00
1 parent 84babf0e4b
commit 6fae5ef5d6
61 files changed
+1191 -1608

No files matched your search

+3 -3
View File
@@ -5104,9 +5104,9 @@
}
},
"react-form-binder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-1.2.0.tgz",
"integrity": "sha512-VFeiB5nCe01WU5aVJILMw7GLgOPsYJvdJEL9WRz7qecKDZx30sKA5bLDOWHsWQDZhediIr3KLpFkPxj0u89tDg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-2.0.0.tgz",
"integrity": "sha512-ihqbA3sp8eOOvjN2cSWOC7pfK+ukuRW5+dgpbrDJKnH/wgJ0LSMaJg2d/lX8bc0XO7+KxRJi7mBdizvCT1qhgQ==",
"requires": {
"eventemitter3": "^2.0.3",
"prop-types": "^15.5.10",
+1 -1
View File
@@ -23,7 +23,7 @@
"eventemitter3": "^3.1.0",
"moment": "^2.22.1",
"react": "^16.3.2",
"react-form-binder": "^1.2.0",
"react-form-binder": "^2.0.0",
"react-native": "^0.55.4",
"react-native-image-picker": "^0.26.7",
"react-native-iphone-x-helper": "^1.0.3",
+36 -5
View File
@@ -22,7 +22,7 @@ import {
BoundPhotoPanel,
FormStaticInput,
} from "../ui"
import { MessageModal, WaitModal } from "../Modal"
import { MessageModal, WaitModal, ProgressModal } from "../Modal"
import autobind from "autobind-decorator"
import KeyboardSpacer from "react-native-keyboard-spacer"
import { isIphoneX } from "react-native-iphone-x-helper"
@@ -88,6 +88,7 @@ export class Activity extends React.Component {
binder: new FormBinder({}, Activity.bindings),
waitModal: null,
messageModal: null,
progressModal: null,
}
const { search } = this.props.location
@@ -105,7 +106,7 @@ export class Activity extends React.Component {
this.setState({
binder: new FormBinder(
{
...this.state.binder.getOriginalFieldValues(),
...this.state.binder.originalObj,
workItem: workItem._id,
team: api.loggedInUser.team,
},
@@ -225,12 +226,35 @@ export class Activity extends React.Component {
@autobind
handleUploadStarted() {
this.setState({ waitModal: { message: "Uploading Photo..." } })
this.setState({
progressModal: { message: "Uploading Photo..." },
uploadPercent: 0,
})
}
@autobind
handleUploadEnded() {
this.setState({ waitModal: null })
handleUploadProgress(uploadData) {
console.log(uploadData)
if (this.state.progressModal) {
this.setState({
uploadPercent: Math.round(
uploadData.uploadedChunks / uploadData.numberOfChunks * 100
),
})
return true
} else {
return false
}
}
@autobind
handleUploadEnded(successful, uploadData) {
this.setState({ progressModal: null })
}
@autobind
handleUploadCanceled() {
this.setState({ progressModal: null })
}
render() {
@@ -313,11 +337,18 @@ export class Activity extends React.Component {
name="photos"
binder={binder}
onUploadStarted={this.handleUploadStarted}
onUploadProgress={this.handleUploadProgress}
onUploadEnded={this.handleUploadEnded}
/>
</View>
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
</ScrollView>
<ProgressModal
open={!!progressModal}
message={progressModal ? progressModal.message : ""}
percent={uploadPercent}
onCancel={this.handleUploadCanceled}
/>
<WaitModal
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
+4 -144
View File
@@ -1,44 +1,15 @@
import passport from "passport"
import createError from "http-errors"
import autobind from "autobind-decorator"
import { catchAll, TeamRoutes } from "."
import { catchAll, TeamRoutes, BaseRoutes } from "."
@autobind
export class ActivityRoutes {
export class ActivityRoutes extends BaseRoutes {
constructor(container) {
super(container, container.db.Activity)
const app = container.app
this.log = container.log
this.db = container.db
this.mq = container.mq
this.ws = container.ws
app
.route("/activities")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.listActivities)
)
.post(
passport.authenticate("bearer", { session: false }),
catchAll(this.createActivity)
)
.put(
passport.authenticate("bearer", { session: false }),
catchAll(this.updateActivity)
)
app
.route("/activities/:_id([a-f0-9]{24})")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.getActivity)
)
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.deleteActivity)
)
app
.route("/activities/all")
.delete(
@@ -47,117 +18,6 @@ export class ActivityRoutes {
)
}
async listActivities(req, res, next) {
const Activity = this.db.Activity
const limit = req.query.limit || 20
const skip = req.query.skip || 0
const partial = !!req.query.partial
let query = {}
const total = await Activity.count({})
let Activities = []
let cursor = Activity.find(query)
.limit(limit)
.skip(skip)
.cursor()
.map((doc) => {
return doc.toClient(partial)
})
cursor.on("data", (doc) => {
Activities.push(doc)
})
cursor.on("end", () => {
res.json({
total: total,
offset: skip,
count: activities.length,
items: activities,
})
})
cursor.on("error", (err) => {
throw createError.InternalServerError(err.message)
})
}
async createActivity(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
// Create a new Activity template then assign it to a value in the req.body
const Activity = this.db.Activity
let activity = new Activity(req.body)
// Save the activity (with promise) - If it doesnt, catch and throw error
const newActivity = await activity.save()
res.json(newActivity.toClient())
}
async updateActivity(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new 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 Activity = this.db.Activity
let activity = await Activity.findById(req.body._id)
if (!activity) {
return next(
createError.NotFound(`Activity with _id ${req.body_id} was not found`)
)
}
let activityUpdates = new Activity(req.body)
// Strip off all BSON types
activity.merge(activityUpdates)
const savedActivity = await activity.save()
res.json(savedActivity.toClient())
}
async getActivity(req, res, next) {
const Activity = this.db.Activity
const _id = req.params._id
const activity = await Activity.findById(_id)
if (!activity) {
throw createError.NotFound(`Activity with _id ${_id} not found`)
}
res.json(activity.toClient())
}
async deleteActivity(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
const Activity = this.db.Activity
const _id = req.params._id
const activity = await Activity.remove({ _id })
if (!activity) {
throw createError.NotFound(`Activity with _id ${_id} not found`)
}
res.json({})
}
async deleteAllActivities(req, res, next) {
const Activity = this.db.Activity
const Team = this.db.Team
+14 -13
View File
@@ -11,19 +11,19 @@ import B64 from "b64"
import { PassThrough } from "stream"
import { catchAll } from "."
function pipeToGridFS(readable, gfsWriteable, decoder) {
function pipeToGridFS(readable, writable, decoder) {
const promise = new Promise((resolve, reject) => {
readable.on("error", (error) => {
reject(error)
})
gfsWriteable.on("error", (error) => {
writeable.on("error", (error) => {
reject(error)
})
gfsWriteable.on("close", (file) => {
writeable.on("finish", (file) => {
resolve(file)
})
})
readable.pipe(decoder).pipe(gfsWriteable)
readable.pipe(decoder).pipe(writeable)
return promise
}
@@ -73,12 +73,13 @@ export class AssetRoutes {
assetId = assetId.slice(0, extIndex)
}
const file = await this.db.gridfs.findOneAsync({ _id: assetId })
const cursor = await this.db.gridfs.findOne({ _id: assetId })
if (!file) {
if (!cursor) {
throw createError.NotFound(`Asset ${assetId} was not found`)
}
const file = cursor.next()
const ifNoneMatch = req.get("If-None-Match")
if (ifNoneMatch && ifNoneMatch === file.md5) {
@@ -98,13 +99,13 @@ export class AssetRoutes {
ETag: file.md5,
})
this.db.gridfs.createReadStream({ _id: file._id }).pipe(res)
this.db.gridfs.openDownloadStream(file._id).pipe(res)
}
async deleteAsset(req, res, next) {
const assetId = req.params._id
await this.db.gridfs.removeAsync({ _id: assetId })
await this.db.gridfs.delete(assetId)
res.json({})
}
@@ -235,11 +236,11 @@ export class AssetRoutes {
if (uploadedChunks >= uploadData.numberOfChunks) {
let readable = redisReadStream(this.rs.client, uploadDataId)
let writeable = this.db.gridfs.createWriteStream({
_id: uploadId,
filename: uploadData.fileName,
content_type: uploadData.contentType,
})
let writeable = this.db.gridfs.openUploadStreamWithId(
uploadId,
uploadData.fileName,
{ contentType: uploadData.contentType }
)
const decoder =
uploadData.chunkContentType === "application/base64"
+147
View File
@@ -0,0 +1,147 @@
import passport from "passport"
import createError from "http-errors"
import autobind from "autobind-decorator"
import { catchAll } from "."
@autobind
export class BaseRoutes {
constructor(container, model) {
this.model = model
this.log = container.log
this.db = container.db
const basePath = "/" + model.collection.collectionName
const app = container.app
app
.route(basePath)
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.listItems)
)
.post(
passport.authenticate("bearer", { session: false }),
catchAll(this.createItem)
)
.put(
passport.authenticate("bearer", { session: false }),
catchAll(this.updateItem)
)
app
.route(basePath + "/:_id([a-f0-9]{24})")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.getItem)
)
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.deleteItem)
)
}
async listItems(req, res, next) {
const ItemModel = this.model
const limit = req.query.limit || 20
const skip = req.query.skip || 0
const partial = !!req.query.partial
let query = {}
const total = await ItemModel.count({})
let items = []
let cursor = ItemModel.find(query)
.limit(limit)
.skip(skip)
.cursor()
.map((doc) => {
return doc.toClient(partial)
})
cursor.on("data", (doc) => {
items.push(doc)
})
cursor.on("end", () => {
res.json({
total: total,
offset: skip,
count: items.length,
items: items,
})
})
cursor.on("error", (err) => {
throw createError.InternalServerError(err.message)
})
}
async createItem(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
const ItemModel = this.model
let item = new ItemModel(req.body)
const newItem = await item.save()
res.json(newItem.toClient())
}
async updateItem(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new 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 ItemModel = this.model
let item = await ItemModel.findById(req.body._id)
if (!item) {
return next(createError.NotFound(`Item with _id ${_id} was not found`))
}
item.merge(new ItemModel(req.body))
const savedItem = await item.save()
res.json(savedItem.toClient())
}
async getItem(req, res, next) {
const ItemModel = this.model
const _id = req.params._id
const item = await ItemModel.findById(_id)
if (!item) {
throw createError.NotFound(`Item with _id ${_id} not found`)
}
res.json(item.toClient())
}
async deleteItem(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
const ItemModel = this.model
const _id = req.params._id
const item = await ItemModel.remove({ _id })
if (!item) {
throw createError.NotFound(`Item with _id ${_id} not found`)
}
res.json({})
}
}
+4 -138
View File
@@ -3,45 +3,14 @@ import createError from "http-errors"
import autobind from "autobind-decorator"
import zlib from "zlib"
import { Readable } from "stream"
import { catchAll } from "."
import { catchAll, BaseRoutes } from "."
@autobind
export class TeamRoutes {
export class TeamRoutes extends BaseRoutes {
constructor(container) {
const app = container.app
super(container, container.db.Team)
this.log = container.log
this.db = container.db
this.mq = container.mq
this.ws = container.ws
app
.route("/teams")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.listTeams)
)
.post(
passport.authenticate("bearer", { session: false }),
catchAll(this.createTeam)
)
.put(
passport.authenticate("bearer", { session: false }),
catchAll(this.updateTeam)
)
app
.route("/teams/:_id([a-f0-9]{24})")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.getTeam)
)
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.deleteTeam)
)
app
container.app
.route("/teams/status")
.get(
passport.authenticate("bearer", { session: false }),
@@ -49,109 +18,6 @@ export class TeamRoutes {
)
}
async listTeams(req, res, next) {
const Team = this.db.Team
let limit = req.query.limit || 20
let skip = req.query.skip || 0
let partial = !!req.query.partial
let query = {}
const total = await Team.count({})
let teams = []
let cursor = Team.find(query)
.limit(limit)
.skip(skip)
.cursor()
.map((doc) => {
return doc.toClient(partial)
})
cursor.on("data", (doc) => {
teams.push(doc)
})
cursor.on("end", () => {
res.json({
total: total,
offset: skip,
count: teams.length,
items: teams,
})
})
cursor.on("error", (err) => {
throw err
})
}
async createTeam(req, res, next) {
if (!req.user.administrator) {
throw 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)
const newTeam = await team.save()
res.json(newTeam.toClient())
}
async updateTeam(req, res, next) {
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 team = await Team.findById(req.body._id)
if (!team) {
throw createError.NotFound(`Team with _id ${req.body_id} was not found`)
}
let teamUpdates = new Team(req.body)
team.merge(teamUpdates)
const savedTeam = await team.save()
res.json(savedTeam.toClient())
}
async getTeam(req, res, next) {
const Team = this.db.Team
const _id = req.params._id
const team = await Team.findById(_id)
if (!team) {
throw createError.NotFound(`Team with _id ${_id} not found`)
}
res.json(team.toClient())
}
async deleteTeam(req, res, next) {
if (!req.user.administrator) {
throw createError.Forbidden()
}
const Team = this.db.Team
const _id = req.params._id
const removedTeam = await Team.remove({ _id })
if (!removedTeam) {
throw createError.NotFound(`Team with _id ${_id} not found`)
}
res.json({})
}
async getTeamStatus(req, res, next) {
const Team = this.db.Team
const Activity = this.db.Activity
+3 -144
View File
@@ -2,33 +2,14 @@ import passport from "passport"
import createError from "http-errors"
import autobind from "autobind-decorator"
import merge from "deepmerge"
import { catchAll } from "."
import { catchAll, BaseRoutes } from "."
@autobind
export class WorkItemRoutes {
export class WorkItemRoutes extends BaseRoutes {
constructor(container) {
super(container, container.db.WorkItem)
const app = container.app
this.log = container.log
this.db = container.db
this.mq = container.mq
this.ws = container.ws
app
.route("/workitems")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.listWorkItems)
)
.post(
passport.authenticate("bearer", { session: false }),
catchAll(this.createWorkItem)
)
.put(
passport.authenticate("bearer", { session: false }),
catchAll(this.updateWorkItem)
)
app
.route("/workitems/activities")
.get(
@@ -36,17 +17,6 @@ export class WorkItemRoutes {
catchAll(this.listWorkItemActivities)
)
app
.route("/workitems/:_id([a-f0-9]{24})")
.get(
passport.authenticate("bearer", { session: false }),
catchAll(this.getWorkItem)
)
.delete(
passport.authenticate("bearer", { session: false }),
catchAll(this.deleteWorkItem)
)
app
.route("/workitems/all")
.delete(
@@ -55,40 +25,6 @@ export class WorkItemRoutes {
)
}
async listWorkItems(req, res, next) {
const WorkItem = this.db.WorkItem
const limit = req.query.limit || 20
const skip = req.query.skip || 0
const partial = !!req.query.partial
let query = {}
const total = await WorkItem.count({})
let workItems = []
let cursor = WorkItem.find(query)
.limit(limit)
.skip(skip)
.cursor()
.map((doc) => {
return doc.toClient(partial)
})
cursor.on("data", (doc) => {
workItems.push(doc)
})
cursor.on("end", () => {
res.json({
total: total,
offset: skip,
count: workItems.length,
items: workItems,
})
})
cursor.on("error", (err) => {
throw createError.InternalServerError(err.message)
})
}
async listWorkItemActivities(req, res, next) {
const WorkItem = this.db.WorkItem
const aggregate = WorkItem.aggregate()
@@ -117,83 +53,6 @@ export class WorkItemRoutes {
res.json({ items })
}
async createWorkItem(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
// Create a new WorkItem template then assign it to a value in the req.body
const WorkItem = this.db.WorkItem
let workItem = new WorkItem(req.body)
// Save the workItem (with promise) - If it doesnt, catch and throw error
const newWorkItem = await workItem.save()
res.json(newWorkItem.toClient())
}
async updateWorkItem(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new 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 WorkItem = this.db.WorkItem
let workItem = await WorkItem.findById(req.body._id)
if (!workItem) {
return next(
createError.NotFound(`WorkItem with _id ${req.body_id} was not found`)
)
}
const workItemUpdates = new WorkItem(req.body)
workItem.merge(workItemUpdates)
const savedWorkItem = await workItem.save()
res.json(savedWorkItem.toClient())
}
async getWorkItem(req, res, next) {
const WorkItem = this.db.WorkItem
const _id = req.params._id
const workItem = await WorkItem.findById(_id)
if (!workItem) {
throw createError.NotFound(`WorkItem with _id ${_id} not found`)
}
res.json(workItem.toClient())
}
async deleteWorkItem(req, res, next) {
const isAdmin = req.user.administrator
if (!isAdmin) {
return new createError.Forbidden()
}
const WorkItem = this.db.WorkItem
const _id = req.params._id
const workItem = await WorkItem.remove({ _id })
if (!workItem) {
throw createError.NotFound(`WorkItem with _id ${_id} not found`)
}
res.json({})
}
async deleteAllWorkItems(req, res, next) {
const Activity = this.db.Activity
const WorkItem = this.db.WorkItem
+1
View File
@@ -1,3 +1,4 @@
export { BaseRoutes } from "./BaseRoutes"
export { AuthRoutes } from "./AuthRoutes"
export { AssetRoutes } from "./AssetRoutes"
export { UserRoutes } from "./UserRoutes"
+2 -4
View File
@@ -1,6 +1,6 @@
import mongoose from "mongoose"
import mongodb from "mongodb"
import Grid from "gridfs-stream"
import { GridFSBucket } from "mongodb"
import mergePlugin from "mongoose-doc-merge"
import autobind from "autobind-decorator"
import * as Schemas from "./schemas"
@@ -18,9 +18,7 @@ export class DB {
autoIndex: !isProduction,
})
this.gridfs = Grid(connection.db, mongoose.mongo)
this.gridfs.findOneAsync = util.promisify(this.gridfs.findOne)
this.gridfs.removeAsync = util.promisify(this.gridfs.remove)
this.gridfs = new GridFSBucket(connection.db)
this.User = connection.model("User", Schemas.userSchema)
this.WorkItem = connection.model("WorkItem", Schemas.workItemSchema)
+2 -1
View File
@@ -24,6 +24,7 @@
"space-before-function-paren": ["error", "never"],
"comma-dangle": ["error", "only-multiline"],
"jsx-quotes": "off",
"quotes": "off"
"quotes": "off",
"indent": 0
}
}
+3 -3
View File
@@ -14167,9 +14167,9 @@
}
},
"react-form-binder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-1.2.0.tgz",
"integrity": "sha512-VFeiB5nCe01WU5aVJILMw7GLgOPsYJvdJEL9WRz7qecKDZx30sKA5bLDOWHsWQDZhediIr3KLpFkPxj0u89tDg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-2.0.0.tgz",
"integrity": "sha512-ihqbA3sp8eOOvjN2cSWOC7pfK+ukuRW5+dgpbrDJKnH/wgJ0LSMaJg2d/lX8bc0XO7+KxRJi7mBdizvCT1qhgQ==",
"requires": {
"eventemitter3": "^2.0.3",
"prop-types": "^15.5.10",
+1 -1
View File
@@ -12,7 +12,7 @@
"radium": "^0.22.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-form-binder": "^1.2.0",
"react-form-binder": "^2.0.0",
"react-router-dom": "^4.1.1",
"regexp-pattern": "^1.0.4",
"socket.io-client": "^2.0.3"
+38 -35
View File
@@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3"
import io from "socket.io-client"
import autobind from "autobind-decorator"
const authTokenName = "AuthToken"
const authTokenKeyName = "AuthToken"
class NetworkError extends Error {
constructor(message) {
@@ -33,30 +33,31 @@ class APIError extends Error {
class API extends EventEmitter {
constructor() {
super()
this.user = null
let token =
localStorage.getItem(authTokenName) ||
sessionStorage.getItem(authTokenName)
localStorage.getItem(authTokenKeyName) ||
sessionStorage.getItem(authTokenKeyName)
if (token) {
this.token = token
this.user = { pending: true }
this._token = token
this._user = { pending: true }
this.who()
.then((user) => {
this.user = user
this._user = user
this.connectSocket()
this.emit("login")
})
.catch(() => {
localStorage.removeItem(authTokenName)
sessionStorage.removeItem(authTokenName)
this.token = null
this.user = null
localStorage.removeItem(authTokenKeyName)
sessionStorage.removeItem(authTokenKeyName)
this._token = null
this._user = {}
this.socket = null
this.emit("logout")
})
} else {
this._user = {}
}
}
@@ -64,7 +65,7 @@ class API extends EventEmitter {
this.socket = io(window.location.origin, {
path: "/api/socketio",
query: {
auth_token: this.token,
auth_token: this._token,
},
})
this.socket.on("disconnect", (reason) => {
@@ -77,10 +78,10 @@ class API extends EventEmitter {
// Filter the few massages that affect our cached user data to avoid a server round trip
switch (eventName) {
case "newThumbnailImage":
this.user.thumbnailImageId = eventData.imageId
this._user.thumbnailImageId = eventData.imageId
break
case "newProfileImage":
this.user.imageId = eventData.imageId
this._user.imageId = eventData.imageId
break
default:
// Nothing to see here...
@@ -99,15 +100,15 @@ class API extends EventEmitter {
}
get loggedInUser() {
return this.user
return this._user
}
makeImageUrl(id, size) {
if (id) {
return "/api/assets/" + id + "?access_token=" + this.token
return "/api/assets/" + id + "?access_token=" + this._token
} else if (size && size.width && size.height) {
return `/api/placeholders/${size.width}x${size.height}?access_token=${
this.token
this._token
}`
} else {
return null
@@ -115,11 +116,11 @@ class API extends EventEmitter {
}
makeAssetUrl(id) {
return id ? "/api/assets/" + id + "?access_token=" + this.token : null
return id ? "/api/assets/" + id + "?access_token=" + this._token : null
}
makeTeamStatusUrl() {
return `/api/teams/status?access_token=${this.token}`
return `/api/teams/status?access_token=${this._token}`
}
static makeParams(params) {
@@ -140,8 +141,8 @@ class API extends EventEmitter {
cache: "no-store",
}
let headers = new Headers()
if (this.token) {
headers.set("Authorization", "Bearer " + this.token)
if (this._token) {
headers.set("Authorization", "Bearer " + this._token)
}
if (method === "POST" || method === "PUT") {
if (requestOptions.binary) {
@@ -207,12 +208,12 @@ class API extends EventEmitter {
}
if (remember) {
localStorage.setItem(authTokenName, token)
localStorage.setItem(authTokenKeyName, token)
} else {
sessionStorage.setItem(authTokenName, token)
sessionStorage.setItem(authTokenKeyName, token)
}
this.token = token
this.user = response.body
this._token = token
this._user = response.body
this.connectSocket()
this.emit("login")
resolve(response.body)
@@ -225,10 +226,10 @@ class API extends EventEmitter {
logout() {
let cb = () => {
// Regardless of response, always logout in the client
localStorage.removeItem(authTokenName)
sessionStorage.removeItem(authTokenName)
this.token = null
this.user = null
localStorage.removeItem(authTokenKeyName)
sessionStorage.removeItem(authTokenKeyName)
this._token = null
this._user = {}
this.disconnectSocket()
this.emit("logout")
}
@@ -256,6 +257,8 @@ class API extends EventEmitter {
return this.post("/auth/password/reset", passwords)
}
// Users
getUser(id) {
return this.get("/users/" + id)
}
@@ -271,16 +274,16 @@ class API extends EventEmitter {
updateUser(user) {
return new Promise((resolve, reject) => {
this.put("/users", user)
.then((user) => {
.then((updatedUser) => {
// If we just updated ourselves, update the internal cached copy
if (user._id === this.user._id) {
this.user = user
if (updatedUser._id === this._user._id) {
this._user = updatedUser
this.emit("login")
}
resolve(user)
resolve(updatedUser)
})
.catch((reason) => {
reject(reason)
.catch((error) => {
reject(error)
})
})
}
+41 -134
View File
@@ -1,4 +1,4 @@
import React, { Component, Fragment } from "react"
import React, { Component } from "react"
import {
Login,
Logout,
@@ -13,160 +13,67 @@ import { Profile } from "./Profile"
import { Users } from "./Users"
import { Teams } from "./Teams"
import { System } from "./System"
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"
import { versionInfo } from "./version"
import { sizeInfo, colorInfo } from "ui/style"
import { api } from "src/API"
import { Header, Column, Footer } from "ui"
import { BrowserRouter, Route, Switch } from "react-router-dom"
import { sizeInfo } from "ui/style"
import PropTypes from "prop-types"
import autobind from "autobind-decorator"
import { versionInfo } from "./version"
export class App extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
constructor(props) {
super(props)
this.state = {
loggedInUser: api.loggedInUser,
}
}
componentDidMount() {
api.addListener("login", this.handleUpdate)
api.addListener("logout", this.handleUpdate)
}
componentWillUnmount() {
api.removeListener("login", this.handleUpdate)
api.removeListener("logout", this.handleUpdate)
}
@autobind
handleUpdate() {
this.setState({ loggedInUser: api.loggedInUser })
}
handleLogout() {
// We have to use window here because App does not have history in it's props
window.location.replace("/logout")
}
handleHome() {
window.location.replace("/")
}
handleProfile() {
window.location.replace("/profile")
}
@autobind
handleChangeTitle(title) {
this.setState({ title })
}
render() {
const { loggedInUser } = this.state
let headerButtonsRight = null
let headerButtonsLeft = null
if (loggedInUser) {
headerButtonsLeft = (
<Fragment>
<HeaderButton image={logoImage} onClick={this.handleHome} />
<HeaderText text={this.state.title} />
</Fragment>
)
headerButtonsRight = (
<Fragment>
<HeaderButton icon="profile" onClick={this.handleProfile} />
<HeaderButton icon="logout" onClick={this.handleLogout} />
</Fragment>
)
}
return (
<Router basename="/">
<BrowserRouter>
<Column minHeight="100vh">
<Column.Item
height={sizeInfo.headerHeight - sizeInfo.headerBorderWidth}>
<Box
background={colorInfo.headerButtonBackground}
borderBottom={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
style={{ boxSizing: "content" }}>
<Row minWidth="100vw">
<Row.Item>{headerButtonsLeft}</Row.Item>
<Row.Item grow />
<Row.Item>{headerButtonsRight}</Row.Item>
</Row>
</Box>
<Route
path="/app"
render={(props) => (
<Column.Item height={sizeInfo.headerHeight}>
<Header
{...props}
left={[
{ image: require("images/badge.png"), path: "/app/home" },
{ text: "Teams", path: "/app/teams" },
{ text: "Users", path: "/app/users" },
]}
right={[
{ icon: "profile", path: "/app/profile" },
{ icon: "logout", path: "/logout" },
]}
/>
</Column.Item>
)}
/>
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/logout" component={Logout} />
<Route exact path="/confirm-email" component={ConfirmEmail} />
<Route exact path="/reset-password" component={ResetPassword} />
<Route exact path="/forgot-password" component={ForgotPassword} />
<ProtectedRoute
exact
path="/profile"
render={(props) => (
<Profile {...props} changeTitle={this.handleChangeTitle} />
)}
/>
<ProtectedRoute
exact
admin
path="/users"
render={(props) => (
<Users {...props} changeTitle={this.handleChangeTitle} />
)}
/>
<ProtectedRoute
exact
admin
path="/teams"
render={(props) => (
<Teams {...props} changeTitle={this.handleChangeTitle} />
)}
/>
<ProtectedRoute
exact
admin
path="/system"
render={(props) => (
<System {...props} changeTitle={this.handleChangeTitle} />
)}
/>
<ProtectedRoute
exact
admin
path="/home"
render={(props) => (
<Home {...props} changeTitle={this.handleChangeTitle} />
)}
/>
<DefaultRoute />
<ProtectedRoute exact path="/app/profile" component={Profile} />
<ProtectedRoute exact admin path="/app/home" component={Home} />
<ProtectedRoute exact admin path="/app/teams" component={Teams} />
<ProtectedRoute exact admin path="/system" component={System} />
<ProtectedRoute exact admin path="/app/users" component={Users} />
<DefaultRoute redirect="/app/home" />
</Switch>
<Route
path="/app"
render={() => (
<Column.Item>
<Box
background={colorInfo.headerButtonBackground}
borderTop={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}>
<Text color="dimmed" margin={sizeInfo.footerTextMargin}>
{"v" + versionInfo.fullVersion} {versionInfo.copyright}
</Text>
</Box>
<Footer
text={
"v" + versionInfo.fullVersion + " " + versionInfo.copyright
}
/>
</Column.Item>
)}
/>
</Column>
</Router>
</BrowserRouter>
)
}
}
+7 -40
View File
@@ -1,47 +1,14 @@
import React, { Fragment, Component } from 'react'
import { api } from 'src/API'
import { Route, Redirect } from 'react-router-dom'
import { Column } from 'ui'
import autobind from 'autobind-decorator'
import React, { Component } from "react"
import { Route, Redirect } from "react-router-dom"
import PropTypes from "prop-types"
export class DefaultRoute extends Component {
@autobind
updateComponent() {
this.forceUpdate()
}
componentDidMount() {
api.addListener('login', this.updateComponent)
}
componentWillUnmount() {
api.removeListener('login', this.updateComponent)
static propTypes = {
redirect: PropTypes.string,
}
render() {
const user = api.loggedInUser
let path = null
if (user) {
if (!user.pending) {
path = user.administrator ? '/home' : '/profile'
}
} else {
path = '/login'
}
return (
<Route
path='/'
render={() => {
return (
<Fragment>
<Column.Item grow />
{path ? <Redirect to={path} /> : null}
</Fragment>
)
}}
/>
)
// NOTE: When working on the site, Redirect to the page you are working on
return <Route render={() => <Redirect to={this.props.redirect} />} />
}
}
+61 -36
View File
@@ -1,28 +1,28 @@
import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types'
import { regExpPattern } from 'regexp-pattern'
import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from 'ui'
import { MessageModal, WaitModal } from '../Modal'
import { api } from 'src/API'
import { FormBinder } from 'react-form-binder'
import headerLogo from 'images/deighton.png'
import { sizeInfo, colorInfo } from 'ui/style'
import autobind from 'autobind-decorator'
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import { regExpPattern } from "regexp-pattern"
import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from "ui"
import { MessageModal, WaitModal } from "../Modal"
import { api } from "src/API"
import { FormBinder } from "react-form-binder"
import headerLogo from "images/badge.png"
import { sizeInfo, colorInfo } from "ui/style"
import autobind from "autobind-decorator"
export class ForgotPassword extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
static bindings = {
email: {
alwaysGet: true,
isValid: (r, v) => (regExpPattern.email.test(v))
isValid: (r, v) => regExpPattern.email.test(v),
},
submit: {
noValue: true,
isDisabled: (r) => (!r.anyModified || !r.allValid)
}
isDisabled: (r) => !r.anyModified || !r.allValid,
},
}
constructor(props) {
@@ -30,7 +30,7 @@ export class ForgotPassword extends Component {
this.state = {
binder: new FormBinder({}, ForgotPassword.bindings),
messageModal: null,
waitModal: null
waitModal: null,
}
}
@@ -45,16 +45,18 @@ export class ForgotPassword extends Component {
const obj = this.state.binder.getModifiedFieldValues()
this.setState({ waitModal: { message: 'Requesting Reset Email' } })
this.setState({ waitModal: { message: "Requesting Reset Email" } })
const cb = (res) => {
this.setState({
waitModal: null,
messageModal: {
icon: 'thumb',
title: 'Password Reset Requested',
message: `If everything checks out, an email will be sent to '${obj.email}' with a reset link. Please click on it to finish resetting the password.`
}
icon: "thumb",
title: "Password Reset Requested",
message: `If everything checks out, an email will be sent to '${
obj.email
}' with a reset link. Please click on it to finish resetting the password.`,
},
})
}
@@ -63,7 +65,7 @@ export class ForgotPassword extends Component {
@autobind
handleMessageModalDismiss() {
this.props.history.replace('/')
this.props.history.replace("/")
}
render() {
@@ -77,8 +79,13 @@ export class ForgotPassword extends Component {
<Row.Item grow />
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item width={sizeInfo.modalWidth}>
<form id='forgotPasswordForm' onSubmit={this.handleSubmit}>
<Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }} radius={sizeInfo.formBoxRadius}>
<form id="forgotPasswordForm" onSubmit={this.handleSubmit}>
<Box
border={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
radius={sizeInfo.formBoxRadius}>
<Row>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item>
@@ -88,31 +95,46 @@ export class ForgotPassword extends Component {
<Row>
<Row.Item grow />
<Row.Item>
<Image source={headerLogo} width={sizeInfo.loginLogoWidth} />
<Image
source={headerLogo}
width={sizeInfo.loginLogoWidth}
/>
</Row.Item>
<Row.Item grow />
</Row>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item>
<Text size='large'>Forgotten Password</Text>
<Text size="large">Forgotten Password</Text>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item>
<BoundInput label='Email' name='email'
placeholder='example@xyz.com' binder={this.state.binder}
message='A valid email address' />
<BoundInput
label="Email"
name="email"
placeholder="example@xyz.com"
binder={this.state.binder}
message="A valid email address"
/>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item>
<Text>The email address of an existing user to send the password reset link to.</Text>
<Text>
The email address of an existing user to send the
password reset link to.
</Text>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item minHeight={sizeInfo.buttonHeight}>
<Row>
<Row.Item grow />
<Row.Item>
<BoundButton text='Submit' name='submit' submit='forgotPasswordForm' binder={binder} />
<BoundButton
text="Submit"
name="submit"
submit="forgotPasswordForm"
binder={binder}
/>
</Row.Item>
</Row>
</Column.Item>
@@ -128,15 +150,18 @@ export class ForgotPassword extends Component {
</Row>
</Column.Item>
<Column.Item grow>
<WaitModal active={!!waitModal}
message={waitModal ? waitModal.message : ''} />
<WaitModal
active={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ''}
message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.detail : ''}
onDismiss={this.handleMessageModalDismiss} />
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal ? messageModal.detail : ""}
onDismiss={this.handleMessageModalDismiss}
/>
</Column.Item>
</Fragment>
)
+1 -1
View File
@@ -14,7 +14,7 @@ import {
BoundCheckbox,
BoundButton,
} from "ui"
import headerLogo from "images/deighton.png"
import headerLogo from "images/logo.png"
import { versionInfo } from "../version"
import { FormBinder } from "react-form-binder"
import autobind from "autobind-decorator"
+19 -10
View File
@@ -6,7 +6,10 @@ import autobind from 'autobind-decorator'
export class ProtectedRoute extends React.Component {
static propTypes = {
location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }),
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
}),
admin: PropTypes.bool,
}
@@ -16,26 +19,32 @@ export class ProtectedRoute extends React.Component {
}
componentDidMount() {
api.addListener('login', this.updateComponent)
api.addListener("login", this.updateComponent)
api.addListener("logout", this.updateComponent)
}
componentWillUnmount() {
api.removeListener('login', this.updateComponent)
api.removeListener("login", this.updateComponent)
api.removeListener("logout", this.updateComponent)
}
render(props) {
const user = api.loggedInUser
if (user) {
if (user.pending) {
// The API might be in the middle of fetching the user information
// Return something and wait for login evint to fire to re-render
return <div />
} else if (!this.props.admin || (this.props.admin && user.administrator)) {
return null
} else {
if (!user._id || (this.props.admin && !user.administrator)) {
return (
<Redirect
to={`/login?redirect=${this.props.location.pathname}${
this.props.location.search
}`}
/>
)
} else {
return <Route {...this.props} />
}
}
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { api } from "src/API"
import { FormBinder } from "react-form-binder"
import { sizeInfo, colorInfo } from "ui/style"
import autobind from "autobind-decorator"
import headerLogo from "images/deighton.png"
import headerLogo from "images/logo.png"
export class ResetPassword extends Component {
static propTypes = {
+19 -16
View File
@@ -1,20 +1,11 @@
import React, { Component, Fragment } from 'react'
import PropTypes from 'prop-types'
import { Row, Column, PanelButton } from 'ui'
import { sizeInfo } from 'ui/style'
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import { Row, Column, PanelButton } from "ui"
import { sizeInfo } from "ui/style"
export class Home extends Component {
static propTypes = {
history: PropTypes.object,
changeTitle: PropTypes.func.isRequired,
}
componentDidMount() {
this.props.changeTitle('Home')
}
componentWillUnmount() {
this.props.changeTitle('')
}
render() {
@@ -25,15 +16,27 @@ export class Home extends Component {
<Row>
<Row.Item grow />
<Row.Item>
<PanelButton icon='users' text='Users' onClick={() => (this.props.history.push('/users'))} />
<PanelButton
icon="users"
text="Users"
onClick={() => this.props.history.push("/users")}
/>
</Row.Item>
<Row.Item width={sizeInfo.panelButtonSpacing} />
<Row.Item>
<PanelButton icon='teams' text='Teams' onClick={() => (this.props.history.push('/teams'))} />
<PanelButton
icon="teams"
text="Teams"
onClick={() => this.props.history.push("/teams")}
/>
</Row.Item>
<Row.Item width={sizeInfo.panelButtonSpacing} />
<Row.Item>
<PanelButton icon='system' text='System' onClick={() => (this.props.history.push('/system'))} />
<PanelButton
icon="system"
text="System"
onClick={() => this.props.history.push("/system")}
/>
</Row.Item>
<Row.Item grow />
</Row>
@@ -0,0 +1,30 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import { Column, Text } from "ui"
export class DetailPlaceholder extends Component {
static propTypes = {
name: PropTypes.string,
}
render() {
const { name } = this.props
const capitalizedName = name.charAt(0).toUpperCase() + name.substr(1)
return (
<Column fillParent>
<Column.Item grow />
<Column.Item>
<Text size="large" align="center" width="100%">
{`Select a ${name} to view details here`}
</Text>
<br />
<Text size="small" align="center" width="100%">
{`Or 'Add New ${capitalizedName}'`}
</Text>
</Column.Item>
<Column.Item grow />
</Column>
)
}
}
+356
View File
@@ -0,0 +1,356 @@
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import autobind from "autobind-decorator"
import { Row, Column, Box } from "ui"
import { YesNoMessageModal, MessageModal, WaitModal } from "../Modal"
import { sizeInfo, colorInfo } from "ui/style"
import { DetailPlaceholder, MasterList } from "."
import pluralize from "pluralize"
export class MasterDetail extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
name: PropTypes.string,
form: PropTypes.func.isRequired,
listItems: PropTypes.func.isRequired,
updateItem: PropTypes.func.isRequired,
createItem: PropTypes.func.isRequired,
deleteItem: PropTypes.func.isRequired,
sort: PropTypes.func.isRequired,
detailCallbacks: PropTypes.object,
listData: PropTypes.func,
children: PropTypes.element,
}
constructor(props) {
super(props)
this.state = {
modified: false,
selectedItem: null,
items: [],
yesNoModal: null,
messageModal: null,
waitModal: null,
changeEmailModal: null,
}
const { name } = this.props
this.capitalizedName = name.charAt(0).toUpperCase() + name.substr(1)
this.pluralizedName = pluralize(name)
}
componentDidMount() {
this.props
.listItems()
.then((list) => {
this.setState({ items: list.items })
const { history } = this.props
const search = new URLSearchParams(history.location.search)
const id = search.get("id")
if (id) {
const item = list.items.find((item) => item._id === id)
if (item) {
this.setState({ selectedItem: item })
} else {
history.replace(history.pathname)
}
}
})
.catch((error) => {
this.showErrorMessage(
`Unable to get the list of ${this.pluralizedName}.`,
error.message
)
})
}
get selectedItem() {
return this.state.selectedItem
}
@autobind
showWait(message) {
this.setState({
waitModal: {
message,
},
})
}
@autobind
hideWait() {
this.setState({
waitModal: null,
})
}
@autobind
showMessage(message, detail) {
this.setState({
icon: "thumb",
message,
detail,
})
}
@autobind
showErrorMessage(message, detail) {
this.setState({
icon: "hand",
message,
detail,
})
}
@autobind
showYesNo(message, onDismiss) {
this.setState({
yesNoModal: {
question: message,
onDismiss,
},
})
}
@autobind
removeUnfinishedNewItem() {
let items = this.state.items
if (items.length > 0 && !items[0]._id) {
this.setState({ items: this.state.items.slice(1) })
}
}
@autobind
handleItemListClick(e, index) {
let item = this.state.items[index]
const { history } = this.props
if (this.state.modified) {
this.nextSelectedItem = item
this.showYesNo(
`This ${
this.props.name
} has been modified. Are you sure you would like to navigate away?`,
this.handleModifiedModalDismiss
)
} else {
this.setState({ selectedItem: item })
this.removeUnfinishedNewItem()
history.replace(`${history.location.pathname}?id=${item._id}`)
}
}
@autobind
handleSave(item) {
if (item._id) {
this.showWait(`Updating ${this.capitalizedName}`)
this.props
.updateItem(item)
.then((updatedItem) => {
this.hideWait()
this.setState({
items: this.state.items.map(
(item) => (item._id === updatedItem._id ? updatedItem : item)
),
modified: false,
selectedItem: updatedItem,
})
})
.catch((error) => {
this.hideWait()
this.showErrorMessage(
"Unable to save the item changes",
error.message
)
})
} else {
this.showWait(`Creating ${this.capitalizedName}`)
this.props
.createItem(item)
.then((createdItem) => {
this.hideWait()
this.setState({
items: this.state.items
.map((item) => (!item._id ? createdItem : item))
.sort(this.props.sort),
modified: false,
selectedItem: createdItem,
})
})
.catch((error) => {
this.hideWait()
this.showErrorMessage("Unable to create the item.", error.message)
})
}
}
@autobind
handleRemove() {
this.showYesNo(
`Are you sure you want to remove this ${this.props.name}?`,
this.handleRemoveModalDismiss
)
}
@autobind
handleRemoveModalDismiss(yes) {
if (yes) {
const selectedItemId = this.state.selectedItem._id
const selectedIndex = this.state.items.findIndex(
(item) => item._id === selectedItemId
)
if (selectedIndex >= 0) {
this.showWait(`Removing ${this.capitalizedName}`)
this.props
.deleteItem(selectedItemId)
.then(() => {
this.hideWait()
this.setState({
items: [
...this.state.items.slice(0, selectedIndex),
...this.state.items.slice(selectedIndex + 1),
],
selectedItem: null,
})
})
.catch((error) => {
this.hideWait()
this.showErrorMessage(
`Unable to remove the ${this.props.name}.`,
error.message
)
})
}
}
this.setState({
yesNoModal: null,
})
}
@autobind
handleModifiedModalDismiss(yes) {
if (yes) {
this.setState({
selectedItem: this.nextSelectedItem,
modified: false,
})
this.removeUnfinishedNewItem()
delete this.nextSelectedItem
}
this.setState({
yesNoModal: null,
})
}
@autobind
handleMessageModalDismiss() {
this.setState({ messageModal: null })
}
@autobind
handleModifiedChanged(modified) {
this.setState({ modified: modified })
}
@autobind
handleAddNewItem() {
let items = this.state.items
if (items.length > 0 && !items[0]._id) {
// Already adding a new item
return
}
let newItem = {}
let newItems = [newItem].concat(this.state.items)
this.setState({ items: newItems, selectedItem: newItem })
}
render() {
const {
messageModal,
yesNoModal,
waitModal,
items,
selectedItem,
modified,
} = this.state
const { name } = this.props
return (
<Fragment>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item grow>
<Row fillParent>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item width="25vw">
<MasterList
capitalizedName={this.capitalizedName}
items={items}
selectedItem={selectedItem}
selectionModified={modified}
onItemListClick={this.handleItemListClick}
onAddNewItem={this.handleAddNewItem}
listData={this.props.listData}
/>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item grow>
<Box
border={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
radius={sizeInfo.formBoxRadius}>
{this.state.selectedItem ? (
React.createElement(this.props.form, {
item: selectedItem,
onSave: this.handleSave,
onRemove: this.handleRemove,
onModifiedChanged: this.handleModifiedChanged,
...this.props.detailCallbacks,
})
) : (
<DetailPlaceholder name={name} />
)}
</Box>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
</Row>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing}>
<YesNoMessageModal
open={!!yesNoModal}
question={yesNoModal ? yesNoModal.question : ""}
onDismiss={yesNoModal && yesNoModal.onDismiss}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal && messageModal.detail}
onDismiss={this.handleMessageModalDismiss}
/>
<WaitModal
active={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
{this.props.children}
</Column.Item>
</Fragment>
)
}
}
+73
View File
@@ -0,0 +1,73 @@
import React from "react"
import PropTypes from "prop-types"
import { Column, List, Button } from "ui"
import { sizeInfo } from "ui/style"
export class MasterList extends React.Component {
static propTypes = {
capitalizedName: PropTypes.string,
items: PropTypes.array,
listData: PropTypes.func,
onItemListClick: PropTypes.func,
selectedItem: PropTypes.object,
selectionModified: PropTypes.bool,
onAddNewItem: PropTypes.func,
}
constructor(props) {
super(props)
this.state = {
items: null,
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.items !== this.props.items) {
this.setState({ items: nextProps.items })
}
}
render() {
const { selectedItem, selectionModified, capitalizedName } = this.props
const { items } = this.state
return (
<Column fillParent>
<Column.Item grow>
<List
items={items}
render={(item, index) => {
const data = item._id
? this.props.listData(item)
: {
icon: "blank",
text: `[New ${capitalizedName}]`,
}
return (
<List.Item
key={item._id || "0"}
onClick={(e) => this.props.onItemListClick(e, index)}
active={item === this.props.selectedItem}>
<List.Icon name={data.icon} size={sizeInfo.listIcon} />
<List.Text>{data.text}</List.Text>
{item === selectedItem && selectionModified ? (
<List.Icon name="edit" size={sizeInfo.listIcon} />
) : null}
</List.Item>
)
}}
/>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item height={sizeInfo.buttonHeight}>
<Button
width="100%"
color="inverse"
onClick={this.props.onAddNewItem}
text={`Add New ${capitalizedName}`}
/>
</Column.Item>
</Column>
)
}
}
+3
View File
@@ -0,0 +1,3 @@
export { MasterDetail } from "./MasterDetail"
export { DetailPlaceholder } from "./DetailPlaceholder"
export { MasterList } from "./MasterList"
+1 -1
View File
@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
import { Box, Image, Column, Row, Button, Link } from "ui"
import { MessageModal, WaitModal, YesNoMessageModal } from "../Modal"
import { sizeInfo, colorInfo } from "ui/style"
import headerLogo from "images/deighton.png"
import headerLogo from "images/logo.png"
import autobind from "autobind-decorator"
import { api } from "../API"
+15 -8
View File
@@ -9,7 +9,7 @@ import { api } from "src/API"
export class TeamForm extends React.Component {
static propTypes = {
team: PropTypes.object,
item: PropTypes.object,
onSave: PropTypes.func,
onRemove: PropTypes.func,
onModifiedChanged: PropTypes.func,
@@ -21,6 +21,9 @@ export class TeamForm extends React.Component {
},
start: {
isValid: (r, v) => v === "" || moment(v).isValid(),
initValue: "",
pre: (v) => (v === null ? "" : v),
post: (v) => (v === "" ? null : v),
},
remove: {
noValue: true,
@@ -43,14 +46,14 @@ export class TeamForm extends React.Component {
this.state = {
binder: new FormBinder(
props.team,
props.item,
TeamForm.bindings,
this.props.onModifiedChanged
),
users: [],
}
this.getUsersForTeam(props.team._id)
this.getUsersForTeam(props.item._id)
}
@autobind
@@ -69,16 +72,20 @@ export class TeamForm extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (nextProps.team !== this.props.team) {
if (nextProps.item !== this.props.item) {
this.setState({
binder: new FormBinder(
nextProps.team,
nextProps.item,
TeamForm.bindings,
nextProps.onModifiedChanged
),
})
this.getUsersForTeam(nextProps.team._id)
if (nextProps.item._id) {
this.getUsersForTeam(nextProps.item._id)
} else {
this.setState({ users: [] })
}
}
}
@@ -95,10 +102,10 @@ export class TeamForm extends React.Component {
@autobind
handleReset() {
const { team, onModifiedChanged } = this.props
const { item, onModifiedChanged } = this.props
this.setState({
binder: new FormBinder(team, TeamForm.bindings, onModifiedChanged),
binder: new FormBinder(item, TeamForm.bindings, onModifiedChanged),
})
if (onModifiedChanged) {
-14
View File
@@ -1,14 +0,0 @@
import React from 'react'
import { Column, Text } from 'ui'
export const TeamFormPlaceholder = () => (
<Column fillParent>
<Column.Item grow />
<Column.Item>
<Text size='large' align='center' width='100%'>Select a team to view details here</Text>
<br />
<Text size='small' align='center' width='100%'>Or 'Add New Team'</Text>
</Column.Item>
<Column.Item grow />
</Column>
)
-55
View File
@@ -1,55 +0,0 @@
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 = {
teams: PropTypes.array,
onTeamListClick: PropTypes.func,
selectedTeam: PropTypes.object,
selectionModified: PropTypes.bool,
onAddNewTeam: PropTypes.func
}
constructor(props) {
super(props)
this.state = {
teams: null
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.teams !== this.props.teams) {
this.setState({ teams: nextProps.teams })
}
}
render() {
const { selectedTeam, selectionModified } = this.props
const { teams } = this.state
return (
<Column fillParent>
<Column.Item grow>
<List items={teams} render={(team, index) => {
return (
<List.Item key={team._id || '0'} onClick={(e) => (this.props.onTeamListClick(e, index))}
active={team === selectedTeam}>
<List.Icon name='team' size={sizeInfo.listIcon} />
<List.Text>
{ team._id ? team.name : '[New Team]' }
</List.Text>
{ team === selectedTeam && selectionModified ? <List.Icon name='edit' size={sizeInfo.listIcon} /> : null }
</List.Item>
)
}} />
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item height={sizeInfo.buttonHeight}>
<Button width='100%' color='inverse' onClick={this.props.onAddNewTeam} text='Add New Team' />
</Column.Item>
</Column>
)
}
}
+18 -293
View File
@@ -1,306 +1,31 @@
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import autobind from "autobind-decorator"
import { TeamList } from "./TeamList"
import React, { Component } from "react"
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"
import { MasterDetail } from "../MasterDetail"
import PropTypes from "prop-types"
export class Teams extends Component {
static propTypes = {
changeTitle: PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.state = {
modified: false,
selectedTeam: null,
teams: [],
yesNoModal: null,
messageModal: null,
waitModal: null,
changeEmailModal: null,
}
}
componentDidMount() {
this.props.changeTitle("Teams")
api
.listTeams()
.then((list) => {
list.items.sort((teamA, teamB) => teamA.name.localeCompare(teamB.name))
this.setState({ teams: list.items })
})
.catch((error) => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to get the list of teams.",
detail: error.message,
},
})
})
}
componentWillUnmount() {
this.props.changeTitle("")
}
removeUnfinishedNewTeam() {
let teams = this.state.teams
if (teams.length > 0 && !teams[0]._id) {
this.setState({ teams: this.state.teams.slice(1) })
}
}
@autobind
handleTeamListClick(e, index) {
let team = this.state.teams[index]
if (this.state.modified) {
this.nextSelectedTeam = team
this.setState({
yesNoModal: {
question:
"This team has been modified. Are you sure you would like to navigate away?",
onDismiss: this.handleModifiedModalDismiss,
},
})
} else {
this.setState({ selectedTeam: team })
this.removeUnfinishedNewTeam()
}
}
@autobind
handleSave(team) {
if (team._id) {
this.setState({ waitModal: { message: "Updating Team" } })
api
.updateTeam(team)
.then((updatedTeam) => {
this.setState({
waitModal: null,
teams: this.state.teams.map(
(team) => (team._id === updatedTeam._id ? updatedTeam : team)
),
modified: false,
selectedTeam: updatedTeam,
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to save the team changes",
detail: error.message,
},
})
})
} else {
this.setState({ waitModal: { message: "Creating Team" } })
api
.createTeam(team)
.then((createdTeam) => {
this.setState({
waitModal: false,
teams: this.state.teams
.map((team) => (!team._id ? createdTeam : team))
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)),
modified: false,
selectedTeam: createdTeam,
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to create the team.",
detail: error.message,
},
})
})
}
}
@autobind
handleChangeEmail() {
this.setState({
changeEmailModal: { oldEmail: this.state.selectedTeam.email },
})
}
@autobind
handleRemove() {
this.setState({
yesNoModal: {
question:
"Are you sure you want to remove this team? 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.teams.findIndex(
(team) => team._id === selectedTeamId
)
if (selectedIndex >= 0) {
this.setState({ waitModal: { message: "Removing Team" } })
api
.deleteTeam(selectedTeamId)
.then(() => {
this.setState({
waitModal: null,
teams: [
...this.state.teams.slice(0, selectedIndex),
...this.state.teams.slice(selectedIndex + 1),
],
selectedTeam: null,
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to remove the team.",
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 teams = this.state.teams
if (teams.length > 0 && !!teams[0]._id) {
let newTeam = {}
let newTeams = [newTeam].concat(this.state.teams)
this.setState({ teams: newTeams, selectedTeam: newTeam })
}
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
render() {
const { messageModal, yesNoModal, changeEmailModal } = this.state
return (
<Fragment>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item grow>
<Row fillParent>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item width="25vw">
<TeamList
teams={this.state.teams}
selectedTeam={this.state.selectedTeam}
selectionModified={this.state.modified}
onTeamListClick={this.handleTeamListClick}
onAddNewTeam={this.handleAddNewTeam}
<MasterDetail
history={this.props.history}
ref={(ref) => (this.masterDetail = ref)}
name="team"
form={TeamForm}
listItems={api.listTeams}
updateItem={api.updateTeam}
createItem={api.createTeam}
deleteItem={api.deleteTeam}
sort={(a, b) => a.name.localeCompare(b.name)}
listData={(team) => ({
icon: "team",
text: team.name,
})}
/>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item grow>
<Box
border={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
radius={sizeInfo.formBoxRadius}>
{this.state.selectedTeam ? (
<TeamForm
team={this.state.selectedTeam}
onSave={this.handleSave}
onRemove={this.handleRemove}
onModifiedChanged={this.handleModifiedChanged}
onChangeEmail={this.handleChangeEmail}
onResendEmail={this.handleResendEmail}
/>
) : (
<TeamFormPlaceholder />
)}
</Box>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
</Row>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing}>
<ChangeEmailModal
open={!!changeEmailModal}
oldEmail={changeEmailModal && changeEmailModal.oldEmail}
onDismiss={this.handleChangeEmailDismiss}
/>
<YesNoMessageModal
open={!!yesNoModal}
question={yesNoModal ? yesNoModal.question : ""}
onDismiss={yesNoModal && yesNoModal.onDismiss}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal && messageModal.detail}
onDismiss={this.handleMessageModalDismiss}
/>
<WaitModal
active={!!this.state.waitModal}
message={this.state.waitModal ? this.state.waitModal.message : ""}
/>
</Column.Item>
</Fragment>
)
}
}
+8 -8
View File
@@ -17,7 +17,7 @@ import { sizeInfo } from "ui/style"
export class UserForm extends React.Component {
static propTypes = {
user: PropTypes.object,
item: PropTypes.object,
onSave: PropTypes.func,
onRemove: PropTypes.func,
onModifiedChanged: PropTypes.func,
@@ -83,7 +83,7 @@ export class UserForm extends React.Component {
super(props)
this.state = {
binder: new FormBinder(
props.user,
props.item,
UserForm.bindings,
props.onModifiedChanged
),
@@ -111,10 +111,10 @@ export class UserForm extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (nextProps.user !== this.props.user) {
if (nextProps.item !== this.props.item) {
this.setState({
binder: new FormBinder(
nextProps.user,
nextProps.item,
UserForm.bindings,
nextProps.onModifiedChanged
),
@@ -137,10 +137,10 @@ export class UserForm extends React.Component {
@autobind
handleReset() {
const { user, onModifiedChanged } = this.props
const { item, onModifiedChanged } = this.props
this.setState({
binder: new FormBinder(user, UserForm.bindings, onModifiedChanged),
binder: new FormBinder(item, UserForm.bindings, onModifiedChanged),
})
if (onModifiedChanged) {
@@ -179,7 +179,7 @@ export class UserForm extends React.Component {
return (
<form
style={{ width: "100%", height: "100%", overflow: "scroll" }}
id="userForm"
id="UserForm"
onSubmit={this.handleSubmit}>
<Column>
<Column.Item height={sizeInfo.formColumnSpacing} />
@@ -303,7 +303,7 @@ export class UserForm extends React.Component {
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item>
<BoundButton
submit="userForm"
submit="UserForm"
text={binder._id ? "Save" : "Add"}
name="submit"
binder={binder}
-14
View File
@@ -1,14 +0,0 @@
import React from 'react'
import { Column, Text } from 'ui'
export const UserFormPlaceholder = () => (
<Column fillParent>
<Column.Item grow />
<Column.Item>
<Text size='large' align='center' width='100%'>Select a registered user to view details here</Text>
<br />
<Text size='small' align='center' width='100%'>Or 'Add New User'</Text>
</Column.Item>
<Column.Item grow />
</Column>
)
-55
View File
@@ -1,55 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Column, List, Button } from 'ui'
import { sizeInfo } from 'ui/style'
export class UserList 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 (
<Column fillParent>
<Column.Item grow>
<List items={users} render={(user, index) => {
return (
<List.Item key={user._id || '0'} onClick={(e) => (this.props.onUserListClick(e, index))}
active={user === this.props.selectedUser}>
<List.Icon name={user.administrator ? 'admin' : 'profile'} size={sizeInfo.listIcon} />
<List.Text>
{ user._id ? user.firstName + ' ' + user.lastName : '[New User]' }
</List.Text>
{ user === selectedUser && selectionModified ? <List.Icon name='edit' size={sizeInfo.listIcon} /> : null }
</List.Item>
)
}} />
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item height={sizeInfo.buttonHeight}>
<Button width='100%' color='inverse' onClick={this.props.onAddNewUser} text='Add New User' />
</Column.Item>
</Column>
)
}
}
+55 -333
View File
@@ -1,146 +1,23 @@
import React, { Component, Fragment } from "react"
import PropTypes from "prop-types"
import React, { Component } from "react"
import autobind from "autobind-decorator"
import { UserList } from "./UserList"
import { UserForm } from "./UserForm"
import { UserFormPlaceholder } from "./UserFormPlaceholder"
import { api } from "src/API"
import { Row, Column, Box } from "ui"
import {
YesNoMessageModal,
MessageModal,
ChangeEmailModal,
WaitModal,
} from "../Modal"
import { sizeInfo, colorInfo } from "ui/style"
import { ChangeEmailModal } from "../Modal"
import { MasterDetail } from "../MasterDetail"
import PropTypes from "prop-types"
export class Users extends Component {
static propTypes = {
changeTitle: PropTypes.func.isRequired,
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
constructor(props) {
super(props)
this.state = {
modified: false,
selectedUser: null,
users: [],
yesNoModal: null,
messageModal: null,
waitModal: null,
changeEmailModal: null,
}
}
componentDidMount() {
this.props.changeTitle("Users")
api
.listUsers()
.then((list) => {
list.items.sort((userA, userB) =>
userA.lastName.localeCompare(userB.lastName)
)
this.setState({ users: list.items })
})
.catch((error) => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to get the list of users.",
detail: error.message,
},
})
})
}
componentWillUnmount() {
this.props.changeTitle("")
}
removeUnfinishedNewUser() {
let users = this.state.users
if (users.length > 0 && !users[0]._id) {
this.setState({ users: this.state.users.slice(1) })
}
}
@autobind
handleUserListClick(e, index) {
let user = this.state.users[index]
if (this.state.modified) {
this.nextSelectedUser = 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({ selectedUser: user })
this.removeUnfinishedNewUser()
}
}
@autobind
handleSave(user) {
if (user._id) {
this.setState({ waitModal: { message: "Updating User" } })
api
.updateUser(user)
.then((updatedUser) => {
this.setState({
waitModal: null,
users: this.state.users.map(
(user) => (user._id === updatedUser._id ? updatedUser : user)
),
modified: false,
selectedUser: updatedUser,
})
})
.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 User" } })
api
.createUser(user)
.then((createdUser) => {
this.setState({
waitModal: false,
users: this.state.users
.map((user) => (!user._id ? createdUser : user))
.sort(
(a, b) =>
((a.lastName < b.lastName ? -1 : a.lastName > b.lastName): 0)
),
modified: false,
selectedUser: createdUser,
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to create the user.",
detail: error.message,
},
})
})
}
}
@autobind
handleChangeEmail() {
this.setState({
@@ -154,57 +31,41 @@ export class Users extends Component {
api
.sendResetPassword(this.state.selectedUser.email)
.then(() => {
this.setState({
waitModal: null,
messageModal: {
icon: "thumb",
message: `An email has been sent to '${
this.state.selectedUser.email
}' with instructions on how to reset their password`,
},
})
this.masterDetail.hideWait()
this.masterDetail.showMessage(
`An email has been sent to '${
this.masterDetail.selectedItem.email
}' with instructions on how to reset their password`
)
})
.catch((error) => {
this.setState({
error: true,
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to request password reset.",
detail: error.message,
},
})
this.masterDetail.hideWait()
this.masterDetail.showErrorMessage(
"Unable to request password reset.",
error.message
)
})
}
@autobind
handleResendEmail() {
this.setState({
waitModal: { message: "Resending Email..." },
})
this.masterDetail.showWait("Resending Email...")
api
.sendConfirmEmail({ existingEmail: this.state.selectedUser.email })
.sendConfirmEmail({ existingEmail: this.masterDetail.selectedItem.email })
.then(() => {
this.setState({
waitModal: null,
messageModal: {
icon: "thumb",
message: `An email has been sent to '${
this.state.selectedUser.email
}' with further instructions.`,
},
})
this.masterDetail.hideWait()
this.masterDetail.showMessage(
`An email has been sent to '${
this.masterDetail.selectedItem.email
}' with further instructions.`
)
})
.catch((error) => {
this.setState({
error: true,
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to request email change.",
detail: error.message,
},
})
this.masterDetail.hideWait()
this.masterDetail.showErrorMessage(
"Unable to request email change.",
error.message
)
})
}
@@ -224,187 +85,48 @@ export class Users extends Component {
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 selectedUserId = this.state.selectedUser._id
const selectedIndex = this.state.users.findIndex(
(user) => user._id === selectedUserId
this.masterDetail.showMessage(
`An email has been sent to '${newEmail}' to confirm this email.`
)
if (selectedIndex >= 0) {
this.setState({ waitModal: { message: "Removing User" } })
api
.deleteUser(selectedUserId)
.then(() => {
this.setState({
waitModal: null,
users: [
...this.state.users.slice(0, selectedIndex),
...this.state.users.slice(selectedIndex + 1),
],
selectedUser: null,
})
})
.catch((error) => {
this.setState({
waitModal: null,
messageModal: {
icon: "hand",
message: "Unable to remove the user.",
detail: error.message,
},
this.masterDetail.showErrorMessage(
"Unable to request email change.",
error.message
)
})
})
}
}
this.setState({
yesNoModal: null,
})
}
@autobind
handleModifiedModalDismiss(yes) {
if (yes) {
this.setState({
selectedUser: this.nextSelectedUser,
modified: false,
})
this.removeUnfinishedNewUser()
delete this.nextSelectedUser
}
this.setState({
yesNoModal: null,
})
}
@autobind
handleMessageModalDismiss() {
this.setState({ messageModal: null })
}
@autobind
handleModifiedChanged(modified) {
this.setState({ modified: modified })
}
@autobind
handleAddNewUser() {
let users = this.state.users
if (users.length > 0 && !!users[0]._id) {
let newUser = {}
let newUsers = [newUser].concat(users)
this.setState({ users: newUsers, selectedUser: newUser })
}
}
render() {
const { messageModal, yesNoModal, changeEmailModal } = this.state
const { changeEmailModal } = this.state
return (
<Fragment>
<Column.Item height={sizeInfo.formColumnSpacing} />
<Column.Item grow>
<Row fillParent>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item width="25vw">
<UserList
users={this.state.users}
selectedUser={this.state.selectedUser}
selectionModified={this.state.modified}
onUserListClick={this.handleUserListClick}
onAddNewUser={this.handleAddNewUser}
/>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
<Row.Item grow>
<Box
border={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
<MasterDetail
history={this.props.history}
ref={(ref) => (this.masterDetail = ref)}
name="user"
form={UserForm}
listItems={api.listUsers}
updateItem={api.updateUser}
createItem={api.createUser}
deleteItem={api.deleteUser}
detailCallbacks={{
onChangeEmail: this.handleChangeEmail,
onResendEmail: this.handleResendEmail,
onResetPassword: this.handleSendPasswordReset,
}}
radius={sizeInfo.formBoxRadius}>
{this.state.selectedUser ? (
<UserForm
user={this.state.selectedUser}
onSave={this.handleSave}
onRemove={this.handleRemove}
onModifiedChanged={this.handleModifiedChanged}
onChangeEmail={this.handleChangeEmail}
onResendEmail={this.handleResendEmail}
onResetPassword={this.handleSendPasswordReset}
/>
) : (
<UserFormPlaceholder />
)}
</Box>
</Row.Item>
<Row.Item width={sizeInfo.formRowSpacing} />
</Row>
</Column.Item>
<Column.Item height={sizeInfo.formColumnSpacing}>
sort={(a, b) => 0}
listData={(user) => ({
icon: user.administrator ? "admin" : "profile",
text: user.firstName + " " + user.lastName,
})}>
<ChangeEmailModal
open={!!changeEmailModal}
oldEmail={changeEmailModal && changeEmailModal.oldEmail}
onDismiss={this.handleChangeEmailDismiss}
/>
<YesNoMessageModal
open={!!yesNoModal}
question={yesNoModal ? yesNoModal.question : ""}
onDismiss={yesNoModal && yesNoModal.onDismiss}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal && messageModal.detail}
onDismiss={this.handleMessageModalDismiss}
/>
<WaitModal
active={!!this.state.waitModal}
message={this.state.waitModal ? this.state.waitModal.message : ""}
/>
</Column.Item>
</Fragment>
</MasterDetail>
)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+25
View File
@@ -0,0 +1,25 @@
import React, { Component } from "react"
import { Box, Text } from "."
import PropTypes from "prop-types"
import { colorInfo, sizeInfo } from "./style"
export class Footer extends Component {
static propTypes = {
text: PropTypes.string,
}
render() {
return (
<Box
background={colorInfo.headerButtonBackground}
borderTop={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}>
<Text color="dimmed" margin={sizeInfo.footerTextMargin}>
{this.props.text}
</Text>
</Box>
)
}
}
+169
View File
@@ -0,0 +1,169 @@
import React, { Component } from "react"
import Radium from "radium"
import PropTypes from "prop-types"
import { Icon, Image, Box, Row } from "."
import { colorInfo, sizeInfo, fontInfo } from "./style"
export class Header extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
location: PropTypes.object,
left: PropTypes.arrayOf(PropTypes.object),
right: PropTypes.arrayOf(PropTypes.object),
}
render() {
const { location } = this.props
const renderHeaderitem = (item, index) => {
if (item.image) {
return (
<Header.Button
key={index}
image={item.image}
onClick={() => this.props.history.push(item.path)}
/>
)
} else if (item.icon) {
return (
<Header.Button
key={index}
icon={item.icon}
onClick={() => this.props.history.push(item.path)}
/>
)
} else if (item.text) {
return (
<Header.TextButton
key={index}
active={location.pathname.endsWith(item.path)}
text={item.text}
onClick={() => this.props.history.push(item.path)}
/>
)
}
}
return (
<Box
background={colorInfo.headerButtonBackground}
borderBottom={{
width: sizeInfo.headerBorderWidth,
color: colorInfo.headerBorder,
}}
style={{ boxSizing: "content" }}>
<Row fillParent>
<Row.Item>{this.props.left.map(renderHeaderitem)}</Row.Item>
<Row.Item grow />
<Row.Item>{this.props.right.map(renderHeaderitem)}</Row.Item>
</Row>
</Box>
)
}
}
Header.Button = Radium(
class HeaderButton extends Component {
static propTypes = {
onClick: PropTypes.func,
icon: PropTypes.string,
image: PropTypes.string,
}
static style = {
background: colorInfo.headerButtonBackground,
verticalAlign: "middle",
borderWidth: 0,
padding: "0 0 0 0",
outline: "none",
":hover": {
background: colorInfo.headerButtonBackgroundHover,
},
":active": {
background: colorInfo.headerButtonBackgroundActive,
},
}
render() {
// Times two to account for zooming
const size = sizeInfo.headerHeight - sizeInfo.headerBorderWidth
const { onClick, icon, image } = this.props
let content = null
if (image) {
content = (
<Image
source={image}
width={size}
height={size}
margin={sizeInfo.headerButtonMargin}
/>
)
} else if (icon) {
content = (
<Icon name={icon} size={size} margin={sizeInfo.headerButtonMargin} />
)
}
return (
<button
type="button"
style={[{ height: size, width: size }, HeaderButton.style]}
onClick={onClick}>
{content}
</button>
)
}
}
)
Header.TextButton = Radium(
class HeaderTextButton extends Component {
static propTypes = {
active: PropTypes.bool,
text: PropTypes.string,
onClick: PropTypes.func,
}
static style = {
display: "inline-block",
fontSize: fontInfo.size.header,
fontFamily: fontInfo.family,
color: fontInfo.color.normal,
textAlign: "left",
outline: "none",
background: "transparent",
verticalAlign: "middle",
borderWidth: 0,
paddingLeft: sizeInfo.headerSpacing,
paddingRight: sizeInfo.headerSpacing,
paddingTop: 0,
paddingBottom: 0,
":hover": {
background: colorInfo.headerButtonBackgroundHover,
},
":active": {
background: colorInfo.headerButtonBackgroundActive,
},
}
render() {
const height = sizeInfo.headerHeight - sizeInfo.headerBorderWidth
const { text, active, onClick } = this.props
return (
<button
type="button"
style={[{ height }, HeaderTextButton.style]}
onClick={onClick}>
<div
style={{
textDecoration: active ? "underline" : "initial",
}}>
{text}
</div>
</button>
)
}
}
)
-46
View File
@@ -1,46 +0,0 @@
import Radium from 'radium'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { Icon, Image } from '.'
import { colorInfo, sizeInfo } from 'ui/style'
@Radium
export class HeaderButton extends Component {
static propTypes = {
onClick: PropTypes.func,
icon: PropTypes.string,
image: PropTypes.string,
}
static style = {
background: colorInfo.headerButtonBackground,
verticalAlign: 'middle',
borderWidth: 0,
padding: '0 0 0 0',
outline: 'none',
':hover': {
background: colorInfo.headerButtonBackgroundHover,
},
':active': {
background: colorInfo.headerButtonBackgroundActive,
}
}
render() {
const size = sizeInfo.headerHeight - 2 * sizeInfo.headerBorderWidth // Times two to account for zooming
const { onClick, icon, image } = this.props
let content = null
if (image) {
content = (<Image source={image} width={size} height={size} margin={sizeInfo.headerButtonMargin} />)
} else if (icon) {
content = (<Icon name={icon} size={size} margin={sizeInfo.headerButtonMargin} />)
}
return (
<button type='button' style={[{ height: size, width: size }, HeaderButton.style]} onClick={onClick}>
{content}
</button>
)
}
}
-34
View File
@@ -1,34 +0,0 @@
import Radium from 'radium'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { sizeInfo, fontInfo } from 'ui/style'
@Radium
export class HeaderText extends Component {
static propTypes = {
text: PropTypes.string,
}
static style = {
position: 'relative',
top: sizeInfo.headerTextOffset,
display: 'inline-block',
fontSize: fontInfo.size.header,
fontFamily: fontInfo.family,
color: fontInfo.color.normal,
textAlign: 'left',
background: 'transparent',
verticalAlign: 'middle',
borderWidth: 0,
paddingLeft: sizeInfo.headerPaddingLeft,
}
render() {
const height = sizeInfo.headerHeight - sizeInfo.headerBorderWidth
const { text } = this.props
return (
<div style={[{ height }, HeaderText.style]}>{text}</div>
)
}
}
+16 -15
View File
@@ -21,21 +21,22 @@ export class Icon extends Component {
}
static svgs = {
logout: require("icons/logout.svg"),
thumb: require("icons/thumb.svg"),
profile: require("icons/profile.svg"),
admin: require("icons/admin.svg"),
hand: require("icons/hand.svg"),
users: require("icons/users.svg"),
team: require("icons/team.svg"),
teams: require("icons/teams.svg"),
system: require("icons/system.svg"),
confirmed: require("icons/confirmed.svg"),
help: require("icons/help.svg"),
warning: require("icons/warning.svg"),
edit: require("icons/edit.svg"),
placeholder: require("icons/placeholder.svg"),
clock: require("icons/clock.svg"),
admin: require("./icons/admin.svg"),
blank: require("./icons/blank.svg"),
clock: require("./icons/clock.svg"),
confirmed: require("./icons/confirmed.svg"),
edit: require("./icons/edit.svg"),
hand: require("./icons/hand.svg"),
help: require("./icons/help.svg"),
logout: require("./icons/logout.svg"),
profile: require("./icons/profile.svg"),
placeholder: require("./icons/placeholder.svg"),
system: require("./icons/system.svg"),
thumb: require("./icons/thumb.svg"),
team: require("./icons/team.svg"),
teams: require("./icons/teams.svg"),
users: require("./icons/users.svg"),
warning: require("./icons/warning.svg"),
}
render() {
File renamed without changes.
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="79px" height="79px" viewBox="0 0 79 79" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>blank</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard" transform="translate(-542.000000, -375.000000)">
<g id="blank" transform="translate(542.000000, 375.000000)"></g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 610 B

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2 -2
View File
@@ -2,8 +2,8 @@ export { Anime } from "./Anime"
export { Box } from "./Box"
export { Button } from "./Button"
export { FormIconButton } from "./FormIconButton"
export { HeaderButton } from "./HeaderButton"
export { HeaderText } from "./HeaderText"
export { Header } from "./Header"
export { Footer } from "./Footer"
export { PanelButton } from "./PanelButton"
export { Checkbox } from "./Checkbox"
export { Input } from "./Input"