Integrated master/detail, refactor Icon, add base router
This commit is contained in:
30
website/src/MasterDetail/DetailPlaceholder.js
Normal file
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
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
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
3
website/src/MasterDetail/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { MasterDetail } from "./MasterDetail"
|
||||
export { DetailPlaceholder } from "./DetailPlaceholder"
|
||||
export { MasterList } from "./MasterList"
|
||||
Reference in New Issue
Block a user