blob: 99928a756077090a65117700a59817d86cabc84b [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
/* eslint camelcase: 0 */
/* globals ContextMenu ToolTip */
'use strict';
/**
* Gets the current project url.
* @param {boolean} rest - Return a "rest" version of the url.
* @return {string} - Project Url
*/
var _getProjectUrl = function(rest = true) {
var nbhd;
var proj;
var nbhd_proj;
var identClasses = document.getElementById('page-body').className.split(' ');
var basePath = rest ? '/rest/' : '/';
for (let cls of identClasses) {
if (cls.indexOf('project-') === 0) {
proj = cls.slice('project-'.length);
}
}
nbhd = window.location.pathname.split('/')[1];
if (proj === '--init--') {
nbhd_proj = nbhd;
} else {
nbhd_proj = `${nbhd}/${proj}`;
}
return basePath + nbhd_proj;
};
const ToolsPropType = React.PropTypes.shape({
mount_point: React.PropTypes.string,
name: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
is_anchored: React.PropTypes.bool.isRequired,
tool_name: React.PropTypes.string.isRequired,
icon: React.PropTypes.string,
children: React.PropTypes.array,
admin_options: React.PropTypes.array
});
/**
* A single NavBar item.
* @constructor
*/
var NavBarItem = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
currentOptionMenu: React.PropTypes.object,
isGrouper: React.PropTypes.bool,
mount_point: React.PropTypes.string,
onOptionClick: React.PropTypes.func.isRequired,
options: React.PropTypes.array,
handleType: React.PropTypes.string,
is_anchored: React.PropTypes.bool
},
render: function() {
var divClasses = "tb-item tb-item-edit";
var spanClasses = this.props.handleType + " ordinal-item";
if (this.props.is_anchored) {
divClasses += " anchored";
}
if (this.props.isGrouper) {
spanClasses += " toolbar-grouper";
}
return (
<div className={divClasses}>
<ToolTip targetSelector=".anchored .draggable-handle"
position="top"
theme="tooltipster-default"
delay={250}/>
<ToolTip targetSelector=".anchored .draggable-handle-sub"
position="right"
theme="tooltipster-default"
delay={250}/>
<a>
{!_.isEmpty(this.props.options) &&
<i className='config-tool fa fa-cog' onClick={this.handleOptionClick}> </i>}
<span
className={spanClasses}
data-mount-point={this.props.mount_point}
title={this.props.is_anchored ? 'This item cannot be moved.' : ''}>
{this.props.name}
</span>
</a>
{this.props.currentOptionMenu.tool &&
this.props.currentOptionMenu.tool === this.props.mount_point &&
<ContextMenu {...this.props} items={this.props.options} onOptionClick={this.props.onOptionClick}/>}
</div>
);
},
handleOptionClick: function(event) {
this.props.onOptionClick(this.props.mount_point);
}
});
/**
* An input component that updates the NavBar's grouping threshold.
* @constructor
*/
var GroupingThreshold = React.createClass({
propTypes: {
initialValue: React.PropTypes.number.isRequired,
isHidden: React.PropTypes.bool,
onUpdateThreshold: React.PropTypes.func
},
getInitialState: function() {
return {
value: this.props.initialValue
};
},
handleChange: function(event) {
this.setState({
value: event.target.value
});
this.props.onUpdateThreshold(event);
},
render: function() {
return (
<div>
{Boolean(this.props.isHidden) &&
<div id='threshold-config'>
<span>
<label htmlFor='threshold-input'>Grouping Threshold</label>
<ToolTip targetSelector="#threshold-input" position="top" contentAsHTML/>
<input type='number' name='threshold-input' id="threshold-input"
title='When you have multiple tools of the same type,\
<u>this number</u> determines if they will fit in the navigation \
bar or be grouped into a dropdown.'
value={this.state.value}
onChange={this.handleChange}
min='1' max='50'
/>
</span>
</div>}
</div>
);
}
});
/**
* The NavBar when in "Normal" mode.
* @constructor
*/
var NormalNavItem = React.createClass({
propTypes: {
name: React.PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired,
children: React.PropTypes.object,
classes: React.PropTypes.string
},
mixins: [React.addons.PureRenderMixin],
render: function() {
return (
<li key={`tb-norm-${_.uniqueId()}`}>
<a href={this.props.url} className={this.props.classes}>
{this.props.name}
</a>
{this.props.children}
</li>
);
}
});
/**
* Toggle Button
* @constructor
*/
var ToggleAddNewTool = React.createClass({
propTypes: {
installableTools: React.PropTypes.arrayOf(
React.PropTypes.shape({
text: React.PropTypes.string.isRequired,
href: React.PropTypes.string,
tooltip: React.PropTypes.string
})
).isRequired
},
getInitialState: function() {
return {
visible: false
};
},
handleToggle: function() {
this.setState({
visible: !this.state.visible
});
},
render: function() {
return (
<div>
<a onClick={this.handleToggle} className="add-tool-toggle">
Add New...
</a>
{this.state.visible &&
<ContextMenu
{...this.props}
classes={['admin_modal']}
onOptionClick={this.handleToggle}
items={this.props.installableTools}
/>
}
</div>
);
}
});
/**
* The NavBar when in "Normal" mode.
* @constructor
*/
var NormalNavBar = React.createClass({
propTypes: {
items: React.PropTypes.arrayOf(ToolsPropType).isRequired,
installableTools: React.PropTypes.arrayOf(
React.PropTypes.shape({
text: React.PropTypes.string.isRequired,
href: React.PropTypes.string,
tooltip: React.PropTypes.string
})
).isRequired
},
buildMenu: function(item, i) {
let classes = window.location.pathname.startsWith(item.url) ? 'active-nav-link' : '';
var subMenu;
if (item.children) {
subMenu = item.children.map(this.buildMenu);
}
return (
<NormalNavItem url={item.url} name={item.name} classes={classes} key={`normal-nav-${_.uniqueId()}`}>
<ul>
{subMenu}
</ul>
</NormalNavItem>
);
},
render: function() {
var listItems = this.props.items.map(this.buildMenu);
return (
<ul
id="normal-nav-bar"
className="dropdown">
{listItems}
<li id="add-tool-container">
<ToggleAddNewTool installableTools={this.props.installableTools}/>
</li>
</ul>
);
}
});
/**
* The NavBar when in "Admin" mode.
* @constructor
*/
var AdminNav = React.createClass({
propTypes: {
tools: React.PropTypes.arrayOf(ToolsPropType),
currentOptionMenu: React.PropTypes.object,
installableTools: React.PropTypes.arrayOf(
React.PropTypes.shape({
text: React.PropTypes.string.isRequired,
href: React.PropTypes.string,
tooltip: React.PropTypes.string
})
),
onOptionClick: React.PropTypes.func.isRequired
},
buildMenu: function(items, isSubMenu = false) {
var _this = this;
var [tools, anchoredTools, endTools] = [[], [], []];
var subMenu;
var childOptionsOpen;
var _handle;
var toolList;
var isAnchored;
for (let item of items) {
if (item.children) {
subMenu = this.buildMenu(item.children, true);
} else {
subMenu = null;
}
_handle = isSubMenu ? "draggable-handle-sub" : 'draggable-handle';
if (item.mount_point === 'admin') {
// force admin to end, just like 'Project.sitemap()' does
toolList = endTools;
isAnchored = true;
} else if (item.is_anchored) {
toolList = anchoredTools;
isAnchored = true;
} else {
toolList = tools;
isAnchored = false;
}
let coreItem = (<NavBarItem
{..._this.props}
mount_point={item.mount_point}
name={item.name}
handleType={_handle}
isGrouper={item.children && item.children.length > 0}
url={item.url}
key={'tb-item-' + _.uniqueId()}
is_anchored={isAnchored}
options={item.admin_options}
/>);
if (subMenu) {
childOptionsOpen = _.contains(_.pluck(item.children, 'mount_point'),
this.props.currentOptionMenu.tool);
toolList.push(<NavBarItemWithSubMenu
key={_.uniqueId()}
tool={coreItem}
subMenu={subMenu}
childOptionsOpen={childOptionsOpen}
/>);
} else {
toolList.push(coreItem);
}
}
return (
<div className='react-drag'>
{anchoredTools}
<ReactReorderable
key={'reorder-' + _.uniqueId()}
handle={"." + _handle}
mode={isSubMenu ? 'list' : 'grid'}
onDragStart={_this.props.onToolDragStart}
onDrop={_this.props.onToolReorder}>
{tools}
</ReactReorderable>
{endTools}
{!isSubMenu && <div id="add-tool-container" className="unlocked-container">
<ToggleAddNewTool installableTools={this.props.installableTools}/>
</div>}
</div>
);
},
render: function() {
var tools = this.buildMenu(this.props.tools);
return <div>{tools}</div>;
}
});
var NavBarItemWithSubMenu = React.createClass({
propTypes: {
subMenu: React.PropTypes.node,
childOptionsOpen: React.PropTypes.bool,
tool: React.PropTypes.node
},
render: function() {
return (
<div className={"tb-item-container" + (this.props.childOptionsOpen ? " child-options-open" : "")}>
{this.props.tool}
{this.props.subMenu &&
<AdminItemGroup key={_.uniqueId()}>
{this.props.subMenu}
</AdminItemGroup>
}
</div>
);
}
});
/**
* The NavBar when in "Admin" mode.
* @constructor
*/
var AdminItemGroup = React.createClass({
propTypes: {
children: React.PropTypes.object
},
render: function() {
return (
<div className="tb-item-grouper">
{this.props.children}
</div>
);
}
});
/**
* The button that toggles NavBar modes.
* @constructor
*/
var ToggleAdminButton = React.createClass({
propTypes: {
handleButtonPush: React.PropTypes.func,
visible: React.PropTypes.bool
},
render: function() {
var classes = this.props.visible ? 'fa fa-unlock' : 'fa fa-lock';
return (
<button id='toggle-admin-btn' onClick={this.props.handleButtonPush} className='admin-toolbar-right'>
<i className={classes}> </i>
</button>
);
}
});
/**
* The main "controller view" of the NavBar.
* @constructor
* @param {object} initialData
*/
var Main = React.createClass({
propTypes: {
initialData: React.PropTypes.shape({
menu: React.PropTypes.arrayOf(ToolsPropType),
installableTools: React.PropTypes.array,
grouping_threshold: React.PropTypes.number.isRequired
}),
installableTools: React.PropTypes.array
},
getInitialState: function() {
return {
data: this.props.initialData,
visible: true,
currentOptionMenu: {
tool: null
}
};
},
/**
* When invoked, this updates the state with the latest data from the server.
*/
getNavJson: function() {
$.get(`${_getProjectUrl(false)}/_nav.json?admin_options=1`, function(result) {
if (this.isMounted()) {
this.setState({
data: result
});
}
}.bind(this));
},
/**
* Handles the locking and unlocking of the NavBar
*/
handleToggleAdmin: function() {
this.setState({
visible: !this.state.visible
});
},
handleShowOptionMenu: function(mount_point) {
this.setState({
currentOptionMenu: {
tool: mount_point
}
});
},
/**
* Handles the changing of the NavBars grouping threshold.
* @param {object} event Fired when the threshold changes
* @return {boolean} False
*/
onUpdateThreshold: function(event) {
var thres = event.target.value;
var url = `${_getProjectUrl()}/admin/configure_tool_grouping`;
var csrf = $.cookie('_session_id');
var data = {
_session_id: csrf,
grouping_threshold: thres
};
$.post(url, data, () => this.getNavJson());
return false;
},
/**
* Handles the sending and updating tool ordinals.
*/
onToolReorder: function() {
$('.react-drag.dragging').removeClass('dragging');
let params = {_session_id: $.cookie('_session_id')};
let toolNodes = $(ReactDOM.findDOMNode(this)).find('span.ordinal-item').not(".toolbar-grouper");
for (let i = 0; i < toolNodes.length; i++) {
params[i] = toolNodes[i].dataset.mountPoint;
}
let _this = this;
let url = _getProjectUrl() + '/admin/mount_order';
$.ajax({
type: 'POST',
url: url,
data: params,
success: function() {
$('#messages').notify('Tool order updated',
{
status: 'confirm',
interval: 500,
timer: 2000
});
_this.getNavJson();
},
error: function() {
$('#messages').notify('Error saving tool order.',
{
status: 'error'
});
}
});
},
onToolDragStart: function(obj) {
// this is done with jQuery instead of rendering different HTML with react
// because that means you re-render the HTML while the drag is happening
// and the actual dragging doesn't work any more
var dragging_mount_point = obj.props.children.props.mount_point;
$(`[data-mount-point="${dragging_mount_point}"]`).closest('.react-drag').addClass('dragging');
},
render: function() {
var _this = this;
var navBarSwitch = showAdmin => {
var navbar;
if (showAdmin) {
navbar = (<AdminNav
tools={_this.state.data.menu}
installableTools={_this.state.data.installable_tools}
data={_this.state.data}
onToolReorder={_this.onToolReorder}
onToolDragStart={_this.onToolDragStart}
editMode={_this.state.visible}
currentOptionMenu={_this.state.currentOptionMenu}
onOptionClick={_this.handleShowOptionMenu}
currentToolOptions={this.state.currentToolOptions}
/>);
} else {
navbar = (<div>
<NormalNavBar
items={_this.state.data.menu}
installableTools={_this.state.data.installable_tools}
/>
</div>);
}
return navbar;
};
var navBar = navBarSwitch(this.state.visible);
var max_tool_count = _.chain(this.state.data.menu)
.map(item => {
return item.children ? _.pluck(item.children, 'tool_name') : item.tool_name;
})
.flatten()
.countBy()
.values()
.max()
.value();
var show_grouping_threshold = max_tool_count > 1;
return (
<div>
{navBar}
<div id='bar-config'>
{show_grouping_threshold &&
<GroupingThreshold
onUpdateThreshold={this.onUpdateThreshold}
isHidden={this.state.visible}
initialValue={parseInt(this.state.data.grouping_threshold, 10)}/>}
</div>
<ToggleAdminButton
handleButtonPush={this.handleToggleAdmin}
visible={this.state.visible}/>
</div>
);
}
});