http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/sidebar.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/sidebar/sidebar.react.jsx b/app/addons/documents/sidebar/sidebar.react.jsx index a96b647..c8f6146 100644 --- a/app/addons/documents/sidebar/sidebar.react.jsx +++ b/app/addons/documents/sidebar/sidebar.react.jsx @@ -15,22 +15,32 @@ define([ 'api', 'react', 'react-dom', - 'addons/documents/sidebar/stores', + 'addons/documents/sidebar/stores.react', 'addons/documents/sidebar/actions', - - 'addons/components/react-components.react', 'addons/components/stores', 'addons/components/actions', + 'addons/documents/index-editor/actions', + 'addons/documents/index-editor/components.react', + 'addons/fauxton/components.react', + 'addons/documents/views', 'addons/documents/helpers', + 'libs/react-bootstrap', 'plugins/prettify' ], -function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, - Components, ComponentsStore, ComponentsActions, DocumentHelper) { +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; @@ -110,38 +120,105 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, </ul> ); } - }); + var IndexSection = React.createClass({ propTypes: { urlNamespace: React.PropTypes.string.isRequired, - databaseName: 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 + 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 () { - return _.map(this.props.items, function (index, key) { - var href = FauxtonAPI.urls(this.props.urlNamespace, 'app', this.props.databaseName, this.props.designDocName); - var className = (this.props.selectedIndex === index) ? 'active' : ''; + + // 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={key}> + <li className={className} key={index}> <a - id={this.props.designDocName + '_' + index} - href={"#/" + href + index} + id={this.props.designDocName + '_' + indexName} + href={"#/" + href + indexName} className="toggle-view"> - {index} + {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; @@ -186,6 +263,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, 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, @@ -206,7 +284,11 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, newList.unshift({ selector: 'views', name: 'Views', - urlNamespace: 'view' + urlNamespace: 'view', + indexLabel: 'view', + onDelete: IndexEditorActions.deleteView, + onClone: IndexEditorActions.cloneView, + onEdit: IndexEditorActions.gotoEditViewPage }); this.setState({ updatedSidebarListTypes: newList }); } @@ -227,9 +309,13 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, 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} - databaseName={this.props.databaseName} + database={this.props.database} designDocName={this.props.designDocName} key={key} title={index.name} @@ -248,8 +334,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, }, getNewButtonLinks: function () { - var databaseName = this.props.databaseName; - var newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', databaseName); + 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) { @@ -261,7 +346,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, return menuLinks; }, [{ title: 'New View', - url: '#' + FauxtonAPI.urls('new', 'addView', databaseName, designDocName), + url: '#' + FauxtonAPI.urls('new', 'addView', this.props.database.id, designDocName), icon: 'fonticon-plus-circled' }]); @@ -281,7 +366,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, toggleBodyClassNames += ' in'; } var designDocName = this.props.designDocName; - var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.databaseName, designDocName); + var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.database.id, designDocName); var metadataRowClass = (this.props.selectedNavInfo.designDocSection === 'metadata') ? 'active' : ''; return ( @@ -344,7 +429,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, key={key} designDoc={designDoc} designDocName={ddName} - databaseName={this.props.databaseName} /> + database={this.props.database} /> ); }.bind(this)); }, @@ -361,13 +446,32 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, var SidebarController = React.createClass({ getStoreState: function () { return { - databaseName: store.getDatabaseName(), + 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() + 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() }; }, @@ -395,33 +499,173 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, 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.databaseName} /> + databaseName={this.state.database.id} /> <DesignDocList selectedNav={this.state.selectedNav} toggle={Actions.toggleContent} toggledSections={this.state.toggledSections} - designDocs={this.state.designDocs} - databaseName={this.state.databaseName} /> - + 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 + DesignDoc: DesignDoc, + CloneIndexModal: CloneIndexModal }; });
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/stores.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/sidebar/stores.js b/app/addons/documents/sidebar/stores.js deleted file mode 100644 index f351fcd..0000000 --- a/app/addons/documents/sidebar/stores.js +++ /dev/null @@ -1,191 +0,0 @@ -// 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', - 'api', - 'addons/documents/sidebar/actiontypes' -], - -function (app, FauxtonAPI, ActionTypes) { - var Stores = {}; - - Stores.SidebarStore = FauxtonAPI.Store.extend({ - - initialize: function () { - this._selected = { - navItem: 'all-docs', - designDocName: '', - designDocSection: '', // metadata / name of index group ("Views", etc.) - indexName: '' - }; - this._loading = true; - this._toggledSections = {}; - }, - - newOptions: function (options) { - this._database = options.database; - this._designDocs = options.designDocs; - this._loading = false; - - // this can be expanded in future as we need. Right now it can only set a top-level nav item ('all docs', - // 'permissions' etc.) and not a nested page - if (options.selectedNavItem) { - this._selected = { - navItem: options.selectedNavItem, - designDocName: '', - designDocSection: '', - indexName: '' - }; - } - }, - - isLoading: function () { - return this._loading; - }, - - getDatabase: function () { - if (this.isLoading()) { - return {}; - } - return this._database; - }, - - // used to toggle both design docs, and any index groups within them - toggleContent: function (designDoc, indexGroup) { - if (!this._toggledSections[designDoc]) { - this._toggledSections[designDoc] = { - visible: true, - indexGroups: {} - }; - return; - } - - if (indexGroup) { - return this.toggleIndexGroup(designDoc, indexGroup); - } - - this._toggledSections[designDoc].visible = !this._toggledSections[designDoc].visible; - }, - - toggleIndexGroup: function (designDoc, indexGroup) { - var expanded = this._toggledSections[designDoc].indexGroups[indexGroup]; - - if (_.isUndefined(expanded)) { - this._toggledSections[designDoc].indexGroups[indexGroup] = true; - return; - } - - this._toggledSections[designDoc].indexGroups[indexGroup] = !expanded; - }, - - isVisible: function (designDoc, indexGroup) { - if (!this._toggledSections[designDoc]) { - return false; - } - if (indexGroup) { - return this._toggledSections[designDoc].indexGroups[indexGroup]; - } - return this._toggledSections[designDoc].visible; - }, - - getSelected: function () { - return this._selected; - }, - - setSelected: function (params) { - this._selected = { - navItem: params.navItem, - designDocName: params.designDocName, - designDocSection: params.designDocSection, - indexName: params.indexName - }; - - if (params.designDocName) { - if (!_.has(this._toggledSections, params.designDocName)) { - this._toggledSections[params.designDocName] = { visible: true, indexGroups: {} }; - } - this._toggledSections[params.designDocName].visible = true; - - if (params.designDocSection) { - this._toggledSections[params.designDocName].indexGroups[params.designDocSection] = true; - } - } - }, - - getToggledSections: function () { - return this._toggledSections; - }, - - getDatabaseName: function () { - if (this.isLoading()) { - return ''; - } - return this._database.safeID(); - }, - - getDesignDocs: function () { - if (this.isLoading()) { - return {}; - } - var docs = this._designDocs.toJSON(); - - docs = _.filter(docs, function (doc) { - if (_.has(doc.doc, 'language')) { - return doc.doc.language !== 'query'; - } - return true; - }); - - return docs.map(function (doc) { - doc.safeId = app.utils.safeURLName(doc._id.replace(/^_design\//, "")); - return _.extend(doc, doc.doc); - }); - }, - - dispatch: function (action) { - switch (action.type) { - case ActionTypes.SIDEBAR_SET_SELECTED_NAV_ITEM: - this.setSelected(action.options); - break; - - case ActionTypes.SIDEBAR_NEW_OPTIONS: - this.newOptions(action.options); - break; - - case ActionTypes.SIDEBAR_TOGGLE_CONTENT: - this.toggleContent(action.designDoc, action.indexGroup); - break; - - case ActionTypes.SIDEBAR_FETCHING: - this._loading = true; - break; - - case ActionTypes.SIDEBAR_REFRESH: - break; - - default: - return; - // do nothing - } - - this.triggerChange(); - } - - }); - - Stores.sidebarStore = new Stores.SidebarStore(); - Stores.sidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.sidebarStore.dispatch); - - return Stores; - -}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/stores.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/sidebar/stores.react.jsx b/app/addons/documents/sidebar/stores.react.jsx new file mode 100644 index 0000000..0b8c031 --- /dev/null +++ b/app/addons/documents/sidebar/stores.react.jsx @@ -0,0 +1,345 @@ +// 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', + 'api', + 'react', + 'addons/documents/sidebar/actiontypes' +], + +function (app, FauxtonAPI, React, ActionTypes) { + var Stores = {}; + + Stores.SidebarStore = FauxtonAPI.Store.extend({ + + initialize: function () { + this.reset(); + }, + + reset: function () { + this._designDocs = new Backbone.Collection(); + this._selected = { + navItem: 'all-docs', + designDocName: '', + designDocSection: '', // 'metadata' / name of index group ("Views", etc.) + indexName: '' + }; + this._loading = true; + this._toggledSections = {}; + + this._deleteIndexModalVisible = false; + this._deleteIndexModalDesignDocName = ''; + this._deleteIndexModalText = ''; + this._deleteIndexModalIndexName = ''; + this._deleteIndexModalOnSubmit = function () { }; + + this._cloneIndexModalVisible = false; + this._cloneIndexDesignDocProp = ''; + this._cloneIndexModalTitle = ''; + this._cloneIndexModalSelectedDesignDoc = ''; + this._cloneIndexModalNewDesignDocName = ''; + this._cloneIndexModalNewIndexName = ''; + this._cloneIndexModalIndexLabel = ''; + this._cloneIndexModalOnSubmit = function () { }; + }, + + newOptions: function (options) { + this._database = options.database; + this._designDocs = options.designDocs; + this._loading = false; + + // this can be expanded in future as we need. Right now it can only set a top-level nav item ('all docs', + // 'permissions' etc.) and not a nested page + if (options.selectedNavItem) { + this._selected = { + navItem: options.selectedNavItem, + designDocName: '', + designDocSection: '', + indexName: '' + }; + } + }, + + updatedDesignDocs: function (designDocs) { + this._designDocs = designDocs; + }, + + isDeleteIndexModalVisible: function () { + return this._deleteIndexModalVisible; + }, + + getDeleteIndexModalText: function () { + return this._deleteIndexModalText; + }, + + getDeleteIndexModalOnSubmit: function () { + return this._deleteIndexModalOnSubmit; + }, + + isLoading: function () { + return this._loading; + }, + + getDatabase: function () { + if (this.isLoading()) { + return {}; + } + return this._database; + }, + + // used to toggle both design docs, and any index groups within them + toggleContent: function (designDoc, indexGroup) { + if (!this._toggledSections[designDoc]) { + this._toggledSections[designDoc] = { + visible: true, + indexGroups: {} + }; + return; + } + + if (indexGroup) { + return this.toggleIndexGroup(designDoc, indexGroup); + } + + this._toggledSections[designDoc].visible = !this._toggledSections[designDoc].visible; + }, + + toggleIndexGroup: function (designDoc, indexGroup) { + var expanded = this._toggledSections[designDoc].indexGroups[indexGroup]; + + if (_.isUndefined(expanded)) { + this._toggledSections[designDoc].indexGroups[indexGroup] = true; + return; + } + + this._toggledSections[designDoc].indexGroups[indexGroup] = !expanded; + }, + + isVisible: function (designDoc, indexGroup) { + if (!this._toggledSections[designDoc]) { + return false; + } + if (indexGroup) { + return this._toggledSections[designDoc].indexGroups[indexGroup]; + } + return this._toggledSections[designDoc].visible; + }, + + getSelected: function () { + return this._selected; + }, + + setSelected: function (params) { + this._selected = { + navItem: params.navItem, + designDocName: params.designDocName, + designDocSection: params.designDocSection, + indexName: params.indexName + }; + + if (params.designDocName) { + if (!_.has(this._toggledSections, params.designDocName)) { + this._toggledSections[params.designDocName] = { visible: true, indexGroups: {} }; + } + this._toggledSections[params.designDocName].visible = true; + + if (params.designDocSection) { + this._toggledSections[params.designDocName].indexGroups[params.designDocSection] = true; + } + } + }, + + getToggledSections: function () { + return this._toggledSections; + }, + + getDatabaseName: function () { + if (this.isLoading()) { + return ''; + } + return this._database.safeID(); + }, + + getDesignDocs: function () { + return this._designDocs; + }, + + // returns a simple array of design doc IDs + getAvailableDesignDocs: function () { + var availableDocs = this.getDesignDocs().filter(function (doc) { + return !doc.isMangoDoc(); + }); + return _.map(availableDocs, function (doc) { + return doc.id; + }); + }, + + getDesignDocList: function () { + if (this.isLoading()) { + return {}; + } + var docs = this._designDocs.toJSON(); + + docs = _.filter(docs, function (doc) { + if (_.has(doc.doc, 'language')) { + return doc.doc.language !== 'query'; + } + return true; + }); + + return docs.map(function (doc) { + doc.safeId = app.utils.safeURLName(doc._id.replace(/^_design\//, "")); + return _.extend(doc, doc.doc); + }); + }, + + showDeleteIndexModal: function (params) { + this._deleteIndexModalIndexName = params.indexName; + this._deleteIndexModalDesignDocName = params.designDocName; + this._deleteIndexModalVisible = true; + this._deleteIndexModalText = (<div>Are you sure you want to delete the <code>{this._deleteIndexModalIndexName}</code> {params.indexLabel}?</div>); + this._deleteIndexModalOnSubmit = params.onDelete; + }, + + getDeleteIndexModalIndexName: function () { + return this._deleteIndexModalIndexName; + }, + + getDeleteIndexDesignDoc: function () { + var designDoc = this._designDocs.find(function (ddoc) { + return '_design/' + this._deleteIndexModalDesignDocName === ddoc.id; + }, this); + + return (designDoc) ? designDoc.dDocModel() : null; + }, + + isCloneIndexModalVisible: function () { + return this._cloneIndexModalVisible; + }, + + getCloneIndexModalTitle: function () { + return this._cloneIndexModalTitle; + }, + + showCloneIndexModal: function (params) { + this._cloneIndexModalIndexLabel = params.indexLabel; + this._cloneIndexModalTitle = params.cloneIndexModalTitle; + this._cloneIndexModalSourceIndexName = params.sourceIndexName; + this._cloneIndexModalSourceDesignDocName = params.sourceDesignDocName; + this._cloneIndexModalSelectedDesignDoc = '_design/' + params.sourceDesignDocName; + this._cloneIndexDesignDocProp = ''; + this._cloneIndexModalVisible = true; + this._cloneIndexModalOnSubmit = params.onSubmit; + }, + + getCloneIndexModalIndexLabel: function () { + return this._cloneIndexModalIndexLabel; + }, + + getCloneIndexModalOnSubmit: function () { + return this._cloneIndexModalOnSubmit; + }, + + getCloneIndexModalSourceIndexName: function () { + return this._cloneIndexModalSourceIndexName; + }, + + getCloneIndexModalSourceDesignDocName: function () { + return this._cloneIndexModalSourceDesignDocName; + }, + + getCloneIndexDesignDocProp: function () { + return this._cloneIndexDesignDocProp; + }, + + getCloneIndexModalSelectedDesignDoc: function () { + return this._cloneIndexModalSelectedDesignDoc; + }, + + getCloneIndexModalNewDesignDocName: function () { + return this._cloneIndexModalNewDesignDocName; + }, + + getCloneIndexModalNewIndexName: function () { + return this._cloneIndexModalNewIndexName; + }, + + dispatch: function (action) { + switch (action.type) { + case ActionTypes.SIDEBAR_SET_SELECTED_NAV_ITEM: + this.setSelected(action.options); + break; + + case ActionTypes.SIDEBAR_NEW_OPTIONS: + this.newOptions(action.options); + break; + + case ActionTypes.SIDEBAR_TOGGLE_CONTENT: + this.toggleContent(action.designDoc, action.indexGroup); + break; + + case ActionTypes.SIDEBAR_FETCHING: + this._loading = true; + break; + + case ActionTypes.SIDEBAR_REFRESH: + break; + + case ActionTypes.SIDEBAR_SHOW_DELETE_INDEX_MODAL: + this.showDeleteIndexModal(action.options); + break; + + case ActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL: + this._deleteIndexModalVisible = false; + break; + + case ActionTypes.SIDEBAR_SHOW_CLONE_INDEX_MODAL: + this.showCloneIndexModal(action.options); + break; + + case ActionTypes.SIDEBAR_HIDE_CLONE_INDEX_MODAL: + this._cloneIndexModalVisible = false; + break; + + case ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_CHANGE: + this._cloneIndexModalSelectedDesignDoc = action.options.value; + break; + + case ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_NEW_NAME_UPDATED: + this._cloneIndexModalNewDesignDocName = action.options.value; + break; + + case ActionTypes.SIDEBAR_CLONE_MODAL_UPDATE_INDEX_NAME: + this._cloneIndexModalNewIndexName = action.options.value; + break; + + case ActionTypes.SIDEBAR_UPDATED_DESIGN_DOCS: + this.updatedDesignDocs(action.options.designDocs); + break; + + default: + return; + // do nothing + } + + this.triggerChange(); + } + + }); + + Stores.sidebarStore = new Stores.SidebarStore(); + Stores.sidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.sidebarStore.dispatch); + + return Stores; + +}); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx b/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx index 68436f8..81695f8 100644 --- a/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx +++ b/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx @@ -23,6 +23,7 @@ define([ describe('DesignDoc', function () { var container; + var database = { id: 'db' }; var selectedNavInfo = { navItem: 'all-docs', @@ -40,80 +41,72 @@ define([ }); it('confirm only single sub-option is shown by default (metadata link)', function () { - var stub = function () { return true; }; var el = TestUtils.renderIntoDocument(<DesignDoc - toggle={stub} + database={database} + toggle={function () {}} sidebarListTypes={[]} - contentVisible={true} - isVisible={stub} - designDoc={{}} + isExpanded={true} selectedNavInfo={selectedNavInfo} - designDocName="id" - databaseName="db-name" />, container); + toggledSections={{}} + designDoc={{ customProp: { one: 'something' } }} + />, container); + var subOptions = $(ReactDOM.findDOMNode(el)).find('.accordion-body li'); assert.equal(subOptions.length, 1); }); it('confirm design doc sidebar extensions appear', function () { - var stub = function () { return true; }; var el = TestUtils.renderIntoDocument(<DesignDoc - toggle={stub} - contentVisible={true} - isVisible={stub} + database={database} + toggle={function () {}} sidebarListTypes={[{ selector: 'customProp', name: 'Search Indexes', icon: 'icon-here', urlNamespace: 'whatever' }]} - designDoc={{ - customProp: { - one: 'something' - } - }} + isExpanded={true} selectedNavInfo={selectedNavInfo} - designDocName="id" - databaseName="db-name" />, container); + toggledSections={{}} + designDoc={{ customProp: { one: 'something' } }} + />, container); + var subOptions = $(ReactDOM.findDOMNode(el)).find('.accordion-body li'); assert.equal(subOptions.length, 3); // 1 for "Metadata" row, 1 for Type List row ("search indexes") and one for the index itself }); it('confirm design doc sidebar extensions do not appear when they have no content', function () { - var stub = function () { return true; }; var el = TestUtils.renderIntoDocument(<DesignDoc - toggle={stub} + database={database} + toggle={function () {}} sidebarListTypes={[{ selector: 'customProp', name: 'Search Indexes', icon: 'icon-here', urlNamespace: 'whatever' }]} - contentVisible={true} - isVisible={stub} + isExpanded={true} selectedNavInfo={selectedNavInfo} designDoc={{}} // note that this is empty - designDocName="id" - databaseName="db-name" />, container); + />, container); + var subOptions = $(ReactDOM.findDOMNode(el)).find('.accordion-body li'); assert.equal(subOptions.length, 1); }); it('confirm doc metadata page is highlighted if selected', function () { - var stub = function () { return true; }; var el = TestUtils.renderIntoDocument(<DesignDoc - toggle={stub} + database={database} + toggle={function () {}} sidebarListTypes={[]} - contentVisible={true} - isVisible={stub} + isExpanded={true} selectedNavInfo={{ navItem: 'designDoc', designDocName: 'id', designDocSection: 'metadata', indexName: '' }} - designDoc={{}} - designDocName="id" - databaseName="db-name" />, container); + designDoc={{}} />, container); assert.equal($(ReactDOM.findDOMNode(el)).find('.accordion-body li.active a').html(), 'Metadata'); }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/tests/sidebar.storesSpec.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/sidebar/tests/sidebar.storesSpec.js b/app/addons/documents/sidebar/tests/sidebar.storesSpec.js index 3012820..b2c1f1e 100644 --- a/app/addons/documents/sidebar/tests/sidebar.storesSpec.js +++ b/app/addons/documents/sidebar/tests/sidebar.storesSpec.js @@ -12,7 +12,7 @@ define([ 'api', - 'addons/documents/sidebar/stores', + 'addons/documents/sidebar/stores.react', 'addons/documents/sidebar/actiontypes', 'testUtils' ], function (FauxtonAPI, Stores, ActionTypes, testUtils) { http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/deletesDocuments.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/deletesDocuments.js b/app/addons/documents/tests/nightwatch/deletesDocuments.js index 9ae5edb..1799fed 100644 --- a/app/addons/documents/tests/nightwatch/deletesDocuments.js +++ b/app/addons/documents/tests/nightwatch/deletesDocuments.js @@ -68,7 +68,7 @@ module.exports = { .clickWhenVisible('#header-dropdown-menu a', waitTime, false) .waitForElementPresent('#header-dropdown-menu a[href*="new_view"]', waitTime, false) .clickWhenVisible('#header-dropdown-menu a[href*="new_view"]', waitTime, false) - .waitForElementPresent('.editor-wrapper', waitTime, false) + .waitForElementPresent('.index-cancel-link', waitTime, false) .waitForElementPresent('#new-ddoc', waitTime, false) .setValue('#new-ddoc', 'sidebar-update') .clearValue('#index-name') http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/doubleEmitResults.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/doubleEmitResults.js b/app/addons/documents/tests/nightwatch/doubleEmitResults.js index 2878744..d83ecb2 100644 --- a/app/addons/documents/tests/nightwatch/doubleEmitResults.js +++ b/app/addons/documents/tests/nightwatch/doubleEmitResults.js @@ -22,7 +22,7 @@ module.exports = { .loginToGUI() .populateDatabase(newDatabaseName) .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') - .waitForElementPresent('.editor-wrapper', waitTime, false) + .waitForElementPresent('.clearfix', waitTime, false) .waitForElementPresent('.doc-row', waitTime, false) .execute(function () { return $('.doc-row').length; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/previousButton.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/previousButton.js b/app/addons/documents/tests/nightwatch/previousButton.js index 8fe2476..39ef44c 100644 --- a/app/addons/documents/tests/nightwatch/previousButton.js +++ b/app/addons/documents/tests/nightwatch/previousButton.js @@ -11,26 +11,8 @@ // the License. module.exports = { - 'View: Navigate previous navigates to _all_docs': function (client) { - var waitTime = client.globals.maxWaitTime, - newDatabaseName = client.globals.testDatabaseName, - baseUrl = client.globals.test_settings.launch_url; - - client - .populateDatabase(newDatabaseName, 3) - .loginToGUI() - .url(baseUrl + '/#/database/' + newDatabaseName + '/_changes') - .clickWhenVisible('#nav-header-keyview') - .clickWhenVisible('#nav-design-function-keyviewviews a') - .clickWhenVisible('#keyview_keyview') - .clickWhenVisible('.breadcrumb-back-link .fonticon-left-open') - .assert.urlContains('_all_docs') - .end(); - }, - 'Mango: Navigate back to _all_docs': function (client) { - var waitTime = client.globals.maxWaitTime, - newDatabaseName = client.globals.testDatabaseName, + var newDatabaseName = client.globals.testDatabaseName, baseUrl = client.globals.test_settings.launch_url; client http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewClone.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewClone.js b/app/addons/documents/tests/nightwatch/viewClone.js new file mode 100644 index 0000000..9127c25 --- /dev/null +++ b/app/addons/documents/tests/nightwatch/viewClone.js @@ -0,0 +1,37 @@ +// 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. + +module.exports = { + + 'Clones a view': function (client) { + var waitTime = client.globals.maxWaitTime, + newDatabaseName = client.globals.testDatabaseName, + baseUrl = client.globals.test_settings.launch_url; + + client + .createDatabase(newDatabaseName) + .populateDatabase(newDatabaseName) + .loginToGUI() + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') + .waitForElementPresent('.prettyprint', waitTime, false) + .assert.containsText('.prettyprint', 'stub') + .clickWhenVisible('.index-list .active span', waitTime, true) + .clickWhenVisible('.popover-content .fonticon-files-o', waitTime, true) + .waitForElementVisible('#new-index-name', waitTime, true) + .setValue('#new-index-name', 'cloned-view') + .clickWhenVisible('.clone-index-modal .btn-success', waitTime, true) + + // now wait for the sidebar to be updated with the new view + .waitForElementVisible('#testdesigndoc_cloned-view', waitTime, true) + .end(); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewCreate.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewCreate.js b/app/addons/documents/tests/nightwatch/viewCreate.js index 26c549b..4eacb91 100644 --- a/app/addons/documents/tests/nightwatch/viewCreate.js +++ b/app/addons/documents/tests/nightwatch/viewCreate.js @@ -56,9 +56,9 @@ module.exports = { .checkForDocumentCreated('_design/test_design_doc-selenium-3') .waitForElementPresent('.prettyprint', waitTime, false) .waitForElementNotPresent('.loading-lines', waitTime, false) + + // page now automatically redirects user to results of View. Confirm the new doc is present. .assert.containsText('.prettyprint', 'hasehase') - .back() - .waitForElementPresent('.watermark-logo', waitTime, false) .end(); }, @@ -110,16 +110,6 @@ module.exports = { .execute('$("#save-view")[0].scrollIntoView();') .clickWhenVisible('#save-view') .checkForDocumentCreated('_design/testdesigndoc/_view/test-new-view') - - .waitForElementPresent('.prettyprint', waitTime, false) - .waitForElementNotPresent('.loading-lines', waitTime, false) - //go back to all docs - .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') - .clickWhenVisible('#nav-header-testdesigndoc', waitTime, false) - .clickWhenVisible('#nav-design-function-testdesigndocviews a', waitTime, false) - .execute('$("#testdesigndoc_test-new-view")[0].scrollIntoView();') - .clickWhenVisible('#testdesigndoc_test-new-view', waitTime, false) - .execute('$(".save")[0].scrollIntoView();') .waitForElementPresent('.prettyprint', waitTime, false) .waitForElementNotPresent('.loading-lines', waitTime, false) .assert.containsText('.prettyprint', 'enteente') @@ -131,7 +121,6 @@ function openDifferentDropdownsAndClick (client, dropDownElement) { var modifier = dropDownElement.slice(1); var waitTime = client.globals.maxWaitTime; var newDatabaseName = client.globals.testDatabaseName; - var newDocumentName = 'create_view_doc' + modifier; var baseUrl = client.globals.test_settings.launch_url; return client @@ -143,5 +132,5 @@ function openDifferentDropdownsAndClick (client, dropDownElement) { .clickWhenVisible(dropDownElement + ' a', waitTime, false) .waitForElementPresent(dropDownElement + ' a[href*="new_view"]', waitTime, false) .clickWhenVisible(dropDownElement + ' a[href*="new_view"]', waitTime, false) - .waitForElementPresent('.editor-wrapper', waitTime, false); + .waitForElementPresent('.index-cancel-link', waitTime, false); } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewDelete.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewDelete.js b/app/addons/documents/tests/nightwatch/viewDelete.js new file mode 100644 index 0000000..0911066 --- /dev/null +++ b/app/addons/documents/tests/nightwatch/viewDelete.js @@ -0,0 +1,40 @@ +// 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. + +module.exports = { + + 'Deletes a view': function (client) { + var waitTime = client.globals.maxWaitTime, + newDatabaseName = client.globals.testDatabaseName, + baseUrl = client.globals.test_settings.launch_url; + + client + .createDatabase(newDatabaseName) + .populateDatabase(newDatabaseName) + .loginToGUI() + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') + .waitForElementPresent('.prettyprint', waitTime, false) + .assert.containsText('.prettyprint', 'stub') + + // confirm the sidebar shows the testdesigndoc design doc + .waitForElementVisible('#testdesigndoc', waitTime, true) + + .clickWhenVisible('.index-list .active span', waitTime, true) + .clickWhenVisible('.popover-content .fonticon-trash', waitTime, true) + .waitForElementVisible('.confirmation-modal .js-btn-success', waitTime, true) + .clickWhenVisible('.confirmation-modal .js-btn-success', waitTime, true) + + // now wait for the sidebar to have removed the design doc + .waitForElementNotPresent('#testdesigndoc', waitTime, true) + .end(); + } +}; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewEdit.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewEdit.js b/app/addons/documents/tests/nightwatch/viewEdit.js index ef9526f..20c0f5f 100644 --- a/app/addons/documents/tests/nightwatch/viewEdit.js +++ b/app/addons/documents/tests/nightwatch/viewEdit.js @@ -12,32 +12,38 @@ module.exports = { - 'Edits a design doc - set new index name': function (client) { - /*jshint multistr: true */ + 'Edits a design doc - renames index': function (client) { var waitTime = client.globals.maxWaitTime, newDatabaseName = client.globals.testDatabaseName, baseUrl = client.globals.test_settings.launch_url; - var viewUrl = newDatabaseName + '/_design/testdesigndoc/_view/hasenindex5000?limit=6&reduce=false'; client + .deleteDatabase(newDatabaseName) .createDatabase(newDatabaseName) .populateDatabase(newDatabaseName) .loginToGUI() - .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') - .waitForElementPresent('.prettyprint', waitTime, false) - .assert.containsText('.prettyprint', 'stub') + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview/edit') + .waitForElementPresent('.index-cancel-link', waitTime, true) + .waitForElementNotPresent('.spinner', waitTime, true) + .waitForElementNotPresent('.loading-lines', waitTime, true) + .waitForElementVisible('#index-name', waitTime, true) + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) + + .waitForAttribute('#index-name', 'value', function (val) { + return val === 'stubview'; + }) .clearValue('#index-name') .setValue('#index-name', 'hasenindex5000') - .execute('\ - var editor = ace.edit("map-function");\ - editor.getSession().setValue("function (doc) { emit(\'hasehase5000\', 1); }");\ - ') + .execute('$("#save-view")[0].scrollIntoView();') .clickWhenVisible('#save-view') - .checkForStringPresent(viewUrl, 'hasehase5000') - .waitForElementNotPresent('.loading-lines', waitTime, false) - .waitForElementVisible('.prettyprint', waitTime, false) - .assert.containsText('.prettyprint', 'hasehase5000') + + // confirm the new index name is present + .waitForElementVisible('#testdesigndoc_hasenindex5000', waitTime, false) .end(); }, @@ -50,12 +56,25 @@ module.exports = { var viewUrl = newDatabaseName + '/_design/testdesigndoc/_view/stubview?limit=6&reduce=false'; client + .deleteDatabase(newDatabaseName) .createDatabase(newDatabaseName) .populateDatabase(newDatabaseName) .loginToGUI() - .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') - .waitForElementPresent('.prettyprint', waitTime, false) - .assert.containsText('.prettyprint', 'stub') + + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview/edit') + .waitForElementPresent('.index-cancel-link', waitTime, true) + .waitForElementNotPresent('.spinner', waitTime, true) + .waitForElementNotPresent('.loading-lines', waitTime, true) + .waitForElementVisible('#index-name', waitTime, true) + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) + + .waitForAttribute('#index-name', 'value', function (val) { + return val === 'stubview'; + }) .execute('\ var editor = ace.edit("map-function");\ @@ -63,12 +82,18 @@ module.exports = { editor._emit(\'blur\');\ ') .execute('$("#save-view")[0].scrollIntoView();') - .clickWhenVisible('#save-view') .checkForStringPresent(viewUrl, 'hasehase6000') + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') .waitForElementNotPresent('.loading-lines', waitTime, false) + .waitForElementNotPresent('.spinner', waitTime, false) .waitForElementVisible('.prettyprint', waitTime, false) + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) .waitForAttribute('#doc-list', 'textContent', function (docContents) { return (/hasehase6000/).test(docContents); }) @@ -83,6 +108,7 @@ module.exports = { dropDownElement = '#header-dropdown-menu'; client + .deleteDatabase(newDatabaseName) .createDatabase(newDatabaseName) .populateDatabase(newDatabaseName) .loginToGUI() @@ -92,6 +118,14 @@ module.exports = { .waitForElementPresent(dropDownElement, waitTime, false) .clickWhenVisible(dropDownElement + ' a') .clickWhenVisible(dropDownElement + ' a[href*="new_view"]') + .waitForElementNotPresent('.spinner', waitTime, true) + .waitForElementNotPresent('.loading-lines', waitTime, true) + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) + .waitForElementVisible('#new-ddoc', waitTime, false) .setValue('#new-ddoc', 'view1-name') .clearValue('#index-name') @@ -105,7 +139,6 @@ module.exports = { .execute('$("#save-view")[0].scrollIntoView();') .clickWhenVisible('#save-view') .checkForDocumentCreated('_design/view1-name') - .waitForElementPresent('.btn.btn-danger.delete', waitTime, false) // create the second view .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') @@ -113,6 +146,14 @@ module.exports = { .clickWhenVisible(dropDownElement + ' a') .clickWhenVisible(dropDownElement + ' a[href*="new_view"]') .waitForElementVisible('#new-ddoc', waitTime, false) + .waitForElementNotPresent('.spinner', waitTime, true) + .waitForElementNotPresent('.loading-lines', waitTime, true) + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) + .setValue('#new-ddoc', 'view2-name') .clearValue('#index-name') .setValue('#index-name', 'view2') @@ -125,15 +166,19 @@ module.exports = { .execute('$("#save-view")[0].scrollIntoView();') .clickWhenVisible('#save-view') .checkForDocumentCreated('_design/view2-name') - .waitForElementPresent('.btn.btn-danger.delete', waitTime, false) - - // go back to the all docs page to ensure a page reload when we return to the Edit View page - .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') - .waitForElementPresent(dropDownElement, waitTime, false) // now redirect back to first view and confirm the fields are all populated properly - .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/view1-name/_view/view1') + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/view1-name/_view/view1/edit') + + .waitForElementNotPresent('.spinner', waitTime, true) + .waitForElementNotPresent('.loading-lines', waitTime, true) .waitForElementVisible('#save-view', waitTime, false) + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) + .execute(function () { var editor = window.ace.edit("map-function"); return editor.getSession().getValue(); @@ -143,34 +188,56 @@ module.exports = { .end(); }, - 'Query Options are kept after a new reduce method is chosen': function (client) { - /*jshint multistr: true */ + 'Editing a view and putting it into a new design doc removes it from the old design doc': function (client) { var waitTime = client.globals.maxWaitTime, - newDatabaseName = client.globals.testDatabaseName, - baseUrl = client.globals.test_settings.launch_url; - - var viewUrl = newDatabaseName + '/_design/testdesigndoc/_view/stubview?reduce=true&group_level=0'; + newDatabaseName = client.globals.testDatabaseName, + baseUrl = client.globals.test_settings.launch_url; client + .deleteDatabase(newDatabaseName) .createDatabase(newDatabaseName) .populateDatabase(newDatabaseName) .loginToGUI() - .url(baseUrl + '/#/database/' + viewUrl) + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview') .waitForElementPresent('.prettyprint', waitTime, false) - .assert.containsText('.prettyprint', '20') - .clickWhenVisible('#reduce-function-selector option[value="_sum"]') - .execute('\ - var editor = ace.edit("map-function");\ - editor.getSession().setValue("function (doc) { emit(\'newstub\', 2); }");\ - ') - .execute('$("#save-view")[0].scrollIntoView();') - .clickWhenVisible('#save-view', waitTime, false) - .checkForStringPresent(viewUrl, '40') - .waitForElementNotPresent('.loading-lines', waitTime, false) - .waitForElementVisible('.prettyprint', waitTime, false) - .waitForAttribute('.prettyprint', 'textContent', function (docContents) { - return (/40/).test(docContents); + + // confirm the sidebar shows the testdesigndoc design doc + .waitForElementVisible('#testdesigndoc', waitTime, true) + + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); }) - .end(); + + // now edit the view and move it into a brand new design doc + .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview/edit') + .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false) + .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) { + var regExp = new RegExp(newDatabaseName); + return regExp.test(docContents); + }) + + .waitForElementPresent('.index-cancel-link', waitTime, true) + .waitForElementVisible('select#ddoc', waitTime, true) + .waitForElementNotPresent('.spinner', waitTime, true) + .waitForElementNotPresent('.loading-lines', waitTime, true) + + .setValue('select#ddoc', 'new-doc') + + // needed to get React to update + show the new design doc field + .click('body') + + .waitForElementPresent('#new-ddoc', waitTime, true) + .execute('$("#new-ddoc")[0].scrollIntoView();') + .setValue('#new-ddoc', 'brand-new-ddoc') + .execute('$("#save-view")[0].scrollIntoView();') + .clickWhenVisible('#save-view') + + // now wait for the old design doc to be gone, and the new one to have shown up + .waitForElementNotPresent('#testdesigndoc', waitTime, true) + .waitForElementPresent('#brand-new-ddoc', waitTime, true) + .end(); } + }; http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewQueryOptions.js ---------------------------------------------------------------------- diff --git a/app/addons/documents/tests/nightwatch/viewQueryOptions.js b/app/addons/documents/tests/nightwatch/viewQueryOptions.js index 424dd18..2ec943c 100644 --- a/app/addons/documents/tests/nightwatch/viewQueryOptions.js +++ b/app/addons/documents/tests/nightwatch/viewQueryOptions.js @@ -25,10 +25,10 @@ module.exports = { .clickWhenVisible('#byKeys', waitTime, false) .setValue('#keys-input', '["document_1"]') .clickWhenVisible('#query-options .btn-success') - .waitForElementNotPresent('#right-content [data-id="document_2"]', waitTime, false) - .assert.elementNotPresent('#right-content [data-id="document_2"]') - .assert.elementNotPresent('#right-content [data-id="document_0"]') - .assert.elementPresent('#right-content [data-id="document_1"]') + .waitForElementNotPresent('#doc-list [data-id="document_2"]', waitTime, false) + .assert.elementNotPresent('#doc-list [data-id="document_2"]') + .assert.elementNotPresent('#doc-list [data-id="document_0"]') + .assert.elementPresent('#doc-list [data-id="document_1"]') .end(); }, @@ -46,9 +46,9 @@ module.exports = { .clickWhenVisible('#byKeys', waitTime, false) .setValue('#keys-input', '["document_1",\n"document_2"]') .clickWhenVisible('#query-options .btn-success') - .waitForElementNotPresent('#right-content [data-id="document_0"]', waitTime, false) - .assert.elementNotPresent('#right-content [data-id="document_0"]') - .assert.elementPresent('#right-content [data-id="document_1"]') + .waitForElementNotPresent('#doc-list [data-id="document_0"]', waitTime, false) + .assert.elementNotPresent('#doc-list [data-id="document_0"]') + .assert.elementPresent('#doc-list [data-id="document_1"]') .end(); } http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/fauxton/components.react.jsx ---------------------------------------------------------------------- diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx index 3d54e58..d18bdb1 100644 --- a/app/addons/fauxton/components.react.jsx +++ b/app/addons/fauxton/components.react.jsx @@ -353,7 +353,10 @@ function (app, FauxtonAPI, React, ReactDOM, ZeroClipboard, ReactBootstrap) { var ConfirmationModal = React.createClass({ propTypes: { visible: React.PropTypes.bool.isRequired, - text: React.PropTypes.string.isRequired, + text: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]).isRequired, onClose: React.PropTypes.func.isRequired, onSubmit: React.PropTypes.func.isRequired }, @@ -377,15 +380,17 @@ function (app, FauxtonAPI, React, ReactDOM, ZeroClipboard, ReactBootstrap) { }, render: function () { + var content = <p>{this.props.text}</p>; + if (!_.isString(this.props.text)) { + content = this.props.text; + } return ( <Modal dialogClassName="confirmation-modal" show={this.props.visible} onHide={this.close}> <Modal.Header closeButton={true}> <Modal.Title>{this.props.title}</Modal.Title> </Modal.Header> <Modal.Body> - <p> - {this.props.text} - </p> + {content} </Modal.Body> <Modal.Footer> <button className="btn btn-success js-btn-success" onClick={this.props.onSubmit}> http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/permissions/routes.js ---------------------------------------------------------------------- diff --git a/app/addons/permissions/routes.js b/app/addons/permissions/routes.js index a92666b..6f578c2 100644 --- a/app/addons/permissions/routes.js +++ b/app/addons/permissions/routes.js @@ -85,15 +85,10 @@ function (app, FauxtonAPI, Databases, Resources, Actions, Permissions, BaseRoute }, cleanup: function () { - if (this.pageContent) { - this.removeView('#dashboard-content'); - } if (this.leftheader) { this.removeView('#breadcrumbs'); } - if (this.sidebar) { - this.removeView('#sidebar'); - } + this.removeComponent('#sidebar-content'); this.stopListening(FauxtonAPI.Events, 'lookaheadTray:update', this.onSelectDatabase); } }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/core/api.js ---------------------------------------------------------------------- diff --git a/app/core/api.js b/app/core/api.js index d925aa0..2f806bd 100644 --- a/app/core/api.js +++ b/app/core/api.js @@ -98,6 +98,14 @@ function (FauxtonAPI, Layout, Router, RouteObject, utils, Store, constants, Flux return url; }; + // out-the-box Fauxton has only Views, but scripts extending Fauxton may introduce others (search indexes, geospatial + // indexes, etc). This returns an array of the special design doc property names for the index types + FauxtonAPI.getIndexTypePropNames = function () { + var indexTypes = FauxtonAPI.getExtensions('IndexTypes:propNames'); + indexTypes.push('views'); + return indexTypes; + }; + return FauxtonAPI; }); http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/assets/less/fauxton.less ---------------------------------------------------------------------- diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less index 0699495..349c9ca 100644 --- a/assets/less/fauxton.less +++ b/assets/less/fauxton.less @@ -584,6 +584,14 @@ footer.pagination-footer { line-height: 30px; } +.simple-header { + font-weight: 400; + font-size: 15pt; + border-bottom: 1px solid #cccccc; + margin-bottom: 30px; + margin-top: 0; +} + // left navigationbar is opened @media (max-width: 730px) { .closeMenu {