@bigfishtv/cockpit
Version:
335 lines (305 loc) • 9.72 kB
JavaScript
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' })
}
}
}
},
}