Integrated master/detail, refactor Icon, add base router
This commit is contained in:
1 parent
84babf0e4b
commit
6fae5ef5d6
61 files changed
+1203
-1620
No files matched your search
Generated
+3
-3
@@ -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",
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+3
-3
@@ -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"
|
||||||
|
|||||||
+38
-35
@@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-137
@@ -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 = {
|
||||||
|
|||||||
+19
-16
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+19
-294
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+62
-340
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-15
@@ -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() {
|
||||||
|
|||||||
File renamed without changes.
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="79px" height="79px" viewBox="0 0 79 79" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>blank</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Artboard" transform="translate(-542.000000, -375.000000)">
|
||||||
|
<g id="blank" transform="translate(542.000000, 375.000000)"></g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user