Integrated master/detail, refactor Icon, add base router
6
mobile/package-lock.json
generated
@@ -5104,9 +5104,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-form-binder": {
|
"react-form-binder": {
|
||||||
"version": "1.2.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-2.0.0.tgz",
|
||||||
"integrity": "sha512-VFeiB5nCe01WU5aVJILMw7GLgOPsYJvdJEL9WRz7qecKDZx30sKA5bLDOWHsWQDZhediIr3KLpFkPxj0u89tDg==",
|
"integrity": "sha512-ihqbA3sp8eOOvjN2cSWOC7pfK+ukuRW5+dgpbrDJKnH/wgJ0LSMaJg2d/lX8bc0XO7+KxRJi7mBdizvCT1qhgQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"eventemitter3": "^2.0.3",
|
"eventemitter3": "^2.0.3",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"eventemitter3": "^3.1.0",
|
"eventemitter3": "^3.1.0",
|
||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"react": "^16.3.2",
|
"react": "^16.3.2",
|
||||||
"react-form-binder": "^1.2.0",
|
"react-form-binder": "^2.0.0",
|
||||||
"react-native": "^0.55.4",
|
"react-native": "^0.55.4",
|
||||||
"react-native-image-picker": "^0.26.7",
|
"react-native-image-picker": "^0.26.7",
|
||||||
"react-native-iphone-x-helper": "^1.0.3",
|
"react-native-iphone-x-helper": "^1.0.3",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
BoundPhotoPanel,
|
BoundPhotoPanel,
|
||||||
FormStaticInput,
|
FormStaticInput,
|
||||||
} from "../ui"
|
} from "../ui"
|
||||||
import { MessageModal, WaitModal } from "../Modal"
|
import { MessageModal, WaitModal, ProgressModal } from "../Modal"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import KeyboardSpacer from "react-native-keyboard-spacer"
|
import KeyboardSpacer from "react-native-keyboard-spacer"
|
||||||
import { isIphoneX } from "react-native-iphone-x-helper"
|
import { isIphoneX } from "react-native-iphone-x-helper"
|
||||||
@@ -88,6 +88,7 @@ export class Activity extends React.Component {
|
|||||||
binder: new FormBinder({}, Activity.bindings),
|
binder: new FormBinder({}, Activity.bindings),
|
||||||
waitModal: null,
|
waitModal: null,
|
||||||
messageModal: null,
|
messageModal: null,
|
||||||
|
progressModal: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { search } = this.props.location
|
const { search } = this.props.location
|
||||||
@@ -105,7 +106,7 @@ export class Activity extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
binder: new FormBinder(
|
binder: new FormBinder(
|
||||||
{
|
{
|
||||||
...this.state.binder.getOriginalFieldValues(),
|
...this.state.binder.originalObj,
|
||||||
workItem: workItem._id,
|
workItem: workItem._id,
|
||||||
team: api.loggedInUser.team,
|
team: api.loggedInUser.team,
|
||||||
},
|
},
|
||||||
@@ -225,12 +226,35 @@ export class Activity extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleUploadStarted() {
|
handleUploadStarted() {
|
||||||
this.setState({ waitModal: { message: "Uploading Photo..." } })
|
this.setState({
|
||||||
|
progressModal: { message: "Uploading Photo..." },
|
||||||
|
uploadPercent: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleUploadEnded() {
|
handleUploadProgress(uploadData) {
|
||||||
this.setState({ waitModal: null })
|
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() {
|
render() {
|
||||||
@@ -313,11 +337,18 @@ export class Activity extends React.Component {
|
|||||||
name="photos"
|
name="photos"
|
||||||
binder={binder}
|
binder={binder}
|
||||||
onUploadStarted={this.handleUploadStarted}
|
onUploadStarted={this.handleUploadStarted}
|
||||||
|
onUploadProgress={this.handleUploadProgress}
|
||||||
onUploadEnded={this.handleUploadEnded}
|
onUploadEnded={this.handleUploadEnded}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
|
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<ProgressModal
|
||||||
|
open={!!progressModal}
|
||||||
|
message={progressModal ? progressModal.message : ""}
|
||||||
|
percent={uploadPercent}
|
||||||
|
onCancel={this.handleUploadCanceled}
|
||||||
|
/>
|
||||||
<WaitModal
|
<WaitModal
|
||||||
open={!!waitModal}
|
open={!!waitModal}
|
||||||
message={waitModal ? waitModal.message : ""}
|
message={waitModal ? waitModal.message : ""}
|
||||||
|
|||||||
@@ -1,44 +1,15 @@
|
|||||||
import passport from "passport"
|
import passport from "passport"
|
||||||
import createError from "http-errors"
|
import createError from "http-errors"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { catchAll, TeamRoutes } from "."
|
import { catchAll, TeamRoutes, BaseRoutes } from "."
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
export class ActivityRoutes {
|
export class ActivityRoutes extends BaseRoutes {
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
|
super(container, container.db.Activity)
|
||||||
|
|
||||||
const app = container.app
|
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
|
app
|
||||||
.route("/activities/all")
|
.route("/activities/all")
|
||||||
.delete(
|
.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) {
|
async deleteAllActivities(req, res, next) {
|
||||||
const Activity = this.db.Activity
|
const Activity = this.db.Activity
|
||||||
const Team = this.db.Team
|
const Team = this.db.Team
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ import B64 from "b64"
|
|||||||
import { PassThrough } from "stream"
|
import { PassThrough } from "stream"
|
||||||
import { catchAll } from "."
|
import { catchAll } from "."
|
||||||
|
|
||||||
function pipeToGridFS(readable, gfsWriteable, decoder) {
|
function pipeToGridFS(readable, writable, decoder) {
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
readable.on("error", (error) => {
|
readable.on("error", (error) => {
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
gfsWriteable.on("error", (error) => {
|
writeable.on("error", (error) => {
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
gfsWriteable.on("close", (file) => {
|
writeable.on("finish", (file) => {
|
||||||
resolve(file)
|
resolve(file)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
readable.pipe(decoder).pipe(gfsWriteable)
|
readable.pipe(decoder).pipe(writeable)
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +73,13 @@ export class AssetRoutes {
|
|||||||
assetId = assetId.slice(0, extIndex)
|
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`)
|
throw createError.NotFound(`Asset ${assetId} was not found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const file = cursor.next()
|
||||||
const ifNoneMatch = req.get("If-None-Match")
|
const ifNoneMatch = req.get("If-None-Match")
|
||||||
|
|
||||||
if (ifNoneMatch && ifNoneMatch === file.md5) {
|
if (ifNoneMatch && ifNoneMatch === file.md5) {
|
||||||
@@ -98,13 +99,13 @@ export class AssetRoutes {
|
|||||||
ETag: file.md5,
|
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) {
|
async deleteAsset(req, res, next) {
|
||||||
const assetId = req.params._id
|
const assetId = req.params._id
|
||||||
|
|
||||||
await this.db.gridfs.removeAsync({ _id: assetId })
|
await this.db.gridfs.delete(assetId)
|
||||||
|
|
||||||
res.json({})
|
res.json({})
|
||||||
}
|
}
|
||||||
@@ -235,11 +236,11 @@ export class AssetRoutes {
|
|||||||
|
|
||||||
if (uploadedChunks >= uploadData.numberOfChunks) {
|
if (uploadedChunks >= uploadData.numberOfChunks) {
|
||||||
let readable = redisReadStream(this.rs.client, uploadDataId)
|
let readable = redisReadStream(this.rs.client, uploadDataId)
|
||||||
let writeable = this.db.gridfs.createWriteStream({
|
let writeable = this.db.gridfs.openUploadStreamWithId(
|
||||||
_id: uploadId,
|
uploadId,
|
||||||
filename: uploadData.fileName,
|
uploadData.fileName,
|
||||||
content_type: uploadData.contentType,
|
{ contentType: uploadData.contentType }
|
||||||
})
|
)
|
||||||
|
|
||||||
const decoder =
|
const decoder =
|
||||||
uploadData.chunkContentType === "application/base64"
|
uploadData.chunkContentType === "application/base64"
|
||||||
|
|||||||
147
server/src/api/routes/BaseRoutes.js
Normal 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({})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,45 +3,14 @@ import createError from "http-errors"
|
|||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import zlib from "zlib"
|
import zlib from "zlib"
|
||||||
import { Readable } from "stream"
|
import { Readable } from "stream"
|
||||||
import { catchAll } from "."
|
import { catchAll, BaseRoutes } from "."
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
export class TeamRoutes {
|
export class TeamRoutes extends BaseRoutes {
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
const app = container.app
|
super(container, container.db.Team)
|
||||||
|
|
||||||
this.log = container.log
|
container.app
|
||||||
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
|
|
||||||
.route("/teams/status")
|
.route("/teams/status")
|
||||||
.get(
|
.get(
|
||||||
passport.authenticate("bearer", { session: false }),
|
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) {
|
async getTeamStatus(req, res, next) {
|
||||||
const Team = this.db.Team
|
const Team = this.db.Team
|
||||||
const Activity = this.db.Activity
|
const Activity = this.db.Activity
|
||||||
|
|||||||
@@ -2,33 +2,14 @@ import passport from "passport"
|
|||||||
import createError from "http-errors"
|
import createError from "http-errors"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import merge from "deepmerge"
|
import merge from "deepmerge"
|
||||||
import { catchAll } from "."
|
import { catchAll, BaseRoutes } from "."
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
export class WorkItemRoutes {
|
export class WorkItemRoutes extends BaseRoutes {
|
||||||
constructor(container) {
|
constructor(container) {
|
||||||
|
super(container, container.db.WorkItem)
|
||||||
const app = container.app
|
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
|
app
|
||||||
.route("/workitems/activities")
|
.route("/workitems/activities")
|
||||||
.get(
|
.get(
|
||||||
@@ -36,17 +17,6 @@ export class WorkItemRoutes {
|
|||||||
catchAll(this.listWorkItemActivities)
|
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
|
app
|
||||||
.route("/workitems/all")
|
.route("/workitems/all")
|
||||||
.delete(
|
.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) {
|
async listWorkItemActivities(req, res, next) {
|
||||||
const WorkItem = this.db.WorkItem
|
const WorkItem = this.db.WorkItem
|
||||||
const aggregate = WorkItem.aggregate()
|
const aggregate = WorkItem.aggregate()
|
||||||
@@ -117,83 +53,6 @@ export class WorkItemRoutes {
|
|||||||
res.json({ items })
|
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) {
|
async deleteAllWorkItems(req, res, next) {
|
||||||
const Activity = this.db.Activity
|
const Activity = this.db.Activity
|
||||||
const WorkItem = this.db.WorkItem
|
const WorkItem = this.db.WorkItem
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export { BaseRoutes } from "./BaseRoutes"
|
||||||
export { AuthRoutes } from "./AuthRoutes"
|
export { AuthRoutes } from "./AuthRoutes"
|
||||||
export { AssetRoutes } from "./AssetRoutes"
|
export { AssetRoutes } from "./AssetRoutes"
|
||||||
export { UserRoutes } from "./UserRoutes"
|
export { UserRoutes } from "./UserRoutes"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import mongoose from "mongoose"
|
import mongoose from "mongoose"
|
||||||
import mongodb from "mongodb"
|
import mongodb from "mongodb"
|
||||||
import Grid from "gridfs-stream"
|
import { GridFSBucket } from "mongodb"
|
||||||
import mergePlugin from "mongoose-doc-merge"
|
import mergePlugin from "mongoose-doc-merge"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import * as Schemas from "./schemas"
|
import * as Schemas from "./schemas"
|
||||||
@@ -18,9 +18,7 @@ export class DB {
|
|||||||
autoIndex: !isProduction,
|
autoIndex: !isProduction,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.gridfs = Grid(connection.db, mongoose.mongo)
|
this.gridfs = new GridFSBucket(connection.db)
|
||||||
this.gridfs.findOneAsync = util.promisify(this.gridfs.findOne)
|
|
||||||
this.gridfs.removeAsync = util.promisify(this.gridfs.remove)
|
|
||||||
|
|
||||||
this.User = connection.model("User", Schemas.userSchema)
|
this.User = connection.model("User", Schemas.userSchema)
|
||||||
this.WorkItem = connection.model("WorkItem", Schemas.workItemSchema)
|
this.WorkItem = connection.model("WorkItem", Schemas.workItemSchema)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"space-before-function-paren": ["error", "never"],
|
"space-before-function-paren": ["error", "never"],
|
||||||
"comma-dangle": ["error", "only-multiline"],
|
"comma-dangle": ["error", "only-multiline"],
|
||||||
"jsx-quotes": "off",
|
"jsx-quotes": "off",
|
||||||
"quotes": "off"
|
"quotes": "off",
|
||||||
|
"indent": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
website/package-lock.json
generated
@@ -14167,9 +14167,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-form-binder": {
|
"react-form-binder": {
|
||||||
"version": "1.2.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-form-binder/-/react-form-binder-2.0.0.tgz",
|
||||||
"integrity": "sha512-VFeiB5nCe01WU5aVJILMw7GLgOPsYJvdJEL9WRz7qecKDZx30sKA5bLDOWHsWQDZhediIr3KLpFkPxj0u89tDg==",
|
"integrity": "sha512-ihqbA3sp8eOOvjN2cSWOC7pfK+ukuRW5+dgpbrDJKnH/wgJ0LSMaJg2d/lX8bc0XO7+KxRJi7mBdizvCT1qhgQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"eventemitter3": "^2.0.3",
|
"eventemitter3": "^2.0.3",
|
||||||
"prop-types": "^15.5.10",
|
"prop-types": "^15.5.10",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"radium": "^0.22.0",
|
"radium": "^0.22.0",
|
||||||
"react": "^16.2.0",
|
"react": "^16.2.0",
|
||||||
"react-dom": "^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",
|
"react-router-dom": "^4.1.1",
|
||||||
"regexp-pattern": "^1.0.4",
|
"regexp-pattern": "^1.0.4",
|
||||||
"socket.io-client": "^2.0.3"
|
"socket.io-client": "^2.0.3"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3"
|
|||||||
import io from "socket.io-client"
|
import io from "socket.io-client"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
|
|
||||||
const authTokenName = "AuthToken"
|
const authTokenKeyName = "AuthToken"
|
||||||
|
|
||||||
class NetworkError extends Error {
|
class NetworkError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
@@ -33,30 +33,31 @@ class APIError extends Error {
|
|||||||
class API extends EventEmitter {
|
class API extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.user = null
|
|
||||||
|
|
||||||
let token =
|
let token =
|
||||||
localStorage.getItem(authTokenName) ||
|
localStorage.getItem(authTokenKeyName) ||
|
||||||
sessionStorage.getItem(authTokenName)
|
sessionStorage.getItem(authTokenKeyName)
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.token = token
|
this._token = token
|
||||||
this.user = { pending: true }
|
this._user = { pending: true }
|
||||||
|
|
||||||
this.who()
|
this.who()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
this.user = user
|
this._user = user
|
||||||
this.connectSocket()
|
this.connectSocket()
|
||||||
this.emit("login")
|
this.emit("login")
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
localStorage.removeItem(authTokenName)
|
localStorage.removeItem(authTokenKeyName)
|
||||||
sessionStorage.removeItem(authTokenName)
|
sessionStorage.removeItem(authTokenKeyName)
|
||||||
this.token = null
|
this._token = null
|
||||||
this.user = null
|
this._user = {}
|
||||||
this.socket = null
|
this.socket = null
|
||||||
this.emit("logout")
|
this.emit("logout")
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this._user = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ class API extends EventEmitter {
|
|||||||
this.socket = io(window.location.origin, {
|
this.socket = io(window.location.origin, {
|
||||||
path: "/api/socketio",
|
path: "/api/socketio",
|
||||||
query: {
|
query: {
|
||||||
auth_token: this.token,
|
auth_token: this._token,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.socket.on("disconnect", (reason) => {
|
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
|
// Filter the few massages that affect our cached user data to avoid a server round trip
|
||||||
switch (eventName) {
|
switch (eventName) {
|
||||||
case "newThumbnailImage":
|
case "newThumbnailImage":
|
||||||
this.user.thumbnailImageId = eventData.imageId
|
this._user.thumbnailImageId = eventData.imageId
|
||||||
break
|
break
|
||||||
case "newProfileImage":
|
case "newProfileImage":
|
||||||
this.user.imageId = eventData.imageId
|
this._user.imageId = eventData.imageId
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
// Nothing to see here...
|
// Nothing to see here...
|
||||||
@@ -99,15 +100,15 @@ class API extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get loggedInUser() {
|
get loggedInUser() {
|
||||||
return this.user
|
return this._user
|
||||||
}
|
}
|
||||||
|
|
||||||
makeImageUrl(id, size) {
|
makeImageUrl(id, size) {
|
||||||
if (id) {
|
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) {
|
} else if (size && size.width && size.height) {
|
||||||
return `/api/placeholders/${size.width}x${size.height}?access_token=${
|
return `/api/placeholders/${size.width}x${size.height}?access_token=${
|
||||||
this.token
|
this._token
|
||||||
}`
|
}`
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
@@ -115,11 +116,11 @@ class API extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeAssetUrl(id) {
|
makeAssetUrl(id) {
|
||||||
return id ? "/api/assets/" + id + "?access_token=" + this.token : null
|
return id ? "/api/assets/" + id + "?access_token=" + this._token : null
|
||||||
}
|
}
|
||||||
|
|
||||||
makeTeamStatusUrl() {
|
makeTeamStatusUrl() {
|
||||||
return `/api/teams/status?access_token=${this.token}`
|
return `/api/teams/status?access_token=${this._token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeParams(params) {
|
static makeParams(params) {
|
||||||
@@ -140,8 +141,8 @@ class API extends EventEmitter {
|
|||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
}
|
}
|
||||||
let headers = new Headers()
|
let headers = new Headers()
|
||||||
if (this.token) {
|
if (this._token) {
|
||||||
headers.set("Authorization", "Bearer " + this.token)
|
headers.set("Authorization", "Bearer " + this._token)
|
||||||
}
|
}
|
||||||
if (method === "POST" || method === "PUT") {
|
if (method === "POST" || method === "PUT") {
|
||||||
if (requestOptions.binary) {
|
if (requestOptions.binary) {
|
||||||
@@ -207,12 +208,12 @@ class API extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (remember) {
|
if (remember) {
|
||||||
localStorage.setItem(authTokenName, token)
|
localStorage.setItem(authTokenKeyName, token)
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.setItem(authTokenName, token)
|
sessionStorage.setItem(authTokenKeyName, token)
|
||||||
}
|
}
|
||||||
this.token = token
|
this._token = token
|
||||||
this.user = response.body
|
this._user = response.body
|
||||||
this.connectSocket()
|
this.connectSocket()
|
||||||
this.emit("login")
|
this.emit("login")
|
||||||
resolve(response.body)
|
resolve(response.body)
|
||||||
@@ -225,10 +226,10 @@ class API extends EventEmitter {
|
|||||||
logout() {
|
logout() {
|
||||||
let cb = () => {
|
let cb = () => {
|
||||||
// Regardless of response, always logout in the client
|
// Regardless of response, always logout in the client
|
||||||
localStorage.removeItem(authTokenName)
|
localStorage.removeItem(authTokenKeyName)
|
||||||
sessionStorage.removeItem(authTokenName)
|
sessionStorage.removeItem(authTokenKeyName)
|
||||||
this.token = null
|
this._token = null
|
||||||
this.user = null
|
this._user = {}
|
||||||
this.disconnectSocket()
|
this.disconnectSocket()
|
||||||
this.emit("logout")
|
this.emit("logout")
|
||||||
}
|
}
|
||||||
@@ -256,6 +257,8 @@ class API extends EventEmitter {
|
|||||||
return this.post("/auth/password/reset", passwords)
|
return this.post("/auth/password/reset", passwords)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users
|
||||||
|
|
||||||
getUser(id) {
|
getUser(id) {
|
||||||
return this.get("/users/" + id)
|
return this.get("/users/" + id)
|
||||||
}
|
}
|
||||||
@@ -271,16 +274,16 @@ class API extends EventEmitter {
|
|||||||
updateUser(user) {
|
updateUser(user) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.put("/users", user)
|
this.put("/users", user)
|
||||||
.then((user) => {
|
.then((updatedUser) => {
|
||||||
// If we just updated ourselves, update the internal cached copy
|
// If we just updated ourselves, update the internal cached copy
|
||||||
if (user._id === this.user._id) {
|
if (updatedUser._id === this._user._id) {
|
||||||
this.user = user
|
this._user = updatedUser
|
||||||
this.emit("login")
|
this.emit("login")
|
||||||
}
|
}
|
||||||
resolve(user)
|
resolve(updatedUser)
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((error) => {
|
||||||
reject(reason)
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component, Fragment } from "react"
|
import React, { Component } from "react"
|
||||||
import {
|
import {
|
||||||
Login,
|
Login,
|
||||||
Logout,
|
Logout,
|
||||||
@@ -13,160 +13,67 @@ import { Profile } from "./Profile"
|
|||||||
import { Users } from "./Users"
|
import { Users } from "./Users"
|
||||||
import { Teams } from "./Teams"
|
import { Teams } from "./Teams"
|
||||||
import { System } from "./System"
|
import { System } from "./System"
|
||||||
import { HeaderButton, HeaderText, Column, Row, Text, Box } from "ui"
|
import { Header, Column, Footer } from "ui"
|
||||||
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"
|
import { BrowserRouter, Route, Switch } from "react-router-dom"
|
||||||
import logoImage from "images/logo.png"
|
import { sizeInfo } from "ui/style"
|
||||||
import { versionInfo } from "./version"
|
|
||||||
import { sizeInfo, colorInfo } from "ui/style"
|
|
||||||
import { api } from "src/API"
|
|
||||||
import PropTypes from "prop-types"
|
import PropTypes from "prop-types"
|
||||||
import autobind from "autobind-decorator"
|
import { versionInfo } from "./version"
|
||||||
|
|
||||||
export class App extends Component {
|
export class App extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
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() {
|
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 (
|
return (
|
||||||
<Router basename="/">
|
<BrowserRouter>
|
||||||
<Column minHeight="100vh">
|
<Column minHeight="100vh">
|
||||||
<Column.Item
|
<Route
|
||||||
height={sizeInfo.headerHeight - sizeInfo.headerBorderWidth}>
|
path="/app"
|
||||||
<Box
|
render={(props) => (
|
||||||
background={colorInfo.headerButtonBackground}
|
<Column.Item height={sizeInfo.headerHeight}>
|
||||||
borderBottom={{
|
<Header
|
||||||
width: sizeInfo.headerBorderWidth,
|
{...props}
|
||||||
color: colorInfo.headerBorder,
|
left={[
|
||||||
}}
|
{ image: require("images/badge.png"), path: "/app/home" },
|
||||||
style={{ boxSizing: "content" }}>
|
{ text: "Teams", path: "/app/teams" },
|
||||||
<Row minWidth="100vw">
|
{ text: "Users", path: "/app/users" },
|
||||||
<Row.Item>{headerButtonsLeft}</Row.Item>
|
]}
|
||||||
<Row.Item grow />
|
right={[
|
||||||
<Row.Item>{headerButtonsRight}</Row.Item>
|
{ icon: "profile", path: "/app/profile" },
|
||||||
</Row>
|
{ icon: "logout", path: "/logout" },
|
||||||
</Box>
|
]}
|
||||||
</Column.Item>
|
/>
|
||||||
|
</Column.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/login" component={Login} />
|
<Route exact path="/login" component={Login} />
|
||||||
<Route exact path="/logout" component={Logout} />
|
<Route exact path="/logout" component={Logout} />
|
||||||
<Route exact path="/confirm-email" component={ConfirmEmail} />
|
<Route exact path="/confirm-email" component={ConfirmEmail} />
|
||||||
<Route exact path="/reset-password" component={ResetPassword} />
|
<Route exact path="/reset-password" component={ResetPassword} />
|
||||||
<Route exact path="/forgot-password" component={ForgotPassword} />
|
<Route exact path="/forgot-password" component={ForgotPassword} />
|
||||||
<ProtectedRoute
|
<ProtectedRoute exact path="/app/profile" component={Profile} />
|
||||||
exact
|
<ProtectedRoute exact admin path="/app/home" component={Home} />
|
||||||
path="/profile"
|
<ProtectedRoute exact admin path="/app/teams" component={Teams} />
|
||||||
render={(props) => (
|
<ProtectedRoute exact admin path="/system" component={System} />
|
||||||
<Profile {...props} changeTitle={this.handleChangeTitle} />
|
<ProtectedRoute exact admin path="/app/users" component={Users} />
|
||||||
)}
|
<DefaultRoute redirect="/app/home" />
|
||||||
/>
|
|
||||||
<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 />
|
|
||||||
</Switch>
|
</Switch>
|
||||||
<Column.Item>
|
<Route
|
||||||
<Box
|
path="/app"
|
||||||
background={colorInfo.headerButtonBackground}
|
render={() => (
|
||||||
borderTop={{
|
<Column.Item>
|
||||||
width: sizeInfo.headerBorderWidth,
|
<Footer
|
||||||
color: colorInfo.headerBorder,
|
text={
|
||||||
}}>
|
"v" + versionInfo.fullVersion + " " + versionInfo.copyright
|
||||||
<Text color="dimmed" margin={sizeInfo.footerTextMargin}>
|
}
|
||||||
{"v" + versionInfo.fullVersion} {versionInfo.copyright}
|
/>
|
||||||
</Text>
|
</Column.Item>
|
||||||
</Box>
|
)}
|
||||||
</Column.Item>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
</Router>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,14 @@
|
|||||||
import React, { Fragment, Component } from 'react'
|
import React, { Component } from "react"
|
||||||
import { api } from 'src/API'
|
import { Route, Redirect } from "react-router-dom"
|
||||||
import { Route, Redirect } from 'react-router-dom'
|
import PropTypes from "prop-types"
|
||||||
import { Column } from 'ui'
|
|
||||||
import autobind from 'autobind-decorator'
|
|
||||||
|
|
||||||
export class DefaultRoute extends Component {
|
export class DefaultRoute extends Component {
|
||||||
@autobind
|
static propTypes = {
|
||||||
updateComponent() {
|
redirect: PropTypes.string,
|
||||||
this.forceUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
api.addListener('login', this.updateComponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
api.removeListener('login', this.updateComponent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const user = api.loggedInUser
|
// NOTE: When working on the site, Redirect to the page you are working on
|
||||||
let path = null
|
return <Route render={() => <Redirect to={this.props.redirect} />} />
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import React, { Component, Fragment } from 'react'
|
import React, { Component, Fragment } from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import { regExpPattern } from 'regexp-pattern'
|
import { regExpPattern } from "regexp-pattern"
|
||||||
import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from 'ui'
|
import { Image, Text, Column, Row, BoundInput, BoundButton, Box } from "ui"
|
||||||
import { MessageModal, WaitModal } from '../Modal'
|
import { MessageModal, WaitModal } from "../Modal"
|
||||||
import { api } from 'src/API'
|
import { api } from "src/API"
|
||||||
import { FormBinder } from 'react-form-binder'
|
import { FormBinder } from "react-form-binder"
|
||||||
import headerLogo from 'images/deighton.png'
|
import headerLogo from "images/badge.png"
|
||||||
import { sizeInfo, colorInfo } from 'ui/style'
|
import { sizeInfo, colorInfo } from "ui/style"
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from "autobind-decorator"
|
||||||
|
|
||||||
export class ForgotPassword extends Component {
|
export class ForgotPassword extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
|
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
}
|
}
|
||||||
|
|
||||||
static bindings = {
|
static bindings = {
|
||||||
email: {
|
email: {
|
||||||
alwaysGet: true,
|
alwaysGet: true,
|
||||||
isValid: (r, v) => (regExpPattern.email.test(v))
|
isValid: (r, v) => regExpPattern.email.test(v),
|
||||||
},
|
},
|
||||||
submit: {
|
submit: {
|
||||||
noValue: true,
|
noValue: true,
|
||||||
isDisabled: (r) => (!r.anyModified || !r.allValid)
|
isDisabled: (r) => !r.anyModified || !r.allValid,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -30,7 +30,7 @@ export class ForgotPassword extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
binder: new FormBinder({}, ForgotPassword.bindings),
|
binder: new FormBinder({}, ForgotPassword.bindings),
|
||||||
messageModal: null,
|
messageModal: null,
|
||||||
waitModal: null
|
waitModal: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,16 +45,18 @@ export class ForgotPassword extends Component {
|
|||||||
|
|
||||||
const obj = this.state.binder.getModifiedFieldValues()
|
const obj = this.state.binder.getModifiedFieldValues()
|
||||||
|
|
||||||
this.setState({ waitModal: { message: 'Requesting Reset Email' } })
|
this.setState({ waitModal: { message: "Requesting Reset Email" } })
|
||||||
|
|
||||||
const cb = (res) => {
|
const cb = (res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
waitModal: null,
|
waitModal: null,
|
||||||
messageModal: {
|
messageModal: {
|
||||||
icon: 'thumb',
|
icon: "thumb",
|
||||||
title: 'Password Reset Requested',
|
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.`
|
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
|
@autobind
|
||||||
handleMessageModalDismiss() {
|
handleMessageModalDismiss() {
|
||||||
this.props.history.replace('/')
|
this.props.history.replace("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -77,8 +79,13 @@ export class ForgotPassword extends Component {
|
|||||||
<Row.Item grow />
|
<Row.Item grow />
|
||||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
<Row.Item width={sizeInfo.formRowSpacing} />
|
||||||
<Row.Item width={sizeInfo.modalWidth}>
|
<Row.Item width={sizeInfo.modalWidth}>
|
||||||
<form id='forgotPasswordForm' onSubmit={this.handleSubmit}>
|
<form id="forgotPasswordForm" onSubmit={this.handleSubmit}>
|
||||||
<Box border={{ width: sizeInfo.headerBorderWidth, color: colorInfo.headerBorder }} radius={sizeInfo.formBoxRadius}>
|
<Box
|
||||||
|
border={{
|
||||||
|
width: sizeInfo.headerBorderWidth,
|
||||||
|
color: colorInfo.headerBorder,
|
||||||
|
}}
|
||||||
|
radius={sizeInfo.formBoxRadius}>
|
||||||
<Row>
|
<Row>
|
||||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
<Row.Item width={sizeInfo.formRowSpacing} />
|
||||||
<Row.Item>
|
<Row.Item>
|
||||||
@@ -88,31 +95,46 @@ export class ForgotPassword extends Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Row.Item grow />
|
<Row.Item grow />
|
||||||
<Row.Item>
|
<Row.Item>
|
||||||
<Image source={headerLogo} width={sizeInfo.loginLogoWidth} />
|
<Image
|
||||||
|
source={headerLogo}
|
||||||
|
width={sizeInfo.loginLogoWidth}
|
||||||
|
/>
|
||||||
</Row.Item>
|
</Row.Item>
|
||||||
<Row.Item grow />
|
<Row.Item grow />
|
||||||
</Row>
|
</Row>
|
||||||
</Column.Item>
|
</Column.Item>
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||||
<Column.Item>
|
<Column.Item>
|
||||||
<Text size='large'>Forgotten Password</Text>
|
<Text size="large">Forgotten Password</Text>
|
||||||
</Column.Item>
|
</Column.Item>
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||||
<Column.Item>
|
<Column.Item>
|
||||||
<BoundInput label='Email' name='email'
|
<BoundInput
|
||||||
placeholder='example@xyz.com' binder={this.state.binder}
|
label="Email"
|
||||||
message='A valid email address' />
|
name="email"
|
||||||
|
placeholder="example@xyz.com"
|
||||||
|
binder={this.state.binder}
|
||||||
|
message="A valid email address"
|
||||||
|
/>
|
||||||
</Column.Item>
|
</Column.Item>
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||||
<Column.Item>
|
<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>
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||||
<Column.Item minHeight={sizeInfo.buttonHeight}>
|
<Column.Item minHeight={sizeInfo.buttonHeight}>
|
||||||
<Row>
|
<Row>
|
||||||
<Row.Item grow />
|
<Row.Item grow />
|
||||||
<Row.Item>
|
<Row.Item>
|
||||||
<BoundButton text='Submit' name='submit' submit='forgotPasswordForm' binder={binder} />
|
<BoundButton
|
||||||
|
text="Submit"
|
||||||
|
name="submit"
|
||||||
|
submit="forgotPasswordForm"
|
||||||
|
binder={binder}
|
||||||
|
/>
|
||||||
</Row.Item>
|
</Row.Item>
|
||||||
</Row>
|
</Row>
|
||||||
</Column.Item>
|
</Column.Item>
|
||||||
@@ -128,15 +150,18 @@ export class ForgotPassword extends Component {
|
|||||||
</Row>
|
</Row>
|
||||||
</Column.Item>
|
</Column.Item>
|
||||||
<Column.Item grow>
|
<Column.Item grow>
|
||||||
<WaitModal active={!!waitModal}
|
<WaitModal
|
||||||
message={waitModal ? waitModal.message : ''} />
|
active={!!waitModal}
|
||||||
|
message={waitModal ? waitModal.message : ""}
|
||||||
|
/>
|
||||||
|
|
||||||
<MessageModal
|
<MessageModal
|
||||||
open={!!messageModal}
|
open={!!messageModal}
|
||||||
icon={messageModal ? messageModal.icon : ''}
|
icon={messageModal ? messageModal.icon : ""}
|
||||||
message={messageModal ? messageModal.message : ''}
|
message={messageModal ? messageModal.message : ""}
|
||||||
detail={messageModal ? messageModal.detail : ''}
|
detail={messageModal ? messageModal.detail : ""}
|
||||||
onDismiss={this.handleMessageModalDismiss} />
|
onDismiss={this.handleMessageModalDismiss}
|
||||||
|
/>
|
||||||
</Column.Item>
|
</Column.Item>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
BoundCheckbox,
|
BoundCheckbox,
|
||||||
BoundButton,
|
BoundButton,
|
||||||
} from "ui"
|
} from "ui"
|
||||||
import headerLogo from "images/deighton.png"
|
import headerLogo from "images/logo.png"
|
||||||
import { versionInfo } from "../version"
|
import { versionInfo } from "../version"
|
||||||
import { FormBinder } from "react-form-binder"
|
import { FormBinder } from "react-form-binder"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import autobind from 'autobind-decorator'
|
|||||||
|
|
||||||
export class ProtectedRoute extends React.Component {
|
export class ProtectedRoute extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }),
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string,
|
||||||
|
search: PropTypes.string,
|
||||||
|
}),
|
||||||
admin: PropTypes.bool,
|
admin: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,26 +19,32 @@ export class ProtectedRoute extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
api.addListener('login', this.updateComponent)
|
api.addListener("login", this.updateComponent)
|
||||||
|
api.addListener("logout", this.updateComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
api.removeListener('login', this.updateComponent)
|
api.removeListener("login", this.updateComponent)
|
||||||
|
api.removeListener("logout", this.updateComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
render(props) {
|
render(props) {
|
||||||
const user = api.loggedInUser
|
const user = api.loggedInUser
|
||||||
|
|
||||||
if (user) {
|
if (user.pending) {
|
||||||
if (user.pending) {
|
return null
|
||||||
// The API might be in the middle of fetching the user information
|
} else {
|
||||||
// Return something and wait for login evint to fire to re-render
|
if (!user._id || (this.props.admin && !user.administrator)) {
|
||||||
return <div />
|
return (
|
||||||
} else if (!this.props.admin || (this.props.admin && user.administrator)) {
|
<Redirect
|
||||||
|
to={`/login?redirect=${this.props.location.pathname}${
|
||||||
|
this.props.location.search
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
return <Route {...this.props} />
|
return <Route {...this.props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { api } from "src/API"
|
|||||||
import { FormBinder } from "react-form-binder"
|
import { FormBinder } from "react-form-binder"
|
||||||
import { sizeInfo, colorInfo } from "ui/style"
|
import { sizeInfo, colorInfo } from "ui/style"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import headerLogo from "images/deighton.png"
|
import headerLogo from "images/logo.png"
|
||||||
|
|
||||||
export class ResetPassword extends Component {
|
export class ResetPassword extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import React, { Component, Fragment } from 'react'
|
import React, { Component, Fragment } from "react"
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from "prop-types"
|
||||||
import { Row, Column, PanelButton } from 'ui'
|
import { Row, Column, PanelButton } from "ui"
|
||||||
import { sizeInfo } from 'ui/style'
|
import { sizeInfo } from "ui/style"
|
||||||
|
|
||||||
export class Home extends Component {
|
export class Home extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
changeTitle: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.changeTitle('Home')
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.changeTitle('')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -25,15 +16,27 @@ export class Home extends Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Row.Item grow />
|
<Row.Item grow />
|
||||||
<Row.Item>
|
<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>
|
||||||
<Row.Item width={sizeInfo.panelButtonSpacing} />
|
<Row.Item width={sizeInfo.panelButtonSpacing} />
|
||||||
<Row.Item>
|
<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>
|
||||||
<Row.Item width={sizeInfo.panelButtonSpacing} />
|
<Row.Item width={sizeInfo.panelButtonSpacing} />
|
||||||
<Row.Item>
|
<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>
|
||||||
<Row.Item grow />
|
<Row.Item grow />
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
30
website/src/MasterDetail/DetailPlaceholder.js
Normal file
@@ -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
website/src/MasterDetail/MasterDetail.js
Normal 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
website/src/MasterDetail/MasterList.js
Normal 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
website/src/MasterDetail/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { MasterDetail } from "./MasterDetail"
|
||||||
|
export { DetailPlaceholder } from "./DetailPlaceholder"
|
||||||
|
export { MasterList } from "./MasterList"
|
||||||
@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
|
|||||||
import { Box, Image, Column, Row, Button, Link } from "ui"
|
import { Box, Image, Column, Row, Button, Link } from "ui"
|
||||||
import { MessageModal, WaitModal, YesNoMessageModal } from "../Modal"
|
import { MessageModal, WaitModal, YesNoMessageModal } from "../Modal"
|
||||||
import { sizeInfo, colorInfo } from "ui/style"
|
import { sizeInfo, colorInfo } from "ui/style"
|
||||||
import headerLogo from "images/deighton.png"
|
import headerLogo from "images/logo.png"
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { api } from "../API"
|
import { api } from "../API"
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { api } from "src/API"
|
|||||||
|
|
||||||
export class TeamForm extends React.Component {
|
export class TeamForm extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
team: PropTypes.object,
|
item: PropTypes.object,
|
||||||
onSave: PropTypes.func,
|
onSave: PropTypes.func,
|
||||||
onRemove: PropTypes.func,
|
onRemove: PropTypes.func,
|
||||||
onModifiedChanged: PropTypes.func,
|
onModifiedChanged: PropTypes.func,
|
||||||
@@ -21,6 +21,9 @@ export class TeamForm extends React.Component {
|
|||||||
},
|
},
|
||||||
start: {
|
start: {
|
||||||
isValid: (r, v) => v === "" || moment(v).isValid(),
|
isValid: (r, v) => v === "" || moment(v).isValid(),
|
||||||
|
initValue: "",
|
||||||
|
pre: (v) => (v === null ? "" : v),
|
||||||
|
post: (v) => (v === "" ? null : v),
|
||||||
},
|
},
|
||||||
remove: {
|
remove: {
|
||||||
noValue: true,
|
noValue: true,
|
||||||
@@ -43,14 +46,14 @@ export class TeamForm extends React.Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
binder: new FormBinder(
|
binder: new FormBinder(
|
||||||
props.team,
|
props.item,
|
||||||
TeamForm.bindings,
|
TeamForm.bindings,
|
||||||
this.props.onModifiedChanged
|
this.props.onModifiedChanged
|
||||||
),
|
),
|
||||||
users: [],
|
users: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getUsersForTeam(props.team._id)
|
this.getUsersForTeam(props.item._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
@@ -69,16 +72,20 @@ export class TeamForm extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.team !== this.props.team) {
|
if (nextProps.item !== this.props.item) {
|
||||||
this.setState({
|
this.setState({
|
||||||
binder: new FormBinder(
|
binder: new FormBinder(
|
||||||
nextProps.team,
|
nextProps.item,
|
||||||
TeamForm.bindings,
|
TeamForm.bindings,
|
||||||
nextProps.onModifiedChanged
|
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
|
@autobind
|
||||||
handleReset() {
|
handleReset() {
|
||||||
const { team, onModifiedChanged } = this.props
|
const { item, onModifiedChanged } = this.props
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
binder: new FormBinder(team, TeamForm.bindings, onModifiedChanged),
|
binder: new FormBinder(item, TeamForm.bindings, onModifiedChanged),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (onModifiedChanged) {
|
if (onModifiedChanged) {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,306 +1,31 @@
|
|||||||
import React, { Component, Fragment } from "react"
|
import React, { Component } from "react"
|
||||||
import PropTypes from "prop-types"
|
|
||||||
import autobind from "autobind-decorator"
|
|
||||||
import { TeamList } from "./TeamList"
|
|
||||||
import { TeamForm } from "./TeamForm"
|
import { TeamForm } from "./TeamForm"
|
||||||
import { TeamFormPlaceholder } from "./TeamFormPlaceholder"
|
|
||||||
import { api } from "src/API"
|
import { api } from "src/API"
|
||||||
import { Row, Column, Box } from "ui"
|
import { MasterDetail } from "../MasterDetail"
|
||||||
import {
|
import PropTypes from "prop-types"
|
||||||
YesNoMessageModal,
|
|
||||||
MessageModal,
|
|
||||||
ChangeEmailModal,
|
|
||||||
WaitModal,
|
|
||||||
} from "../Modal"
|
|
||||||
import { sizeInfo, colorInfo } from "ui/style"
|
|
||||||
|
|
||||||
export class Teams extends Component {
|
export class Teams extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
changeTitle: PropTypes.func.isRequired,
|
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
}
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { messageModal, yesNoModal, changeEmailModal } = this.state
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<MasterDetail
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
history={this.props.history}
|
||||||
<Column.Item grow>
|
ref={(ref) => (this.masterDetail = ref)}
|
||||||
<Row fillParent>
|
name="team"
|
||||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
form={TeamForm}
|
||||||
<Row.Item width="25vw">
|
listItems={api.listTeams}
|
||||||
<TeamList
|
updateItem={api.updateTeam}
|
||||||
teams={this.state.teams}
|
createItem={api.createTeam}
|
||||||
selectedTeam={this.state.selectedTeam}
|
deleteItem={api.deleteTeam}
|
||||||
selectionModified={this.state.modified}
|
sort={(a, b) => a.name.localeCompare(b.name)}
|
||||||
onTeamListClick={this.handleTeamListClick}
|
listData={(team) => ({
|
||||||
onAddNewTeam={this.handleAddNewTeam}
|
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { sizeInfo } from "ui/style"
|
|||||||
|
|
||||||
export class UserForm extends React.Component {
|
export class UserForm extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
user: PropTypes.object,
|
item: PropTypes.object,
|
||||||
onSave: PropTypes.func,
|
onSave: PropTypes.func,
|
||||||
onRemove: PropTypes.func,
|
onRemove: PropTypes.func,
|
||||||
onModifiedChanged: PropTypes.func,
|
onModifiedChanged: PropTypes.func,
|
||||||
@@ -83,7 +83,7 @@ export class UserForm extends React.Component {
|
|||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
binder: new FormBinder(
|
binder: new FormBinder(
|
||||||
props.user,
|
props.item,
|
||||||
UserForm.bindings,
|
UserForm.bindings,
|
||||||
props.onModifiedChanged
|
props.onModifiedChanged
|
||||||
),
|
),
|
||||||
@@ -111,10 +111,10 @@ export class UserForm extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.user !== this.props.user) {
|
if (nextProps.item !== this.props.item) {
|
||||||
this.setState({
|
this.setState({
|
||||||
binder: new FormBinder(
|
binder: new FormBinder(
|
||||||
nextProps.user,
|
nextProps.item,
|
||||||
UserForm.bindings,
|
UserForm.bindings,
|
||||||
nextProps.onModifiedChanged
|
nextProps.onModifiedChanged
|
||||||
),
|
),
|
||||||
@@ -137,10 +137,10 @@ export class UserForm extends React.Component {
|
|||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleReset() {
|
handleReset() {
|
||||||
const { user, onModifiedChanged } = this.props
|
const { item, onModifiedChanged } = this.props
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
binder: new FormBinder(user, UserForm.bindings, onModifiedChanged),
|
binder: new FormBinder(item, UserForm.bindings, onModifiedChanged),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (onModifiedChanged) {
|
if (onModifiedChanged) {
|
||||||
@@ -179,7 +179,7 @@ export class UserForm extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
style={{ width: "100%", height: "100%", overflow: "scroll" }}
|
style={{ width: "100%", height: "100%", overflow: "scroll" }}
|
||||||
id="userForm"
|
id="UserForm"
|
||||||
onSubmit={this.handleSubmit}>
|
onSubmit={this.handleSubmit}>
|
||||||
<Column>
|
<Column>
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||||
@@ -303,7 +303,7 @@ export class UserForm extends React.Component {
|
|||||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
<Row.Item width={sizeInfo.formRowSpacing} />
|
||||||
<Row.Item>
|
<Row.Item>
|
||||||
<BoundButton
|
<BoundButton
|
||||||
submit="userForm"
|
submit="UserForm"
|
||||||
text={binder._id ? "Save" : "Add"}
|
text={binder._id ? "Save" : "Add"}
|
||||||
name="submit"
|
name="submit"
|
||||||
binder={binder}
|
binder={binder}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +1,23 @@
|
|||||||
import React, { Component, Fragment } from "react"
|
import React, { Component } from "react"
|
||||||
import PropTypes from "prop-types"
|
|
||||||
import autobind from "autobind-decorator"
|
import autobind from "autobind-decorator"
|
||||||
import { UserList } from "./UserList"
|
|
||||||
import { UserForm } from "./UserForm"
|
import { UserForm } from "./UserForm"
|
||||||
import { UserFormPlaceholder } from "./UserFormPlaceholder"
|
|
||||||
import { api } from "src/API"
|
import { api } from "src/API"
|
||||||
import { Row, Column, Box } from "ui"
|
import { ChangeEmailModal } from "../Modal"
|
||||||
import {
|
import { MasterDetail } from "../MasterDetail"
|
||||||
YesNoMessageModal,
|
import PropTypes from "prop-types"
|
||||||
MessageModal,
|
|
||||||
ChangeEmailModal,
|
|
||||||
WaitModal,
|
|
||||||
} from "../Modal"
|
|
||||||
import { sizeInfo, colorInfo } from "ui/style"
|
|
||||||
|
|
||||||
export class Users extends Component {
|
export class Users extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
changeTitle: PropTypes.func.isRequired,
|
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
modified: false,
|
|
||||||
selectedUser: null,
|
|
||||||
users: [],
|
|
||||||
yesNoModal: null,
|
|
||||||
messageModal: null,
|
|
||||||
waitModal: null,
|
|
||||||
changeEmailModal: 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
|
@autobind
|
||||||
handleChangeEmail() {
|
handleChangeEmail() {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -154,57 +31,41 @@ export class Users extends Component {
|
|||||||
api
|
api
|
||||||
.sendResetPassword(this.state.selectedUser.email)
|
.sendResetPassword(this.state.selectedUser.email)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({
|
this.masterDetail.hideWait()
|
||||||
waitModal: null,
|
this.masterDetail.showMessage(
|
||||||
messageModal: {
|
`An email has been sent to '${
|
||||||
icon: "thumb",
|
this.masterDetail.selectedItem.email
|
||||||
message: `An email has been sent to '${
|
}' with instructions on how to reset their password`
|
||||||
this.state.selectedUser.email
|
)
|
||||||
}' with instructions on how to reset their password`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setState({
|
this.masterDetail.hideWait()
|
||||||
error: true,
|
this.masterDetail.showErrorMessage(
|
||||||
waitModal: null,
|
"Unable to request password reset.",
|
||||||
messageModal: {
|
error.message
|
||||||
icon: "hand",
|
)
|
||||||
message: "Unable to request password reset.",
|
|
||||||
detail: error.message,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleResendEmail() {
|
handleResendEmail() {
|
||||||
this.setState({
|
this.masterDetail.showWait("Resending Email...")
|
||||||
waitModal: { message: "Resending Email..." },
|
|
||||||
})
|
|
||||||
api
|
api
|
||||||
.sendConfirmEmail({ existingEmail: this.state.selectedUser.email })
|
.sendConfirmEmail({ existingEmail: this.masterDetail.selectedItem.email })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({
|
this.masterDetail.hideWait()
|
||||||
waitModal: null,
|
this.masterDetail.showMessage(
|
||||||
messageModal: {
|
`An email has been sent to '${
|
||||||
icon: "thumb",
|
this.masterDetail.selectedItem.email
|
||||||
message: `An email has been sent to '${
|
}' with further instructions.`
|
||||||
this.state.selectedUser.email
|
)
|
||||||
}' with further instructions.`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setState({
|
this.masterDetail.hideWait()
|
||||||
error: true,
|
this.masterDetail.showErrorMessage(
|
||||||
waitModal: null,
|
"Unable to request email change.",
|
||||||
messageModal: {
|
error.message
|
||||||
icon: "hand",
|
)
|
||||||
message: "Unable to request email change.",
|
|
||||||
detail: error.message,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,187 +85,48 @@ export class Users extends Component {
|
|||||||
newEmail,
|
newEmail,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.setState({
|
this.masterDetail.showMessage(
|
||||||
waitModal: null,
|
`An email has been sent to '${newEmail}' to confirm this email.`
|
||||||
messageModal: {
|
)
|
||||||
icon: "hand",
|
|
||||||
message: `An email has been sent to '${newEmail}' to confirm this email.`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setState({
|
this.masterDetail.showErrorMessage(
|
||||||
error: true,
|
"Unable to request email change.",
|
||||||
waitModal: null,
|
error.message
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
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.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() {
|
render() {
|
||||||
const { messageModal, yesNoModal, changeEmailModal } = this.state
|
const { changeEmailModal } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<MasterDetail
|
||||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
history={this.props.history}
|
||||||
<Column.Item grow>
|
ref={(ref) => (this.masterDetail = ref)}
|
||||||
<Row fillParent>
|
name="user"
|
||||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
form={UserForm}
|
||||||
<Row.Item width="25vw">
|
listItems={api.listUsers}
|
||||||
<UserList
|
updateItem={api.updateUser}
|
||||||
users={this.state.users}
|
createItem={api.createUser}
|
||||||
selectedUser={this.state.selectedUser}
|
deleteItem={api.deleteUser}
|
||||||
selectionModified={this.state.modified}
|
detailCallbacks={{
|
||||||
onUserListClick={this.handleUserListClick}
|
onChangeEmail: this.handleChangeEmail,
|
||||||
onAddNewUser={this.handleAddNewUser}
|
onResendEmail: this.handleResendEmail,
|
||||||
/>
|
onResetPassword: this.handleSendPasswordReset,
|
||||||
</Row.Item>
|
}}
|
||||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
sort={(a, b) => 0}
|
||||||
<Row.Item grow>
|
listData={(user) => ({
|
||||||
<Box
|
icon: user.administrator ? "admin" : "profile",
|
||||||
border={{
|
text: user.firstName + " " + user.lastName,
|
||||||
width: sizeInfo.headerBorderWidth,
|
})}>
|
||||||
color: colorInfo.headerBorder,
|
<ChangeEmailModal
|
||||||
}}
|
open={!!changeEmailModal}
|
||||||
radius={sizeInfo.formBoxRadius}>
|
oldEmail={changeEmailModal && changeEmailModal.oldEmail}
|
||||||
{this.state.selectedUser ? (
|
onDismiss={this.handleChangeEmailDismiss}
|
||||||
<UserForm
|
/>
|
||||||
user={this.state.selectedUser}
|
</MasterDetail>
|
||||||
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}>
|
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
website/src/assets/images/badge.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
25
website/src/ui/Footer.js
Normal 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
website/src/ui/Header.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,21 +21,22 @@ export class Icon extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static svgs = {
|
static svgs = {
|
||||||
logout: require("icons/logout.svg"),
|
admin: require("./icons/admin.svg"),
|
||||||
thumb: require("icons/thumb.svg"),
|
blank: require("./icons/blank.svg"),
|
||||||
profile: require("icons/profile.svg"),
|
clock: require("./icons/clock.svg"),
|
||||||
admin: require("icons/admin.svg"),
|
confirmed: require("./icons/confirmed.svg"),
|
||||||
hand: require("icons/hand.svg"),
|
edit: require("./icons/edit.svg"),
|
||||||
users: require("icons/users.svg"),
|
hand: require("./icons/hand.svg"),
|
||||||
team: require("icons/team.svg"),
|
help: require("./icons/help.svg"),
|
||||||
teams: require("icons/teams.svg"),
|
logout: require("./icons/logout.svg"),
|
||||||
system: require("icons/system.svg"),
|
profile: require("./icons/profile.svg"),
|
||||||
confirmed: require("icons/confirmed.svg"),
|
placeholder: require("./icons/placeholder.svg"),
|
||||||
help: require("icons/help.svg"),
|
system: require("./icons/system.svg"),
|
||||||
warning: require("icons/warning.svg"),
|
thumb: require("./icons/thumb.svg"),
|
||||||
edit: require("icons/edit.svg"),
|
team: require("./icons/team.svg"),
|
||||||
placeholder: require("icons/placeholder.svg"),
|
teams: require("./icons/teams.svg"),
|
||||||
clock: require("icons/clock.svg"),
|
users: require("./icons/users.svg"),
|
||||||
|
warning: require("./icons/warning.svg"),
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
12
website/src/ui/icons/blank.svg
Normal 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 |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 965 B After Width: | Height: | Size: 965 B |
|
Before Width: | Height: | Size: 918 B After Width: | Height: | Size: 918 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -2,8 +2,8 @@ export { Anime } from "./Anime"
|
|||||||
export { Box } from "./Box"
|
export { Box } from "./Box"
|
||||||
export { Button } from "./Button"
|
export { Button } from "./Button"
|
||||||
export { FormIconButton } from "./FormIconButton"
|
export { FormIconButton } from "./FormIconButton"
|
||||||
export { HeaderButton } from "./HeaderButton"
|
export { Header } from "./Header"
|
||||||
export { HeaderText } from "./HeaderText"
|
export { Footer } from "./Footer"
|
||||||
export { PanelButton } from "./PanelButton"
|
export { PanelButton } from "./PanelButton"
|
||||||
export { Checkbox } from "./Checkbox"
|
export { Checkbox } from "./Checkbox"
|
||||||
export { Input } from "./Input"
|
export { Input } from "./Input"
|
||||||
|
|||||||