Integrated master/detail, refactor Icon, add base router
This commit is contained in:
1 parent
84babf0e4b
commit
6fae5ef5d6
61 files changed
+1203
-1620
No files matched your search
+2
-1
@@ -24,6 +24,7 @@
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
"comma-dangle": ["error", "only-multiline"],
|
||||
"jsx-quotes": "off",
|
||||
"quotes": "off"
|
||||
"quotes": "off",
|
||||
"indent": 0
|
||||
}
|
||||
}
|
||||
Generated
+3
-3
@@ -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"
|
||||
|
||||
+38
-35
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
+44
-137
@@ -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 = {
|
||||
|
||||
+19
-16
@@ -1,20 +1,11 @@
|
||||
import React, { Component, Fragment } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Row, Column, PanelButton } from 'ui'
|
||||
import { sizeInfo } from 'ui/style'
|
||||
import React, { Component, Fragment } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Row, Column, PanelButton } from "ui"
|
||||
import { sizeInfo } from "ui/style"
|
||||
|
||||
export class Home extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
changeTitle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.changeTitle('Home')
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.changeTitle('')
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -25,15 +16,27 @@ export class Home extends Component {
|
||||
<Row>
|
||||
<Row.Item grow />
|
||||
<Row.Item>
|
||||
<PanelButton icon='users' text='Users' onClick={() => (this.props.history.push('/users'))} />
|
||||
<PanelButton
|
||||
icon="users"
|
||||
text="Users"
|
||||
onClick={() => this.props.history.push("/users")}
|
||||
/>
|
||||
</Row.Item>
|
||||
<Row.Item width={sizeInfo.panelButtonSpacing} />
|
||||
<Row.Item>
|
||||
<PanelButton icon='teams' text='Teams' onClick={() => (this.props.history.push('/teams'))} />
|
||||
<PanelButton
|
||||
icon="teams"
|
||||
text="Teams"
|
||||
onClick={() => this.props.history.push("/teams")}
|
||||
/>
|
||||
</Row.Item>
|
||||
<Row.Item width={sizeInfo.panelButtonSpacing} />
|
||||
<Row.Item>
|
||||
<PanelButton icon='system' text='System' onClick={() => (this.props.history.push('/system'))} />
|
||||
<PanelButton
|
||||
icon="system"
|
||||
text="System"
|
||||
onClick={() => this.props.history.push("/system")}
|
||||
/>
|
||||
</Row.Item>
|
||||
<Row.Item grow />
|
||||
</Row>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Column, Text } from "ui"
|
||||
|
||||
export class DetailPlaceholder extends Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name } = this.props
|
||||
const capitalizedName = name.charAt(0).toUpperCase() + name.substr(1)
|
||||
|
||||
return (
|
||||
<Column fillParent>
|
||||
<Column.Item grow />
|
||||
<Column.Item>
|
||||
<Text size="large" align="center" width="100%">
|
||||
{`Select a ${name} to view details here`}
|
||||
</Text>
|
||||
<br />
|
||||
<Text size="small" align="center" width="100%">
|
||||
{`Or 'Add New ${capitalizedName}'`}
|
||||
</Text>
|
||||
</Column.Item>
|
||||
<Column.Item grow />
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import React, { Component, Fragment } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import autobind from "autobind-decorator"
|
||||
import { Row, Column, Box } from "ui"
|
||||
import { YesNoMessageModal, MessageModal, WaitModal } from "../Modal"
|
||||
import { sizeInfo, colorInfo } from "ui/style"
|
||||
import { DetailPlaceholder, MasterList } from "."
|
||||
import pluralize from "pluralize"
|
||||
|
||||
export class MasterDetail extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
name: PropTypes.string,
|
||||
form: PropTypes.func.isRequired,
|
||||
listItems: PropTypes.func.isRequired,
|
||||
updateItem: PropTypes.func.isRequired,
|
||||
createItem: PropTypes.func.isRequired,
|
||||
deleteItem: PropTypes.func.isRequired,
|
||||
sort: PropTypes.func.isRequired,
|
||||
detailCallbacks: PropTypes.object,
|
||||
listData: PropTypes.func,
|
||||
children: PropTypes.element,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
modified: false,
|
||||
selectedItem: null,
|
||||
items: [],
|
||||
yesNoModal: null,
|
||||
messageModal: null,
|
||||
waitModal: null,
|
||||
changeEmailModal: null,
|
||||
}
|
||||
|
||||
const { name } = this.props
|
||||
|
||||
this.capitalizedName = name.charAt(0).toUpperCase() + name.substr(1)
|
||||
this.pluralizedName = pluralize(name)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props
|
||||
.listItems()
|
||||
.then((list) => {
|
||||
this.setState({ items: list.items })
|
||||
|
||||
const { history } = this.props
|
||||
const search = new URLSearchParams(history.location.search)
|
||||
const id = search.get("id")
|
||||
|
||||
if (id) {
|
||||
const item = list.items.find((item) => item._id === id)
|
||||
|
||||
if (item) {
|
||||
this.setState({ selectedItem: item })
|
||||
} else {
|
||||
history.replace(history.pathname)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showErrorMessage(
|
||||
`Unable to get the list of ${this.pluralizedName}.`,
|
||||
error.message
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
get selectedItem() {
|
||||
return this.state.selectedItem
|
||||
}
|
||||
|
||||
@autobind
|
||||
showWait(message) {
|
||||
this.setState({
|
||||
waitModal: {
|
||||
message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
hideWait() {
|
||||
this.setState({
|
||||
waitModal: null,
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
showMessage(message, detail) {
|
||||
this.setState({
|
||||
icon: "thumb",
|
||||
message,
|
||||
detail,
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
showErrorMessage(message, detail) {
|
||||
this.setState({
|
||||
icon: "hand",
|
||||
message,
|
||||
detail,
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
showYesNo(message, onDismiss) {
|
||||
this.setState({
|
||||
yesNoModal: {
|
||||
question: message,
|
||||
onDismiss,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
removeUnfinishedNewItem() {
|
||||
let items = this.state.items
|
||||
|
||||
if (items.length > 0 && !items[0]._id) {
|
||||
this.setState({ items: this.state.items.slice(1) })
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleItemListClick(e, index) {
|
||||
let item = this.state.items[index]
|
||||
const { history } = this.props
|
||||
|
||||
if (this.state.modified) {
|
||||
this.nextSelectedItem = item
|
||||
this.showYesNo(
|
||||
`This ${
|
||||
this.props.name
|
||||
} has been modified. Are you sure you would like to navigate away?`,
|
||||
this.handleModifiedModalDismiss
|
||||
)
|
||||
} else {
|
||||
this.setState({ selectedItem: item })
|
||||
this.removeUnfinishedNewItem()
|
||||
history.replace(`${history.location.pathname}?id=${item._id}`)
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleSave(item) {
|
||||
if (item._id) {
|
||||
this.showWait(`Updating ${this.capitalizedName}`)
|
||||
this.props
|
||||
.updateItem(item)
|
||||
.then((updatedItem) => {
|
||||
this.hideWait()
|
||||
this.setState({
|
||||
items: this.state.items.map(
|
||||
(item) => (item._id === updatedItem._id ? updatedItem : item)
|
||||
),
|
||||
modified: false,
|
||||
selectedItem: updatedItem,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.hideWait()
|
||||
this.showErrorMessage(
|
||||
"Unable to save the item changes",
|
||||
error.message
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.showWait(`Creating ${this.capitalizedName}`)
|
||||
|
||||
this.props
|
||||
.createItem(item)
|
||||
.then((createdItem) => {
|
||||
this.hideWait()
|
||||
this.setState({
|
||||
items: this.state.items
|
||||
.map((item) => (!item._id ? createdItem : item))
|
||||
.sort(this.props.sort),
|
||||
modified: false,
|
||||
selectedItem: createdItem,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.hideWait()
|
||||
this.showErrorMessage("Unable to create the item.", error.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleRemove() {
|
||||
this.showYesNo(
|
||||
`Are you sure you want to remove this ${this.props.name}?`,
|
||||
this.handleRemoveModalDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleRemoveModalDismiss(yes) {
|
||||
if (yes) {
|
||||
const selectedItemId = this.state.selectedItem._id
|
||||
const selectedIndex = this.state.items.findIndex(
|
||||
(item) => item._id === selectedItemId
|
||||
)
|
||||
|
||||
if (selectedIndex >= 0) {
|
||||
this.showWait(`Removing ${this.capitalizedName}`)
|
||||
|
||||
this.props
|
||||
.deleteItem(selectedItemId)
|
||||
.then(() => {
|
||||
this.hideWait()
|
||||
this.setState({
|
||||
items: [
|
||||
...this.state.items.slice(0, selectedIndex),
|
||||
...this.state.items.slice(selectedIndex + 1),
|
||||
],
|
||||
selectedItem: null,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
this.hideWait()
|
||||
this.showErrorMessage(
|
||||
`Unable to remove the ${this.props.name}.`,
|
||||
error.message
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
yesNoModal: null,
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleModifiedModalDismiss(yes) {
|
||||
if (yes) {
|
||||
this.setState({
|
||||
selectedItem: this.nextSelectedItem,
|
||||
modified: false,
|
||||
})
|
||||
this.removeUnfinishedNewItem()
|
||||
delete this.nextSelectedItem
|
||||
}
|
||||
|
||||
this.setState({
|
||||
yesNoModal: null,
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleMessageModalDismiss() {
|
||||
this.setState({ messageModal: null })
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleModifiedChanged(modified) {
|
||||
this.setState({ modified: modified })
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleAddNewItem() {
|
||||
let items = this.state.items
|
||||
|
||||
if (items.length > 0 && !items[0]._id) {
|
||||
// Already adding a new item
|
||||
return
|
||||
}
|
||||
|
||||
let newItem = {}
|
||||
let newItems = [newItem].concat(this.state.items)
|
||||
this.setState({ items: newItems, selectedItem: newItem })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
messageModal,
|
||||
yesNoModal,
|
||||
waitModal,
|
||||
items,
|
||||
selectedItem,
|
||||
modified,
|
||||
} = this.state
|
||||
const { name } = this.props
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||
<Column.Item grow>
|
||||
<Row fillParent>
|
||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
||||
<Row.Item width="25vw">
|
||||
<MasterList
|
||||
capitalizedName={this.capitalizedName}
|
||||
items={items}
|
||||
selectedItem={selectedItem}
|
||||
selectionModified={modified}
|
||||
onItemListClick={this.handleItemListClick}
|
||||
onAddNewItem={this.handleAddNewItem}
|
||||
listData={this.props.listData}
|
||||
/>
|
||||
</Row.Item>
|
||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
||||
<Row.Item grow>
|
||||
<Box
|
||||
border={{
|
||||
width: sizeInfo.headerBorderWidth,
|
||||
color: colorInfo.headerBorder,
|
||||
}}
|
||||
radius={sizeInfo.formBoxRadius}>
|
||||
{this.state.selectedItem ? (
|
||||
React.createElement(this.props.form, {
|
||||
item: selectedItem,
|
||||
onSave: this.handleSave,
|
||||
onRemove: this.handleRemove,
|
||||
onModifiedChanged: this.handleModifiedChanged,
|
||||
...this.props.detailCallbacks,
|
||||
})
|
||||
) : (
|
||||
<DetailPlaceholder name={name} />
|
||||
)}
|
||||
</Box>
|
||||
</Row.Item>
|
||||
<Row.Item width={sizeInfo.formRowSpacing} />
|
||||
</Row>
|
||||
</Column.Item>
|
||||
<Column.Item height={sizeInfo.formColumnSpacing}>
|
||||
<YesNoMessageModal
|
||||
open={!!yesNoModal}
|
||||
question={yesNoModal ? yesNoModal.question : ""}
|
||||
onDismiss={yesNoModal && yesNoModal.onDismiss}
|
||||
/>
|
||||
|
||||
<MessageModal
|
||||
open={!!messageModal}
|
||||
icon={messageModal ? messageModal.icon : ""}
|
||||
message={messageModal ? messageModal.message : ""}
|
||||
detail={messageModal && messageModal.detail}
|
||||
onDismiss={this.handleMessageModalDismiss}
|
||||
/>
|
||||
|
||||
<WaitModal
|
||||
active={!!waitModal}
|
||||
message={waitModal ? waitModal.message : ""}
|
||||
/>
|
||||
|
||||
{this.props.children}
|
||||
</Column.Item>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { Column, List, Button } from "ui"
|
||||
import { sizeInfo } from "ui/style"
|
||||
|
||||
export class MasterList extends React.Component {
|
||||
static propTypes = {
|
||||
capitalizedName: PropTypes.string,
|
||||
items: PropTypes.array,
|
||||
listData: PropTypes.func,
|
||||
onItemListClick: PropTypes.func,
|
||||
selectedItem: PropTypes.object,
|
||||
selectionModified: PropTypes.bool,
|
||||
onAddNewItem: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
items: null,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.items !== this.props.items) {
|
||||
this.setState({ items: nextProps.items })
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedItem, selectionModified, capitalizedName } = this.props
|
||||
const { items } = this.state
|
||||
|
||||
return (
|
||||
<Column fillParent>
|
||||
<Column.Item grow>
|
||||
<List
|
||||
items={items}
|
||||
render={(item, index) => {
|
||||
const data = item._id
|
||||
? this.props.listData(item)
|
||||
: {
|
||||
icon: "blank",
|
||||
text: `[New ${capitalizedName}]`,
|
||||
}
|
||||
return (
|
||||
<List.Item
|
||||
key={item._id || "0"}
|
||||
onClick={(e) => this.props.onItemListClick(e, index)}
|
||||
active={item === this.props.selectedItem}>
|
||||
<List.Icon name={data.icon} size={sizeInfo.listIcon} />
|
||||
<List.Text>{data.text}</List.Text>
|
||||
{item === selectedItem && selectionModified ? (
|
||||
<List.Icon name="edit" size={sizeInfo.listIcon} />
|
||||
) : null}
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Column.Item>
|
||||
<Column.Item height={sizeInfo.formColumnSpacing} />
|
||||
<Column.Item height={sizeInfo.buttonHeight}>
|
||||
<Button
|
||||
width="100%"
|
||||
color="inverse"
|
||||
onClick={this.props.onAddNewItem}
|
||||
text={`Add New ${capitalizedName}`}
|
||||
/>
|
||||
</Column.Item>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { MasterDetail } from "./MasterDetail"
|
||||
export { DetailPlaceholder } from "./DetailPlaceholder"
|
||||
export { MasterList } from "./MasterList"
|
||||
@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
|
||||
import { Box, Image, Column, Row, Button, Link } from "ui"
|
||||
import { 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
-294
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
+62
-340
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,25 @@
|
||||
import React, { Component } from "react"
|
||||
import { Box, Text } from "."
|
||||
import PropTypes from "prop-types"
|
||||
import { colorInfo, sizeInfo } from "./style"
|
||||
|
||||
export class Footer extends Component {
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box
|
||||
background={colorInfo.headerButtonBackground}
|
||||
borderTop={{
|
||||
width: sizeInfo.headerBorderWidth,
|
||||
color: colorInfo.headerBorder,
|
||||
}}>
|
||||
<Text color="dimmed" margin={sizeInfo.footerTextMargin}>
|
||||
{this.props.text}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { Component } from "react"
|
||||
import Radium from "radium"
|
||||
import PropTypes from "prop-types"
|
||||
import { Icon, Image, Box, Row } from "."
|
||||
import { colorInfo, sizeInfo, fontInfo } from "./style"
|
||||
|
||||
export class Header extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
location: PropTypes.object,
|
||||
left: PropTypes.arrayOf(PropTypes.object),
|
||||
right: PropTypes.arrayOf(PropTypes.object),
|
||||
}
|
||||
|
||||
render() {
|
||||
const { location } = this.props
|
||||
|
||||
const renderHeaderitem = (item, index) => {
|
||||
if (item.image) {
|
||||
return (
|
||||
<Header.Button
|
||||
key={index}
|
||||
image={item.image}
|
||||
onClick={() => this.props.history.push(item.path)}
|
||||
/>
|
||||
)
|
||||
} else if (item.icon) {
|
||||
return (
|
||||
<Header.Button
|
||||
key={index}
|
||||
icon={item.icon}
|
||||
onClick={() => this.props.history.push(item.path)}
|
||||
/>
|
||||
)
|
||||
} else if (item.text) {
|
||||
return (
|
||||
<Header.TextButton
|
||||
key={index}
|
||||
active={location.pathname.endsWith(item.path)}
|
||||
text={item.text}
|
||||
onClick={() => this.props.history.push(item.path)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
background={colorInfo.headerButtonBackground}
|
||||
borderBottom={{
|
||||
width: sizeInfo.headerBorderWidth,
|
||||
color: colorInfo.headerBorder,
|
||||
}}
|
||||
style={{ boxSizing: "content" }}>
|
||||
<Row fillParent>
|
||||
<Row.Item>{this.props.left.map(renderHeaderitem)}</Row.Item>
|
||||
<Row.Item grow />
|
||||
<Row.Item>{this.props.right.map(renderHeaderitem)}</Row.Item>
|
||||
</Row>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Header.Button = Radium(
|
||||
class HeaderButton extends Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
icon: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
}
|
||||
|
||||
static style = {
|
||||
background: colorInfo.headerButtonBackground,
|
||||
verticalAlign: "middle",
|
||||
borderWidth: 0,
|
||||
padding: "0 0 0 0",
|
||||
outline: "none",
|
||||
":hover": {
|
||||
background: colorInfo.headerButtonBackgroundHover,
|
||||
},
|
||||
":active": {
|
||||
background: colorInfo.headerButtonBackgroundActive,
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
// Times two to account for zooming
|
||||
const size = sizeInfo.headerHeight - sizeInfo.headerBorderWidth
|
||||
const { onClick, icon, image } = this.props
|
||||
let content = null
|
||||
|
||||
if (image) {
|
||||
content = (
|
||||
<Image
|
||||
source={image}
|
||||
width={size}
|
||||
height={size}
|
||||
margin={sizeInfo.headerButtonMargin}
|
||||
/>
|
||||
)
|
||||
} else if (icon) {
|
||||
content = (
|
||||
<Icon name={icon} size={size} margin={sizeInfo.headerButtonMargin} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
style={[{ height: size, width: size }, HeaderButton.style]}
|
||||
onClick={onClick}>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Header.TextButton = Radium(
|
||||
class HeaderTextButton extends Component {
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
text: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
static style = {
|
||||
display: "inline-block",
|
||||
fontSize: fontInfo.size.header,
|
||||
fontFamily: fontInfo.family,
|
||||
color: fontInfo.color.normal,
|
||||
textAlign: "left",
|
||||
outline: "none",
|
||||
background: "transparent",
|
||||
verticalAlign: "middle",
|
||||
borderWidth: 0,
|
||||
paddingLeft: sizeInfo.headerSpacing,
|
||||
paddingRight: sizeInfo.headerSpacing,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
":hover": {
|
||||
background: colorInfo.headerButtonBackgroundHover,
|
||||
},
|
||||
":active": {
|
||||
background: colorInfo.headerButtonBackgroundActive,
|
||||
},
|
||||
}
|
||||
|
||||
render() {
|
||||
const height = sizeInfo.headerHeight - sizeInfo.headerBorderWidth
|
||||
const { text, active, onClick } = this.props
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
style={[{ height }, HeaderTextButton.style]}
|
||||
onClick={onClick}>
|
||||
<div
|
||||
style={{
|
||||
textDecoration: active ? "underline" : "initial",
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,46 +0,0 @@
|
||||
import Radium from 'radium'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { Icon, Image } from '.'
|
||||
import { colorInfo, sizeInfo } from 'ui/style'
|
||||
|
||||
@Radium
|
||||
export class HeaderButton extends Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
icon: PropTypes.string,
|
||||
image: PropTypes.string,
|
||||
}
|
||||
|
||||
static style = {
|
||||
background: colorInfo.headerButtonBackground,
|
||||
verticalAlign: 'middle',
|
||||
borderWidth: 0,
|
||||
padding: '0 0 0 0',
|
||||
outline: 'none',
|
||||
':hover': {
|
||||
background: colorInfo.headerButtonBackgroundHover,
|
||||
},
|
||||
':active': {
|
||||
background: colorInfo.headerButtonBackgroundActive,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const size = sizeInfo.headerHeight - 2 * sizeInfo.headerBorderWidth // Times two to account for zooming
|
||||
const { onClick, icon, image } = this.props
|
||||
let content = null
|
||||
|
||||
if (image) {
|
||||
content = (<Image source={image} width={size} height={size} margin={sizeInfo.headerButtonMargin} />)
|
||||
} else if (icon) {
|
||||
content = (<Icon name={icon} size={size} margin={sizeInfo.headerButtonMargin} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<button type='button' style={[{ height: size, width: size }, HeaderButton.style]} onClick={onClick}>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Radium from 'radium'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { sizeInfo, fontInfo } from 'ui/style'
|
||||
|
||||
@Radium
|
||||
export class HeaderText extends Component {
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
}
|
||||
|
||||
static style = {
|
||||
position: 'relative',
|
||||
top: sizeInfo.headerTextOffset,
|
||||
display: 'inline-block',
|
||||
fontSize: fontInfo.size.header,
|
||||
fontFamily: fontInfo.family,
|
||||
color: fontInfo.color.normal,
|
||||
textAlign: 'left',
|
||||
background: 'transparent',
|
||||
verticalAlign: 'middle',
|
||||
borderWidth: 0,
|
||||
paddingLeft: sizeInfo.headerPaddingLeft,
|
||||
}
|
||||
|
||||
render() {
|
||||
const height = sizeInfo.headerHeight - sizeInfo.headerBorderWidth
|
||||
const { text } = this.props
|
||||
|
||||
return (
|
||||
<div style={[{ height }, HeaderText.style]}>{text}</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
+16
-15
@@ -21,21 +21,22 @@ export class Icon extends Component {
|
||||
}
|
||||
|
||||
static svgs = {
|
||||
logout: require("icons/logout.svg"),
|
||||
thumb: require("icons/thumb.svg"),
|
||||
profile: require("icons/profile.svg"),
|
||||
admin: require("icons/admin.svg"),
|
||||
hand: require("icons/hand.svg"),
|
||||
users: require("icons/users.svg"),
|
||||
team: require("icons/team.svg"),
|
||||
teams: require("icons/teams.svg"),
|
||||
system: require("icons/system.svg"),
|
||||
confirmed: require("icons/confirmed.svg"),
|
||||
help: require("icons/help.svg"),
|
||||
warning: require("icons/warning.svg"),
|
||||
edit: require("icons/edit.svg"),
|
||||
placeholder: require("icons/placeholder.svg"),
|
||||
clock: require("icons/clock.svg"),
|
||||
admin: require("./icons/admin.svg"),
|
||||
blank: require("./icons/blank.svg"),
|
||||
clock: require("./icons/clock.svg"),
|
||||
confirmed: require("./icons/confirmed.svg"),
|
||||
edit: require("./icons/edit.svg"),
|
||||
hand: require("./icons/hand.svg"),
|
||||
help: require("./icons/help.svg"),
|
||||
logout: require("./icons/logout.svg"),
|
||||
profile: require("./icons/profile.svg"),
|
||||
placeholder: require("./icons/placeholder.svg"),
|
||||
system: require("./icons/system.svg"),
|
||||
thumb: require("./icons/thumb.svg"),
|
||||
team: require("./icons/team.svg"),
|
||||
teams: require("./icons/teams.svg"),
|
||||
users: require("./icons/users.svg"),
|
||||
warning: require("./icons/warning.svg"),
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="79px" height="79px" viewBox="0 0 79 79" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>blank</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Artboard" transform="translate(-542.000000, -375.000000)">
|
||||
<g id="blank" transform="translate(542.000000, 375.000000)"></g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 610 B |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -2,8 +2,8 @@ export { Anime } from "./Anime"
|
||||
export { Box } from "./Box"
|
||||
export { 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"
|
||||
|
||||
Reference in new issue
Block a user