Files
deighton-ar/website/src/MasterDetail/MasterDetail.js
2018-05-25 10:16:28 -07:00

365 lines
8.9 KiB
JavaScript

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,
listSort: 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.sort(this.props.listSort) })
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
)
})
}
@autobind
showWait(message) {
this.setState({
waitModal: {
message,
},
})
}
@autobind
hideWait() {
this.setState({
waitModal: null,
})
}
@autobind
showMessage(message, detail) {
this.setState({
messageModal: {
icon: "thumb",
message,
detail,
},
})
}
@autobind
showErrorMessage(message, detail) {
this.setState({
messageModal: {
icon: "hand",
message,
detail,
},
})
}
@autobind
showYesNo(message, onDismiss) {
this.setState({
yesNoModal: {
question: message,
onDismiss,
},
})
}
@autobind
hideYesNo() {
this.setState({
yesNoModal: null,
})
}
@autobind
getSelectedItem() {
return this.state.selectedItem
}
@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.listSort),
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.hideYesNo()
}
@autobind
handleModifiedModalDismiss(yes) {
if (yes) {
this.setState({
selectedItem: this.nextSelectedItem,
modified: false,
})
this.removeUnfinishedNewItem()
delete this.nextSelectedItem
}
this.hideYesNo()
}
@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}>
{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
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
{this.props.children}
</Column.Item>
</Fragment>
)
}
}