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