UNPKG

@bigfishtv/cockpit

Version:

274 lines (243 loc) 8.97 kB
import PropTypes from 'prop-types' import React, { Component } from 'react' import ReactDOM from 'react-dom' import * as SortTypes from '../../constants/SortTypes' import * as Conditions from '../../constants/Conditions' import { post } from '../../api/xhrUtils' import { isObject } from '../../utils/typeUtils' import { titleCase } from '../../utils/stringUtils' import { showDeletePrompt } from '../../utils/promptUtils' import { filterOutProtectedSchema, removeAssocationsFromSchema } from '../../utils/formUtils' import { filterDataByQueryWithSchema, filterDataByFilterset, sortByObjectKey } from '../../utils/tableUtils' import Tree from '../tree/Tree' import Panel from '../container/panel/Panel' import Button from '../button/Button' import Bulkhead from '../page/Bulkhead' import MainContent from '../container/MainContent' import SearchInput from '../input/SearchInput' import ReorderableCell from '../tree/ReorderableCell' const DefaultPanelToolbar = ({ duplicable, handleDuplicate, selectedIds, data, handleDelete }) => ( <div className="gutter-left"> {duplicable && <Button text="Duplicate" onClick={handleDuplicate} disabled={selectedIds.length !== 1} />} <Button text="Delete" onClick={handleDelete} style="error" disabled={!selectedIds.length} /> </div> ) const DefaultBulkheadToolbar = ({ modelLabel, model, addUrl }) => ( <Button text={'New ' + titleCase(modelLabel || model)} onClick={() => (window.location.href = addUrl)} style="primary" size="large" /> ) // we define this because react-docgen fails when defaultProp directly references an imported component const DefaultReorderableCell = props => <ReorderableCell {...props} /> export default class AutoReorderableIndex extends Component { static propTypes = { /** the lowercase plural model e.g. volunteer_applications */ model: PropTypes.string.isRequired, /** the schema key for the order field, default is 'order' */ orderKey: PropTypes.string, /** array of table schema passed in from backend */ schema: PropTypes.array, /** array of entity assocations passed in from backend */ assocations: PropTypes.array, /** function to handle when a row is double clicked */ onSelect: PropTypes.func, /** default form value provided from backend, typically array of entity objects */ defaultValue: PropTypes.array, /** query string to pre-entered */ defaultQuery: PropTypes.string, /** filterset state */ defaultFilterset: PropTypes.object, /** fields that start with an underscore (_) get excluded by default, this offers a way to whitelist some of those */ protectedFieldWhitelist: PropTypes.array, /** panel toolbar component to replace default one, takes props: selectedIds, data, originalData, handleDelete, handleQueryChange, handleFilterChange, handleDataUpdate */ PanelToolbar: PropTypes.func, /** panel toolbar component to replace default one, takes props: selectedIds, data, originalData, handleDelete, handleQueryChange, handleFilterChange, handleDataUpdate */ BulkheadToolbar: PropTypes.func, } static defaultProps = { defaultValue: [], schema: [], associations: [], orderKey: 'order', model: 'model', defaultQuery: '', defaultFilterset: {}, protectedFieldWhitelist: [], Cell: DefaultReorderableCell, Sidebar: null, PanelToolbar: DefaultPanelToolbar, BulkheadToolbar: DefaultBulkheadToolbar, } constructor(props) { super(props) this.state = { data: props.defaultValue, selectedIds: [], filterset: props.defaultFilterset, query: props.defaultQuery, loading: false, } } getSubmitUrl(data) { return data.id ? this.props.updateUrl + '/' + data.id + '.json' : this.props.addUrl } handleQueryChange = value => { const query = typeof value == 'string' ? value : value.target.value this.setState({ query, selectedIds: [] }) } /** * Overloaded function -- handles updates to filterset object, usually used by dropdown selectors * @param {String / Object} property - is property or object of key values * @param {String} value - is value of property, otherwise leave blank */ handleFilterChange = (property, value) => { let { filterset } = this.state if (isObject(property)) { Object.keys(property).forEach(key => (filterset[key] = property[key])) } else { filterset[property] = value } this.setState({ filterset }) } handleEdit = entity => { if (this.props.onSelect) this.props.onSelect(entity) else window.location.href = this.props.updateUrl + entity.id } handleDelete = () => { const { data, selectedIds } = this.state showDeletePrompt({ subject: this.props.modelLabel || null, selectedIds, data: data.filter(item => selectedIds.indexOf(item.id) >= 0), queryUrl: this.props.deleteUrl, callback: () => { const deletedIds = selectedIds // deletedData.map(item => item.id) const data = this.state.data.filter(item => deletedIds.indexOf(item.id) < 0) this.setState({ data, selectedIds: [] }) }, }) } handleReorder = (value, changes) => { if (!changes.length) return const { orderKey, reorderUrl } = this.props const { selectedIds } = this.state const oldData = [...this.state.data] const entities = this.state.data // begin new articles as current issue articles without any of the selected items let newEntities = entities.filter(item => selectedIds.indexOf(item.id) < 0) changes.forEach(change => { const item = entities.reduce((prev, item) => (item.id === change.id ? item : prev), null) if (item) newEntities.splice(change.index, 0, item) }) // update order value newEntities = newEntities.map((item, order) => ({ ...item, [orderKey]: order })) // save the new orders const postData = newEntities.reduce((data, entity) => ({ ...data, [entity.id]: entity[orderKey] }), {}) post({ url: reorderUrl, data: postData, quietSuccess: true, failureMessage: 'Failed to reorder', callback: () => {}, callbackError: () => this.setState({ data: oldData }), }) this.setState({ data: newEntities }) } handleDataUpdate = data => { this.setState({ data, selectedIds: [] }) } render() { const { schema, associations, model, orderKey, protectedFieldWhitelist, Cell, Sidebar, PanelToolbar, BulkheadToolbar, } = this.props const { data, query, selectedIds, filterset } = this.state const _schema = filterOutProtectedSchema(removeAssocationsFromSchema(associations, schema), protectedFieldWhitelist) const _data = filterDataByFilterset( filterDataByQueryWithSchema(data, _schema, query), filterset, Conditions.AND ).sort(sortByObjectKey(orderKey, SortTypes.ASC, 'numeric')) const panelProps = { handleDelete: this.handleDelete, handleQueryChange: this.handleQueryChange, handleFilterChange: this.handleFilterChange, handleDataUpdate: this.handleDataUpdate, originalData: data, data: _data, selectedIds, filterset, query, } return ( <MainContent> <Bulkhead title={titleCase(model)} Toolbar={() => <BulkheadToolbar {...this.props} />} /> <div className="finder"> {Sidebar && ( <div className="finder-menu"> <Sidebar {...panelProps} /> </div> )} <div className="finder-content" ref="finderContent"> <Panel title={<SearchInput value={query} onChange={this.handleQueryChange} />} PanelToolbar={PanelToolbar} {...panelProps}> <Tree value={_data} collapsedIds={[]} TreeCell={Cell} selectedIds={selectedIds} treeItemTarget={treeItemTarget} onChange={this.handleReorder} onSelectItem={this.handleEdit} onSelectionChange={selectedIds => this.setState({ selectedIds })} onCombinationChange={({ selectedIds }) => this.setState({ selectedIds })} /> </Panel> </div> </div> </MainContent> ) } } /** * We have to pass in our own treeItemTarget function to prevent the 'drag-into' functionality */ const EDGE_SIZE = 10 const treeItemTarget = { drop(props, monitor, component) { if (monitor.isOver({ shallow: true })) { const draggedId = monitor.getItem().id const targetId = props.id const position = component.state.position props.endDrag(draggedId, targetId, position) } }, hover(props, monitor, component) { if (!props.reorderable) return const isOverCurrent = monitor.isOver({ shallow: true }) if (isOverCurrent) { const ownId = props.id const draggedId = monitor.getItem().id if (draggedId === ownId || component.props.selected) return const boundingRect = ReactDOM.findDOMNode(component).getBoundingClientRect() const clientOffset = monitor.getClientOffset() const offsetY = clientOffset.y - boundingRect.top if (clientOffset.y > boundingRect.top && clientOffset.y < boundingRect.bottom) { if (offsetY < EDGE_SIZE) component.setState({ position: 'above' }) else component.setState({ position: 'below' }) } } }, }