@bigfishtv/cockpit
Version:
379 lines (340 loc) • 12.9 kB
JavaScript
var _dec, _class, _class2, _temp;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
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';
var HeaderToolbar = function HeaderToolbar(props) {
var _props$headerToolbarP = props.headerToolbarProps,
allCollapsed = _props$headerToolbarP.allCollapsed,
collapsedIds = _props$headerToolbarP.collapsedIds,
selectedIds = _props$headerToolbarP.selectedIds,
onCollapseAll = _props$headerToolbarP.onCollapseAll,
onExpandAll = _props$headerToolbarP.onExpandAll,
onDelete = _props$headerToolbarP.onDelete;
return React.createElement(
'div',
null,
React.createElement(Button, { text: 'Collapse All', onClick: onCollapseAll, disabled: allCollapsed }),
React.createElement(Button, { text: 'Expand All', onClick: onExpandAll, disabled: !collapsedIds.length }),
React.createElement(Button, { text: 'Delete', style: 'error', onClick: onDelete, disabled: !selectedIds.length })
);
};
var PageTreeCell = function PageTreeCell(props) {
var id = props.id,
title = props.title,
status = props.status,
path = props.path,
userCanAccess = props.userCanAccess,
isCollapsed = props.isCollapsed,
selectedDrag = props.selectedDrag,
showIndicator = props.showIndicator,
onIndicatorClick = props.onIndicatorClick,
onIndicatorDoubleClick = props.onIndicatorDoubleClick,
isOver = props.isOver,
position = props.position,
onClick = props.onClick,
onDoubleClick = props.onDoubleClick,
selected = props.selected;
return React.createElement(
'div',
{ className: classnames('tree-item', isOver && 'drag-' + position) },
React.createElement(
'div',
{
className: classnames('tree-cell', { dragging: selectedDrag, selected: selected, disabled: !userCanAccess }),
onClick: userCanAccess ? onClick : null,
onDoubleClick: userCanAccess ? onDoubleClick : null },
React.createElement(
'div',
{ className: 'tree-cell-icon' },
showIndicator && React.createElement(
'div',
{
className: classnames('tree-cell-control', isCollapsed && 'collapsed'),
onClick: onIndicatorClick,
onDoubleClick: onIndicatorDoubleClick },
React.createElement(Icon, { name: 'chevron-' + (isCollapsed ? 'right' : 'down'), size: '18' })
)
),
React.createElement(
'div',
{ className: 'tree-cell-status' },
React.createElement('div', { className: classnames('status', status) })
),
!userCanAccess && React.createElement(
'div',
{ className: 'tree-cell-icon' },
React.createElement(Icon, { name: 'lock', size: 12 })
),
userCanAccess ? React.createElement(
'div',
{ className: 'tree-cell-title' },
React.createElement(
'a',
{ href: '/admin/pages/edit/' + id, onClick: function onClick(event) {
return event.stopPropagation();
} },
title
)
) : React.createElement(
'div',
{ className: 'tree-cell-title disabled' },
title
),
React.createElement(
'div',
{ className: 'tree-cell-text' },
React.createElement(
'a',
{ href: path, target: '_blank', onClick: function onClick(event) {
return event.stopPropagation();
} },
path
)
)
)
);
};
function filterPages(data, user) {
return data.map(function (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(function (page) {
page.userCanAccess = true;
markRecursive(page.children);
});
}
/**
* Pages tree view page template
*/
var Pages = (_dec = connect(function (_ref) {
var viewer = _ref.viewer;
return { viewer: viewer };
}), _dec(_class = (_temp = _class2 = function (_Component) {
_inherits(Pages, _Component);
function Pages(props) {
_classCallCheck(this, Pages);
var _this = _possibleConstructorReturn(this, _Component.call(this));
_this.handleChange = function (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: function callback(data) {
return _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');
}
};
_this.handleDelete = function () {
var _this$state = _this.state,
selectedIds = _this$state.selectedIds,
data = _this$state.data;
var itemsToDelete = collectValuesImmutable(data, 'title', function (item) {
return selectedIds.indexOf(item.get('id')) >= 0;
}).map(function (value) {
return { title: value };
});
showDeletePrompt({
subject: 'page',
style: 'error',
selectedIds: selectedIds,
data: itemsToDelete,
callback: function callback() {
_this.setState({
data: pruneTreeImmutable(data, 'id', selectedIds, 'children'),
selectedIds: []
});
}
});
};
_this.handleSelectionChange = function (selectedIds) {
_this.setState({ selectedIds: selectedIds });
};
_this.handleCollapseChange = function (collapsedIds) {
var collapsableIds = collectValuesImmutable(_this.state.data, 'id', function (item) {
return item.get('children') && item.get('children').size > 0;
});
var allCollapsed = isEqual(collapsableIds.sort(), collapsedIds.sort());
_this.setState({ collapsedIds: collapsedIds, allCollapsed: allCollapsed });
};
_this.handleCombinationChange = function (mixed) {
var newState = {};
Object.keys(mixed).map(function (key) {
if (key in _this.state) newState[key] = mixed[key];
});
_this.setState(newState);
};
_this.handleCollapseAll = function () {
var collapsedIds = collectValuesImmutable(_this.state.data, 'id', function (item) {
return item.get('children') && item.get('children').size > 0;
});
_this.setState({ collapsedIds: collapsedIds, allCollapsed: true });
};
_this.handleExpandAll = function () {
_this.setState({ collapsedIds: [], allCollapsed: false });
};
_this.handleSelectedItem = function (item) {
window.location = '/admin/pages/edit/' + item.get('id');
};
var pages = filterPages(props.treeData, props.viewer);
_this.state = {
data: Immutable.fromJS(pages),
selectedIds: [],
collapsedIds: localStorage.pagesCollapsedIds ? JSON.parse(localStorage.pagesCollapsedIds) : [], //getInitialCollapsed(pages),
allCollapsed: false
};
return _this;
}
Pages.prototype.componentDidUpdate = function componentDidUpdate() {
localStorage.pagesCollapsedIds = JSON.stringify(this.state.collapsedIds);
};
Pages.prototype.render = function render() {
var _state = this.state,
data = _state.data,
selectedIds = _state.selectedIds,
collapsedIds = _state.collapsedIds,
allCollapsed = _state.allCollapsed;
var headerToolbarProps = {
selectedIds: selectedIds,
allCollapsed: allCollapsed,
collapsedIds: collapsedIds,
onCollapseAll: this.handleCollapseAll,
onExpandAll: this.handleExpandAll,
onDelete: this.handleDelete
};
return React.createElement(
MainContent,
null,
React.createElement(Bulkhead, { title: 'Pages', Toolbar: HeaderToolbar, headerToolbarProps: headerToolbarProps }),
React.createElement(
'div',
{ className: 'panel margin-medium' },
React.createElement(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
})
)
);
};
return Pages;
}(Component), _class2.propTypes = {
/** update url to hit on page reorder */
moveUrl: PropTypes.string,
/** threaded array of objects - tree data */
treeData: PropTypes.array
}, _class2.defaultProps = {
treeData: []
}, _temp)) || _class);
// 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
export { Pages as default };
var treeItemSource = {
canDrag: function canDrag(props) {
return props.userCanAccess;
},
beginDrag: function beginDrag(props) {
props.beginDrag(props.id);
return { id: props.id, index: props.index };
},
endDrag: function 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
var expandTimeout = null;
var EDGE_SIZE = 10;
var PARENT_BELOW_EDGE_SIZE = 50;
var treeItemTarget = {
drop: function 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 (var key in monitor.internalMonitor.registry.handlers) {
if (key.charAt(0) == 'T') {
var 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 })) {
var draggedId = monitor.getItem().id;
var targetId = props.id;
var position = component.state.position;
props.endDrag(draggedId, targetId, position);
}
},
hover: function hover(props, monitor, component) {
if (!component.decoratedComponentInstance.props.userCanAccess) return;
if (!props.reorderable) return;
var isOverCurrent = monitor.isOver({ shallow: true });
if (isOverCurrent) {
var ownId = props.id;
var draggedId = monitor.getItem().id;
if (draggedId === ownId || component.props.selected) return;
var boundingRect = ReactDOM.findDOMNode(component).getBoundingClientRect();
var clientOffset = monitor.getClientOffset();
var offsetY = clientOffset.y - boundingRect.top;
var rowHeight = boundingRect.bottom - boundingRect.top;
var 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(function () {
component.setState({ forceExpand: true });
}, 1000);
}
component.setState({ position: 'into' });
} else if (offsetY < EDGE_SIZE) {
component.setState({ position: 'above' });
} else {
component.setState({ position: 'below' });
}
}
}
}
};