blob: 53725fec2f592c99a220c39a9ed559639b643b85 [file] [log] [blame]
// Licensed 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.
define([
'../../../app',
'../../../core/api',
'react',
'react-dom',
'./stores.react',
'./actions',
'../../components/react-components.react',
'../../components/stores',
'../../components/actions',
'../index-editor/actions',
'../index-editor/components.react',
'../../fauxton/components.react',
'../../documents/views',
'../../documents/helpers',
'react-bootstrap',
'../../../../assets/js/plugins/prettify'
],
function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, Components, ComponentsStore, ComponentsActions,
IndexEditorActions, IndexEditorComponents, GeneralComponents, DocumentViews, DocumentHelper, ReactBootstrap) {
var DeleteDBModal = DocumentViews.Views.DeleteDBModal;
var store = Stores.sidebarStore;
var LoadLines = Components.LoadLines;
var DesignDocSelector = IndexEditorComponents.DesignDocSelector;
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
var Popover = ReactBootstrap.Popover;
var Modal = ReactBootstrap.Modal;
var ConfirmationModal = GeneralComponents.ConfirmationModal;
var DeleteDatabaseModal = Components.DeleteDatabaseModal;
var deleteDbModalStore = ComponentsStore.deleteDbModalStore;
var MainSidebar = React.createClass({
propTypes: {
selectedNavItem: React.PropTypes.string.isRequired
},
getNewButtonLinks: function () { // these are links for the sidebar '+' on All Docs and All Design Docs
return DocumentHelper.getNewButtonLinks(this.props.databaseName);
},
buildDocLinks: function () {
var base = FauxtonAPI.urls('base', 'app', this.props.databaseName);
return FauxtonAPI.getExtensions('docLinks').map(function (link) {
return (
<li key={link.url} className={this.getNavItemClass(link.url)}>
<a id={link.url} href={base + link.url}>{link.title}</a>
</li>
);
}, this);
},
getNavItemClass: function (navItem) {
return (navItem === this.props.selectedNavItem) ? 'active' : '';
},
render: function () {
var docLinks = this.buildDocLinks();
var changesUrl = '#' + FauxtonAPI.urls('changes', 'app', this.props.databaseName, '');
var permissionsUrl = '#' + FauxtonAPI.urls('permissions', 'app', this.props.databaseName);
var databaseUrl = FauxtonAPI.urls('allDocs', 'app', this.props.databaseName, '');
var mangoQueryUrl = FauxtonAPI.urls('mango', 'query-app', this.props.databaseName);
var runQueryWithMangoText = app.i18n.en_US['run-query-with-mango'];
var buttonLinks = this.getNewButtonLinks();
return (
<ul className="nav nav-list">
<li className={this.getNavItemClass('all-docs')}>
<a id="all-docs"
href={"#/" + databaseUrl}
className="toggle-view">
All Documents
</a>
<div id="new-all-docs-button" className="add-dropdown">
<Components.MenuDropDown links={buttonLinks} />
</div>
</li>
<li className={this.getNavItemClass('mango-query')}>
<a
id="mango-query"
href={'#' + mangoQueryUrl}
className="toggle-view">
{runQueryWithMangoText}
</a>
</li>
<li className={this.getNavItemClass('permissions')}>
<a id="permissions" href={permissionsUrl}>Permissions</a>
</li>
<li className={this.getNavItemClass('changes')}>
<a id="changes" href={changesUrl}>Changes</a>
</li>
{docLinks}
<li className={this.getNavItemClass('design-docs')}>
<a
id="design-docs"
href={"#/" + databaseUrl + '?startkey="_design"&endkey="_design0"'}
className="toggle-view">
Design Documents
</a>
<div id="new-design-docs-button" className="add-dropdown">
<Components.MenuDropDown links={buttonLinks} />
</div>
</li>
</ul>
);
}
});
var IndexSection = React.createClass({
propTypes: {
urlNamespace: React.PropTypes.string.isRequired,
indexLabel: React.PropTypes.string.isRequired,
database: React.PropTypes.object.isRequired,
designDocName: React.PropTypes.string.isRequired,
items: React.PropTypes.array.isRequired,
isExpanded: React.PropTypes.bool.isRequired,
selectedIndex: React.PropTypes.string.isRequired,
onDelete: React.PropTypes.func.isRequired,
onClone: React.PropTypes.func.isRequired
},
getInitialState: function () {
return {
placement: 'bottom'
};
},
// this dynamically changes the placement of the menu (top/bottom) to prevent it going offscreen and causing some
// unsightly shifting
setPlacement: function (rowId) {
var rowTop = document.getElementById(rowId).getBoundingClientRect().top;
var toggleHeight = 150; // the height of the menu overlay, arrow, view row
var placement = (rowTop + toggleHeight > window.innerHeight) ? 'top' : 'bottom';
this.setState({ placement: placement });
},
createItems: function () {
// sort the indexes alphabetically
var sortedItems = this.props.items.sort();
return _.map(sortedItems, function (indexName, index) {
var href = FauxtonAPI.urls(this.props.urlNamespace, 'app', this.props.database.id, this.props.designDocName);
var className = (this.props.selectedIndex === indexName) ? 'active' : '';
return (
<li className={className} key={index}>
<a
id={this.props.designDocName + '_' + indexName}
href={"#/" + href + indexName}
className="toggle-view">
{indexName}
</a>
<OverlayTrigger
ref={"indexMenu-" + index}
trigger="click"
onEnter={this.setPlacement.bind(this, this.props.designDocName + '_' + indexName)}
placement={this.state.placement}
rootClose={true}
overlay={
<Popover id="index-menu-component-popover">
<ul>
<li onClick={this.indexAction.bind(this, 'edit', { indexName: indexName, onEdit: this.props.onEdit })}>
<span className="fonticon fonticon-file-code-o"></span>
Edit
</li>
<li onClick={this.indexAction.bind(this, 'clone', { indexName: indexName, onClone: this.props.onClone })}>
<span className="fonticon fonticon-files-o"></span>
Clone
</li>
<li onClick={this.indexAction.bind(this, 'delete', { indexName: indexName, onDelete: this.props.onDelete })}>
<span className="fonticon fonticon-trash"></span>
Delete
</li>
</ul>
</Popover>
}>
<span className="index-menu-toggle fonticon fonticon-wrench2"></span>
</OverlayTrigger>
</li>
);
}, this);
},
indexAction: function (action, params, e) {
e.preventDefault();
// ensures the menu gets closed. The hide() on the ref doesn't consistently close it
$('body').trigger('click');
switch (action) {
case 'delete':
Actions.showDeleteIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onDelete);
break;
case 'clone':
Actions.showCloneIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onClone);
break;
case 'edit':
params.onEdit(this.props.database.id, this.props.designDocName, params.indexName);
break;
}
},
toggle: function (e) {
e.preventDefault();
var newToggleState = !this.props.isExpanded;
var state = newToggleState ? 'show' : 'hide';
$(ReactDOM.findDOMNode(this)).find('.accordion-body').collapse(state);
this.props.toggle(this.props.designDocName, this.props.title);
},
render: function () {
// if this section has no content, omit it to prevent clutter. Otherwise it would show a toggle option that
// would hide/show nothing
if (this.props.items.length === 0) {
return null;
}
var toggleClassNames = 'accordion-header index-group-header';
var toggleBodyClassNames = 'index-list accordion-body collapse';
if (this.props.isExpanded) {
toggleClassNames += ' down';
toggleBodyClassNames += ' in';
}
var title = this.props.title;
var designDocName = this.props.designDocName;
var linkId = "nav-design-function-" + designDocName + this.props.selector;
return (
<li id={linkId}>
<a className={toggleClassNames} data-toggle="collapse" onClick={this.toggle}>
<div className="fonticon-play"></div>
{title}
</a>
<ul className={toggleBodyClassNames}>
{this.createItems()}
</ul>
</li>
);
}
});
var DesignDoc = React.createClass({
propTypes: {
database: React.PropTypes.object.isRequired,
sidebarListTypes: React.PropTypes.array.isRequired,
isExpanded: React.PropTypes.bool.isRequired,
selectedNavInfo: React.PropTypes.object.isRequired,
toggledSections: React.PropTypes.object.isRequired
},
getInitialState: function () {
return {
updatedSidebarListTypes: this.props.sidebarListTypes
};
},
componentWillMount: function () {
if (_.isEmpty(this.state.updatedSidebarListTypes) ||
(_.has(this.state.updatedSidebarListTypes[0], 'selector') && this.state.updatedSidebarListTypes[0].selector !== 'views')) {
var newList = this.state.updatedSidebarListTypes;
newList.unshift({
selector: 'views',
name: 'Views',
urlNamespace: 'view',
indexLabel: 'view',
onDelete: IndexEditorActions.deleteView,
onClone: IndexEditorActions.cloneView,
onEdit: IndexEditorActions.gotoEditViewPage
});
this.setState({ updatedSidebarListTypes: newList });
}
},
indexList: function () {
return _.map(this.state.updatedSidebarListTypes, function (index, key) {
var expanded = _.has(this.props.toggledSections, index.name) && this.props.toggledSections[index.name];
// if an index in this list is selected, pass that down
var selectedIndex = '';
if (this.props.selectedNavInfo.designDocSection === index.name) {
selectedIndex = this.props.selectedNavInfo.indexName;
}
return (
<IndexSection
icon={index.icon}
isExpanded={expanded}
urlNamespace={index.urlNamespace}
indexLabel={index.indexLabel}
onEdit={index.onEdit}
onDelete={index.onDelete}
onClone={index.onClone}
selectedIndex={selectedIndex}
toggle={this.props.toggle}
database={this.props.database}
designDocName={this.props.designDocName}
key={key}
title={index.name}
selector={index.selector}
items={_.keys(this.props.designDoc[index.selector])} />
);
}.bind(this));
},
toggle: function (e) {
e.preventDefault();
var newToggleState = !this.props.isExpanded;
var state = newToggleState ? 'show' : 'hide';
$(ReactDOM.findDOMNode(this)).find('#' + this.props.designDocName).collapse(state);
this.props.toggle(this.props.designDocName);
},
getNewButtonLinks: function () {
var newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', this.props.database.id);
var designDocName = this.props.designDocName;
var addNewLinks = _.reduce(FauxtonAPI.getExtensions('sidebar:links'), function (menuLinks, link) {
menuLinks.push({
title: link.title,
url: '#' + newUrlPrefix + '/' + link.url + '/' + designDocName,
icon: 'fonticon-plus-circled'
});
return menuLinks;
}, [{
title: 'New View',
url: '#' + FauxtonAPI.urls('new', 'addView', this.props.database.id, designDocName),
icon: 'fonticon-plus-circled'
}]);
return [{
title: 'Add New',
links: addNewLinks
}];
},
render: function () {
var buttonLinks = this.getNewButtonLinks();
var toggleClassNames = 'design-doc-section accordion-header';
var toggleBodyClassNames = 'design-doc-body accordion-body collapse';
if (this.props.isExpanded) {
toggleClassNames += ' down';
toggleBodyClassNames += ' in';
}
var designDocName = this.props.designDocName;
var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.database.id, designDocName);
var metadataRowClass = (this.props.selectedNavInfo.designDocSection === 'metadata') ? 'active' : '';
return (
<li className="nav-header">
<div id={"sidebar-tab-" + designDocName} className={toggleClassNames}>
<div id={"nav-header-" + designDocName} onClick={this.toggle} className='accordion-list-item'>
<div className="fonticon-play"></div>
<p className='design-doc-name'>
<span title={'_design/' + designDocName}>{designDocName}</span>
</p>
</div>
<div className='new-button add-dropdown'>
<Components.MenuDropDown links={buttonLinks} />
</div>
</div>
<ul className={toggleBodyClassNames} id={this.props.designDocName}>
<li className={metadataRowClass}>
<a href={"#/" + designDocMetaUrl} className="toggle-view accordion-header">
Metadata
</a>
</li>
{this.indexList()}
</ul>
</li>
);
}
});
var DesignDocList = React.createClass({
componentWillMount: function () {
var list = FauxtonAPI.getExtensions('sidebar:list');
this.sidebarListTypes = _.isUndefined(list) ? [] : list;
},
designDocList: function () {
return _.map(this.props.designDocs, function (designDoc, key) {
var ddName = designDoc.safeId;
// only pass down the selected nav info and toggle info if they're relevant for this particular design doc
var expanded = false,
toggledSections = {};
if (_.has(this.props.toggledSections, ddName)) {
expanded = this.props.toggledSections[ddName].visible;
toggledSections = this.props.toggledSections[ddName].indexGroups;
}
var selectedNavInfo = {};
if (this.props.selectedNav.navItem === 'designDoc' && this.props.selectedNav.designDocName === ddName) {
selectedNavInfo = this.props.selectedNav;
}
return (
<DesignDoc
toggle={this.props.toggle}
sidebarListTypes={this.sidebarListTypes}
isExpanded={expanded}
toggledSections={toggledSections}
selectedNavInfo={selectedNavInfo}
key={key}
designDoc={designDoc}
designDocName={ddName}
database={this.props.database} />
);
}.bind(this));
},
render: function () {
return (
<ul className="nav nav-list">
{this.designDocList()}
</ul>
);
}
});
var SidebarController = React.createClass({
getStoreState: function () {
return {
database: store.getDatabase(),
selectedNav: store.getSelected(),
designDocs: store.getDesignDocs(),
designDocList: store.getDesignDocList(),
availableDesignDocIds: store.getAvailableDesignDocs(),
toggledSections: store.getToggledSections(),
isLoading: store.isLoading(),
database: store.getDatabase(),
deleteDbModalProperties: deleteDbModalStore.getShowDeleteDatabaseModal(),
deleteIndexModalVisible: store.isDeleteIndexModalVisible(),
deleteIndexModalText: store.getDeleteIndexModalText(),
deleteIndexModalOnSubmit: store.getDeleteIndexModalOnSubmit(),
deleteIndexModalIndexName: store.getDeleteIndexModalIndexName(),
deleteIndexModalDesignDoc: store.getDeleteIndexDesignDoc(),
cloneIndexModalVisible: store.isCloneIndexModalVisible(),
cloneIndexModalTitle: store.getCloneIndexModalTitle(),
cloneIndexModalSelectedDesignDoc: store.getCloneIndexModalSelectedDesignDoc(),
cloneIndexModalNewDesignDocName: store.getCloneIndexModalNewDesignDocName(),
cloneIndexModalOnSubmit: store.getCloneIndexModalOnSubmit(),
cloneIndexDesignDocProp: store.getCloneIndexDesignDocProp(),
cloneIndexModalNewIndexName: store.getCloneIndexModalNewIndexName(),
cloneIndexSourceIndexName: store.getCloneIndexModalSourceIndexName(),
cloneIndexSourceDesignDocName: store.getCloneIndexModalSourceDesignDocName(),
cloneIndexModalIndexLabel: store.getCloneIndexModalIndexLabel()
};
},
getInitialState: function () {
return this.getStoreState();
},
componentDidMount: function () {
store.on('change', this.onChange, this);
deleteDbModalStore.on('change', this.onChange, this);
},
componentWillUnmount: function () {
store.off('change', this.onChange);
deleteDbModalStore.off('change', this.onChange, this);
},
onChange: function () {
if (this.isMounted()) {
this.setState(this.getStoreState());
}
},
showDeleteDatabaseModal: function (payload) {
ComponentsActions.showDeleteDatabaseModal(payload);
},
// handles deleting of any index regardless of type. The delete handler and all relevant info is set when the user
// clicks the delete action for a particular index
deleteIndex: function () {
// if the user is currently on the index that's being deleted, pass that info along to the delete handler. That can
// be used to redirect the user to somewhere appropriate
var isOnIndex = this.state.selectedNav.navItem === 'designDoc' &&
('_design/' + this.state.selectedNav.designDocName) === this.state.deleteIndexModalDesignDoc.id &&
this.state.selectedNav.indexName === this.state.deleteIndexModalIndexName;
this.state.deleteIndexModalOnSubmit({
isOnIndex: isOnIndex,
indexName: this.state.deleteIndexModalIndexName,
designDoc: this.state.deleteIndexModalDesignDoc,
designDocs: this.state.designDocs,
database: this.state.database
});
},
cloneIndex: function () {
this.state.cloneIndexModalOnSubmit({
sourceIndexName: this.state.cloneIndexSourceIndexName,
sourceDesignDocName: this.state.cloneIndexSourceDesignDocName,
targetDesignDocName: this.state.cloneIndexModalSelectedDesignDoc,
newDesignDocName: this.state.cloneIndexModalNewDesignDocName,
newIndexName: this.state.cloneIndexModalNewIndexName,
designDocs: this.state.designDocs,
database: this.state.database,
onComplete: Actions.hideCloneIndexModal
});
},
render: function () {
if (this.state.isLoading) {
return <LoadLines />;
}
return (
<nav className="sidenav">
<MainSidebar
selectedNavItem={this.state.selectedNav.navItem}
databaseName={this.state.database.id} />
<DesignDocList
selectedNav={this.state.selectedNav}
toggle={Actions.toggleContent}
toggledSections={this.state.toggledSections}
designDocs={this.state.designDocList}
database={this.state.database} />
<DeleteDatabaseModal
showHide={this.showDeleteDatabaseModal}
modalProps={this.state.deleteDbModalProperties} />
{/* the delete and clone index modals handle all index types, hence the props all being pulled from the store */}
<ConfirmationModal
title="Confirm Deletion"
visible={this.state.deleteIndexModalVisible}
text={this.state.deleteIndexModalText}
onClose={Actions.hideDeleteIndexModal}
onSubmit={this.deleteIndex} />
<CloneIndexModal
visible={this.state.cloneIndexModalVisible}
title={this.state.cloneIndexModalTitle}
close={Actions.hideCloneIndexModal}
submit={this.cloneIndex}
designDocArray={this.state.availableDesignDocIds}
selectedDesignDoc={this.state.cloneIndexModalSelectedDesignDoc}
newDesignDocName={this.state.cloneIndexModalNewDesignDocName}
newIndexName={this.state.cloneIndexModalNewIndexName}
indexLabel={this.state.cloneIndexModalIndexLabel} />
</nav>
);
}
});
var CloneIndexModal = React.createClass({
propTypes: {
visible: React.PropTypes.bool.isRequired,
title: React.PropTypes.string,
close: React.PropTypes.func.isRequired,
submit: React.PropTypes.func.isRequired,
designDocArray: React.PropTypes.array.isRequired,
selectedDesignDoc: React.PropTypes.string.isRequired,
newDesignDocName: React.PropTypes.string.isRequired,
newIndexName: React.PropTypes.string.isRequired,
indexLabel: React.PropTypes.string.isRequired
},
getDefaultProps: function () {
return {
title: 'Clone Index',
visible: false
};
},
submit: function () {
if (!this.refs.designDocSelector.validate()) {
return;
}
if (this.props.newIndexName === '') {
FauxtonAPI.addNotification({
msg: 'Please enter the new index name.',
type: 'error',
clear: true
});
return;
}
this.props.submit();
},
close: function (e) {
if (e) {
e.preventDefault();
}
this.props.close();
},
setNewIndexName: function (e) {
Actions.setNewCloneIndexName(e.target.value);
},
render: function () {
return (
<Modal dialogClassName="clone-index-modal" show={this.props.visible} onHide={this.close}>
<Modal.Header closeButton={true}>
<Modal.Title>{this.props.title}</Modal.Title>
</Modal.Header>
<Modal.Body>
<form className="form" method="post" onSubmit={this.submit}>
<p>
Select the design document where the cloned {this.props.indexLabel} will be created, and then enter
a name for the cloned {this.props.indexLabel}.
</p>
<div className="row">
<DesignDocSelector
ref="designDocSelector"
designDocList={this.props.designDocArray}
selectedDesignDocName={this.props.selectedDesignDoc}
newDesignDocName={this.props.newDesignDocName}
onSelectDesignDoc={Actions.selectDesignDoc}
onChangeNewDesignDocName={Actions.updateNewDesignDocName} />
</div>
<div className="clone-index-name-row">
<label className="new-index-title-label" htmlFor="new-index-name">{this.props.indexLabel} Name</label>
<input type="text" id="new-index-name" value={this.props.newIndexName} onChange={this.setNewIndexName}
placeholder="Enter new view name" />
</div>
</form>
</Modal.Body>
<Modal.Footer>
<button onClick={this.submit} data-bypass="true" className="btn btn-success save">
<i className="icon fonticon-ok-circled" /> Clone {this.props.indexLabel}</button>
<a href="#" className="cancel-link" onClick={this.close} data-bypass="true">Cancel</a>
</Modal.Footer>
</Modal>
);
}
});
return {
SidebarController: SidebarController,
DesignDoc: DesignDoc,
CloneIndexModal: CloneIndexModal
};
});