UNPKG

@bigfishtv/cockpit

Version:

382 lines (352 loc) 11.7 kB
import React, { Component } from 'react' import PropTypes from 'prop-types' import newId from '../../utils/newId' import * as Conditions from '../../constants/Conditions' import { showDeletePrompt } from '../../utils/promptUtils' import { isObject } from '../../utils/typeUtils' import { post } from '../../api/xhrUtils' import { titleCase } from '../../utils/stringUtils' import { filterOutProtectedSchema, removeAssocationsFromSchema } from '../../utils/formUtils' import { getFieldsFromSchemaAndAssociations, filterDataByQueryWithFields, filterDataByFilterset, } from '../../utils/tableUtils' import TableView from '../table/Table' import Spinner from '../Spinner' import MainContent from '../container/MainContent' import Panel from '../container/panel/Panel' import Bulkhead from '../page/Bulkhead' import FilterInput from '../input/SearchInput' import Hint from '../Hint' import Button from '../button/Button' const DefaultPanelToolbar = ({ movable, duplicable, handleMove, handleDuplicate, exportable, handleExport, selectedIds, data, handleDelete, ...props }) => ( <div> {duplicable && <Button text="Duplicate" onClick={handleDuplicate} disabled={selectedIds.length !== 1} />} {movable && <Button text="Move" onClick={handleMove} disabled={!selectedIds.length} />} <Button text="Delete" onClick={handleDelete} style="error" disabled={!selectedIds.length} /> {exportable && ( <Button text={selectedIds.length > 0 ? 'Export ' + selectedIds.length : 'Export'} onClick={handleExport} /> )} </div> ) const DefaultBulkheadToolbar = ({ modelLabel, model, addUrl }) => ( <Button text={'New ' + titleCase(modelLabel || model)} onClick={() => (window.location.href = addUrl)} style="primary" size="large" /> ) const DefaultPanelDrawer = ({ data, originalData, filterset, query, selectedIds }) => { const filters = Object.keys(filterset).reduce( (obj, key) => (filterset[key] !== null ? { ...obj, [key]: filterset[key] } : obj), {} ) const filterKeys = Object.keys(filters) const numKeys = filterKeys.length let queryStr = '' if (!!query || numKeys > 0) queryStr += 'Filtered ' if (!!query) queryStr += `by "${query}" ` if (numKeys > 0) queryStr += 'where ' return ( <div className="panel-drawer"> <span style={{ float: 'left' }}> Showing <strong>{data.length}</strong> of <strong>{originalData.length}</strong> {selectedIds.length > 0 && ( <span> &nbsp;&nbsp;&ndash;&nbsp;&nbsp; <strong>{selectedIds.length === data.length ? 'All' : selectedIds.length}</strong> selected </span> )} </span> {(!!query || numKeys > 0) && ( <span style={{ float: 'right' }}> {queryStr} {numKeys > 0 && filterKeys.map((key, i) => { let value = filters[key] if (isObject(value)) value = ' is ' + JSON.encode(value) else if (typeof value == 'function') value = ' matches a custom function' else value = ' is ' + value.toString() return ( <span key={key}> {titleCase(key)} {value} {i < numKeys - 2 ? ', ' : i < numKeys - 1 ? ' and ' : '.'} </span> ) })} </span> )} </div> ) } export class AutoTableIndexContainer extends Component { static propTypes = { /** the lowercase plural model e.g. volunteer_applications */ model: PropTypes.string, /** array of table schema passed in from backend */ schema: PropTypes.array, /** array of entity assocations passed in from backend */ assocations: PropTypes.array, /** array of entity assocations passed in from backend */ hint: PropTypes.string, /** function that receives assocations and schema column, returns a component */ componentResolver: PropTypes.func, /** function that receives assocations and schema column, returns a field object to be merged with auto-generated field e.g. {width: 60} */ attributeModifier: PropTypes.func, /** 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, /** fields that start with an underscore (_) get excluded by default, this offers a way to whitelist some of those */ protectedFieldWhitelist: PropTypes.array, /** panel title component to replace default search field */ PanelTitle: PropTypes.func, /** panel toolbar component to replace default one, takes props: selectedIds, data, originalData, duplicable, handleDuplicate, handleDelete */ PanelToolbar: PropTypes.func, /** Any extra props to pass through to PanelToolbar */ panelProps: PropTypes.object, /** Function for rendering item titles, used in delete prompt */ renderListItem: PropTypes.func, /** panel toolbar component to replace default one, takes props: selectedIds, data, originalData, duplicable, handleDuplicate, handleDelete */ BulkheadToolbar: PropTypes.func, children: PropTypes.func, duplicateTransformer: PropTypes.func, } static defaultProps = { defaultValue: [], schema: [], associations: [], model: 'model', negativeHeight: 228, movable: false, duplicable: false, exportable: false, defaultQuery: '', defaultFilterset: {}, panelProps: {}, renderListItem: null, protectedFieldWhitelist: [], PanelToolbar: DefaultPanelToolbar, PanelDrawer: DefaultPanelDrawer, BulkheadToolbar: DefaultBulkheadToolbar, duplicateTransformer: item => item, } constructor(props) { super(props) this.state = { data: props.defaultValue, selectedIds: [], filterset: props.defaultFilterset, query: props.defaultQuery, loading: false, negativeHeight: props.hint ? props.negativeHeight + 49 : props.negativeHeight, } this.fields = getFieldsFromSchemaAndAssociations( props.schema, props.associations, props.componentResolver, props.attributeModifier ) } 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: [] }) } handleSelectionChange = selectedIds => { this.setState({ selectedIds }) } handleNegativeHeightChange = negativeHeight => { this.setState({ negativeHeight }) } /** * 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 = (item, isNew = false) => { if (this.props.onSelect) this.props.onSelect(item) else window.location.href = this.props.updateUrl + item.id } handleDelete = () => { const { data, selectedIds } = this.state showDeletePrompt({ subject: this.props.modelLabel || null, selectedIds, renderListItem: this.props.renderListItem, data: data.filter(item => selectedIds.indexOf(item.id) >= 0), queryUrl: this.props.deleteUrl, callback: deletedData => { const deletedIds = selectedIds // deletedData.map(item => item.id) const data = this.state.data.filter(item => deletedIds.indexOf(item.id) < 0) this.setState({ data, selectedIds: [] }) }, }) } handleDuplicate = () => { const { model, duplicateUrl, duplicateTransformer } = this.props const { data, selectedIds } = this.state const selectedItem = data.filter(item => item.id === selectedIds[0])[0] const url = duplicateUrl || `/admin/${model}/${selectedItem.id}.json?clone=1` post({ url, data: { ...duplicateTransformer(selectedItem), id: newId() }, callback: result => this.setState({ data: [...data, result] }), }) } handleExport = () => { const { data, selectedIds } = this.state const itemIds = selectedIds.length > 0 ? selectedIds : data.map(item => item.id) let query = itemIds.map(id => 'ids[]=' + id).join('&') window.open(this.props.exportUrl + '?' + query) } handleMove = () => { if (this.props.onMove) { const { data, selectedIds } = this.state const selectedItems = data.filter(item => ~selectedIds.indexOf(item.id)) this.props.onMove(selectedItems, this.handleUpdateCallback) } } handleUpdateCallback = mapperFunc => { const { data } = this.state const newData = mapperFunc(data) this.setState({ data: newData, originalData: newData }) } handleDataUpdate = data => { this.setState({ data, originalData: data }) } render() { const { schema, associations, protectedFieldWhitelist, panelProps } = this.props const { data, query, selectedIds, loading, filterset, negativeHeight } = this.state const _schema = filterOutProtectedSchema(removeAssocationsFromSchema(associations, schema), protectedFieldWhitelist) const _data = filterDataByFilterset( filterDataByQueryWithFields(data, this.fields, query), filterset, Conditions.AND ) const _panelProps = { handleEdit: this.handleEdit, handleDelete: this.handleDelete, handleExport: this.handleExport, handleMove: this.handleMove, handleDuplicate: this.handleDuplicate, handleQueryChange: this.handleQueryChange, handleFilterChange: this.handleFilterChange, handleDataUpdate: this.handleDataUpdate, handleSelectionChange: this.handleSelectionChange, handleNegativeHeightChange: this.handleNegativeHeightChange, movable: this.props.movable, duplicable: this.props.duplicable, exportable: this.props.exportable, originalData: data, data: _data, selectedIds, filterset, query, ...panelProps, } return this.props.children({ ...this.props, ..._panelProps, panelProps: _panelProps, schema: _schema, data: _data, loading, negativeHeight, }) } } export class AutoTableIndexBase extends Component { render() { const { PanelToolbar, PanelDrawer, PanelTitle, panelProps, handleQueryChange, handleSelectionChange, handleEdit, handleNegativeHeightChange, query, hint, negativeHeight, loading, schema, associations, data, selectedIds, ...props } = this.props return ( <Panel PanelToolbar={PanelToolbar} PanelDrawer={PanelDrawer} title={PanelTitle !== undefined ? PanelTitle : <FilterInput value={query} onChange={handleQueryChange} />} {...panelProps}> {hint && <Hint title={hint} onHide={() => handleNegativeHeightChange(negativeHeight - 49)} />} {loading ? ( <div className="loader-center margin-top-xlarge"> <Spinner spinnerName="circle" /> </div> ) : ( <TableView {...props} schema={schema} associations={associations} data={data} selectedIds={selectedIds} onSelect={handleEdit} onSelectionChange={handleSelectionChange} negativeHeight={negativeHeight} /> )} </Panel> ) } } export default class AutoTableIndex extends Component { render() { return ( <AutoTableIndexContainer {...this.props}> {props => { const { model, panelProps, BulkheadToolbar } = props return ( <MainContent> <Bulkhead title={titleCase(model)} Toolbar={() => <BulkheadToolbar {...props} {...panelProps} />} /> <div className="finder"> <div className="finder-content" ref="finderContent"> <AutoTableIndexBase {...props} /> </div> </div> </MainContent> ) }} </AutoTableIndexContainer> ) } }