Integrated master/detail, refactor Icon, add base router
@@ -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"
|
||||
|
||||