UNPKG

@bigfishtv/cockpit

Version:

335 lines (305 loc) 9.72 kB
import PropTypes from 'prop-types' import React, { Component } from 'react' import classnames from 'classnames' import Immutable from 'immutable' import { connect } from 'react-redux' import ReactDOM from 'react-dom' import isEqual from 'lodash/isEqual' import MainContent from '../container/MainContent' import Icon from '../Icon' import Tree from '../tree/Tree' import Bulkhead from '../page/Bulkhead' import Button from '../button/Button' import { post } from '../../api/xhrUtils' import { showDeletePrompt } from '../../utils/promptUtils' import { userCanAccess } from '../../utils/roleUtils' import { pruneTreeImmutable, collectValuesImmutable } from '../../utils/treeUtils' import { notifyFailure } from '../../actions/notifications' const HeaderToolbar = props => { const { allCollapsed, collapsedIds, selectedIds, onCollapseAll, onExpandAll, onDelete } = props.headerToolbarProps return ( <div> <Button text="Collapse All" onClick={onCollapseAll} disabled={allCollapsed} /> <Button text="Expand All" onClick={onExpandAll} disabled={!collapsedIds.length} /> <Button text="Delete" style="error" onClick={onDelete} disabled={!selectedIds.length} /> </div> ) } const PageTreeCell = props => { const { id, title, status, path, userCanAccess, isCollapsed, selectedDrag, showIndicator, onIndicatorClick, onIndicatorDoubleClick, isOver, position, onClick, onDoubleClick, selected, } = props return ( <div className={classnames('tree-item', isOver && 'drag-' + position)}> <div className={classnames('tree-cell', { dragging: selectedDrag, selected: selected, disabled: !userCanAccess })} onClick={userCanAccess ? onClick : null} onDoubleClick={userCanAccess ? onDoubleClick : null}> <div className="tree-cell-icon"> {showIndicator && ( <div className={classnames('tree-cell-control', isCollapsed && 'collapsed')} onClick={onIndicatorClick} onDoubleClick={onIndicatorDoubleClick}> <Icon name={'chevron-' + (isCollapsed ? 'right' : 'down')} size="18" /> </div> )} </div> <div className="tree-cell-status"> <div className={classnames('status', status)} /> </div> {!userCanAccess && ( <div className="tree-cell-icon"> <Icon name="lock" size={12} /> </div> )} {userCanAccess ? ( <div className="tree-cell-title"> <a href={'/admin/pages/edit/' + id} onClick={event => event.stopPropagation()}> {title} </a> </div> ) : ( <div className="tree-cell-title disabled">{title}</div> )} <div className="tree-cell-text"> <a href={path} target="_blank" onClick={event => event.stopPropagation()}> {path} </a> </div> </div> </div> ) } function filterPages(data, user) { return data.map(page => { page.userCanAccess = userCanAccess([{ model: 'Pages', foreign_key: page.id }], user) if (page.userCanAccess) { markRecursive(page.children) } else { page.children = filterPages(page.children, user) } return page }) } function markRecursive(children) { children.map(page => { page.userCanAccess = true markRecursive(page.children) }) } /** * Pages tree view page template */ @connect(({ viewer }) => ({ viewer })) export default class Pages extends Component { static propTypes = { /** update url to hit on page reorder */ moveUrl: PropTypes.string, /** threaded array of objects - tree data */ treeData: PropTypes.array, } static defaultProps = { treeData: [], } constructor(props) { super() const pages = filterPages(props.treeData, props.viewer) this.state = { data: Immutable.fromJS(pages), selectedIds: [], collapsedIds: localStorage.pagesCollapsedIds ? JSON.parse(localStorage.pagesCollapsedIds) : [], //getInitialCollapsed(pages), allCollapsed: false, } } componentDidUpdate() { localStorage.pagesCollapsedIds = JSON.stringify(this.state.collapsedIds) } handleChange = (newTree, delta) => { if (this.props.moveUrl) { this.setState({ data: newTree }) post({ url: this.props.moveUrl, data: delta, quietSuccess: true, errorMessage: 'Failed to update tree', callback: data => this.setState({ data: Immutable.fromJS(filterPages(data, this.props.viewer)) }), }) } else { this.props.dispatch(notifyFailure('Failed to update tree')) console.warn('[Pages] moveUrl not supplied, not saving tree') } } handleDelete = () => { const { selectedIds, data } = this.state const itemsToDelete = collectValuesImmutable(data, 'title', item => selectedIds.indexOf(item.get('id')) >= 0).map( value => ({ title: value }) ) showDeletePrompt({ subject: 'page', style: 'error', selectedIds, data: itemsToDelete, callback: () => { this.setState({ data: pruneTreeImmutable(data, 'id', selectedIds, 'children'), selectedIds: [], }) }, }) } handleSelectionChange = selectedIds => { this.setState({ selectedIds }) } handleCollapseChange = collapsedIds => { const collapsableIds = collectValuesImmutable( this.state.data, 'id', item => item.get('children') && item.get('children').size > 0 ) const allCollapsed = isEqual(collapsableIds.sort(), collapsedIds.sort()) this.setState({ collapsedIds, allCollapsed }) } handleCombinationChange = mixed => { const newState = {} Object.keys(mixed).map(key => { if (key in this.state) newState[key] = mixed[key] }) this.setState(newState) } handleCollapseAll = () => { const collapsedIds = collectValuesImmutable( this.state.data, 'id', item => item.get('children') && item.get('children').size > 0 ) this.setState({ collapsedIds, allCollapsed: true }) } handleExpandAll = () => { this.setState({ collapsedIds: [], allCollapsed: false }) } handleSelectedItem = item => { window.location = '/admin/pages/edit/' + item.get('id') } render() { const { data, selectedIds, collapsedIds, allCollapsed } = this.state const headerToolbarProps = { selectedIds, allCollapsed, collapsedIds, onCollapseAll: this.handleCollapseAll, onExpandAll: this.handleExpandAll, onDelete: this.handleDelete, } return ( <MainContent> <Bulkhead title="Pages" Toolbar={HeaderToolbar} headerToolbarProps={headerToolbarProps} /> <div className="panel margin-medium"> <Tree value={data} immutable={true} TreeCell={PageTreeCell} treeItemSource={treeItemSource} treeItemTarget={treeItemTarget} selectedIds={selectedIds} collapsedIds={collapsedIds} onChange={this.handleChange} onSelectItem={this.handleSelectedItem} onSelectionChange={this.handleSelectionChange} onCollapseChange={this.handleCollapseChange} onCombinationChange={this.handleCombinationChange} /> </div> </MainContent> ) } } // this is referenced in TreeItem -- it needs to be provided as a prop for react-dnd decorator in order for it to be optionally replaced by tree props const treeItemSource = { canDrag(props) { return props.userCanAccess }, beginDrag(props) { props.beginDrag(props.id) return { id: props.id, index: props.index } }, endDrag(props, monitor) { if (!monitor.didDrop()) { props.endDrag() } }, } // this is referenced in TreeItem -- it needs to be provided as a prop for react-dnd decorator in order for it to be optionally replaced by tree props let expandTimeout = null const EDGE_SIZE = 10 const PARENT_BELOW_EDGE_SIZE = 50 const treeItemTarget = { drop(props, monitor, component) { if (!component.decoratedComponentInstance.props.userCanAccess) return if (!props.reorderable) return // goes through all drop targets and resets their forceExpand state so they don't randomly open upon drag if hover expanded for (let key in monitor.internalMonitor.registry.handlers) { if (key.charAt(0) == 'T') { const item = monitor.internalMonitor.registry.handlers[key].component if (item.state && item.state.forceExpand) { item.props.onCollapse() item.setState({ forceExpand: false }) } } } 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 (!component.decoratedComponentInstance.props.userCanAccess) return 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 const rowHeight = boundingRect.bottom - boundingRect.top const bottomEdgeSize = !props.collapsed && props.children && props.children.size > 0 ? PARENT_BELOW_EDGE_SIZE : EDGE_SIZE if (clientOffset.y > boundingRect.top && clientOffset.y < boundingRect.bottom) { if ( clientOffset.y > boundingRect.top + EDGE_SIZE && clientOffset.y < boundingRect.top + rowHeight - bottomEdgeSize ) { if (component.props.collapsed && component.state.position != 'into') { if (expandTimeout !== null) clearTimeout(expandTimeout) expandTimeout = setTimeout(() => { component.setState({ forceExpand: true }) }, 1000) } component.setState({ position: 'into' }) } else if (offsetY < EDGE_SIZE) { component.setState({ position: 'above' }) } else { component.setState({ position: 'below' }) } } } }, }