365 lines
8.9 KiB
JavaScript
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>
|
|
)
|
|
}
|
|
}
|