This is an automated email from the ASF dual-hosted git repository. amaranhao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git
The following commit(s) were added to refs/heads/master by this push: new a3f62b4 Sidebar redux refactoring (#1126) a3f62b4 is described below commit a3f62b4f38bd2444d4de31249f4aff5ffe6ed62e Author: Antonio Maranhao <30349380+antonio-maran...@users.noreply.github.com> AuthorDate: Tue Oct 2 19:04:42 2018 -0400 Sidebar redux refactoring (#1126) * Split components into separate files * Refactoring to use redux --- .../documents/__tests__/fetch-actions.test.js | 4 +- .../documents/__tests__/results-toolbar.test.js | 3 +- app/addons/documents/components/results-toolbar.js | 10 +- app/addons/documents/helpers.js | 11 +- .../index-editor/__tests__/actions.test.js | 1 + app/addons/documents/index-editor/actions.js | 11 +- .../documents/index-results/actions/fetch.js | 2 +- .../containers/QueryOptionsContainer.js | 2 +- app/addons/documents/layouts.js | 15 +- app/addons/documents/routes-documents.js | 14 +- app/addons/documents/routes-index-editor.js | 26 +- app/addons/documents/routes-mango.js | 3 - app/addons/documents/shared-routes.js | 2 +- .../sidebar/SidebarControllerContainer.js | 94 ++- .../sidebar/__tests__/sidebar.actions.test.js | 11 +- .../sidebar/__tests__/sidebar.components.test.js | 18 +- .../sidebar/__tests__/sidebar.reducers.test.js | 77 +++ .../sidebar/__tests__/sidebar.stores.test.js | 74 --- app/addons/documents/sidebar/actions.js | 141 ++--- app/addons/documents/sidebar/actiontypes.js | 2 +- .../sidebar/components/CloneIndexModal.js | 114 ++++ .../documents/sidebar/components/DesignDoc.js | 158 +++++ .../documents/sidebar/components/DesignDocList.js | 82 +++ .../documents/sidebar/components/IndexSection.js | 151 +++++ .../documents/sidebar/components/MainSidebar.js | 98 ++++ .../sidebar/components/SidebarController.js | 146 +++++ app/addons/documents/sidebar/helpers.js | 40 ++ app/addons/documents/sidebar/reducers.js | 216 ++++++- app/addons/documents/sidebar/sidebar.js | 639 +-------------------- app/addons/documents/sidebar/stores.js | 337 ----------- app/addons/permissions/layout.js | 6 +- app/main.js | 6 + 32 files changed, 1324 insertions(+), 1190 deletions(-) diff --git a/app/addons/documents/__tests__/fetch-actions.test.js b/app/addons/documents/__tests__/fetch-actions.test.js index ba85e5f..ec7af92 100644 --- a/app/addons/documents/__tests__/fetch-actions.test.js +++ b/app/addons/documents/__tests__/fetch-actions.test.js @@ -296,7 +296,7 @@ describe('Docs Fetch API', () => { beforeEach(() => { notificationSpy = sinon.spy(FauxtonAPI, 'addNotification'); - sidebarSpy = sinon.stub(SidebarActions, 'updateDesignDocs'); + sidebarSpy = sinon.stub(SidebarActions, 'dispatchUpdateDesignDocs'); }); afterEach(() => { @@ -325,7 +325,7 @@ describe('Docs Fetch API', () => { expect(sidebarSpy.calledOnce).toBe(false); }); - it('calls updateDesignDocs when one of the deleted docs is a ddoc', () => { + it('calls dispatchUpdateDesignDocs when one of the deleted docs is a ddoc', () => { const res = [ { id: '_design/foo', diff --git a/app/addons/documents/__tests__/results-toolbar.test.js b/app/addons/documents/__tests__/results-toolbar.test.js index 255b87a..c830b2c 100644 --- a/app/addons/documents/__tests__/results-toolbar.test.js +++ b/app/addons/documents/__tests__/results-toolbar.test.js @@ -25,7 +25,8 @@ describe('Results Toolbar', () => { hasSelectedItem: false, toggleSelectAll: () => {}, isLoading: false, - queryOptionsParams: {} + queryOptionsParams: {}, + databaseName: 'mydb' }; beforeEach(() => { diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js index e9f0b13..abaa037 100644 --- a/app/addons/documents/components/results-toolbar.js +++ b/app/addons/documents/components/results-toolbar.js @@ -13,12 +13,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import BulkDocumentHeaderController from "../header/header"; -import Stores from "../sidebar/stores"; import Components from "../../components/react-components"; import Helpers from '../helpers'; const {BulkActionComponent} = Components; -const store = Stores.sidebarStore; export class ResultsToolBar extends React.Component { shouldComponentUpdate (nextProps) { @@ -26,7 +24,6 @@ export class ResultsToolBar extends React.Component { } render () { - const database = store.getDatabase(); const { hasResults, isListDeletable, @@ -34,7 +31,8 @@ export class ResultsToolBar extends React.Component { allDocumentsSelected, hasSelectedItem, toggleSelectAll, - isLoading + isLoading, + databaseName } = this.props; // Determine if we need to display the bulk action selector @@ -56,10 +54,10 @@ export class ResultsToolBar extends React.Component { } let createDocumentLink = null; - if (database) { + if (databaseName) { createDocumentLink = ( <div className="document-result-screen__toolbar-flex-container"> - <a href={Helpers.getNewDocUrl(database.id)} className="btn save document-result-screen__toolbar-create-btn btn-primary"> + <a href={Helpers.getNewDocUrl(databaseName)} className="btn save document-result-screen__toolbar-create-btn btn-primary"> Create Document </a> </div> diff --git a/app/addons/documents/helpers.js b/app/addons/documents/helpers.js index 3c2a2ac..a59d3a9 100644 --- a/app/addons/documents/helpers.js +++ b/app/addons/documents/helpers.js @@ -102,20 +102,19 @@ const selectedViewContainsReduceFunction = (designDocs, selectedNavItem) => { let showReduce = false; // If a map/reduce view is selected, check if view contains reduce field if (designDocs && isViewSelected(selectedNavItem)) { - const ddocID = '_design/' + selectedNavItem.params.designDocName; + const ddocID = '_design/' + selectedNavItem.designDocName; const ddoc = designDocs.find(ddoc => ddoc._id === ddocID); showReduce = ddoc !== undefined && ddoc.views - && ddoc.views[selectedNavItem.params.indexName] !== undefined - && ddoc.views[selectedNavItem.params.indexName].reduce !== undefined; + && ddoc.views[selectedNavItem.indexName] !== undefined + && ddoc.views[selectedNavItem.indexName].reduce !== undefined; } return showReduce; }; const isViewSelected = (selectedNavItem) => { return (selectedNavItem.navItem === 'designDoc' - && selectedNavItem.params - && selectedNavItem.params.designDocSection === 'Views' - && selectedNavItem.params.indexName); + && selectedNavItem.designDocSection === 'Views' + && selectedNavItem.indexName); }; export default { diff --git a/app/addons/documents/index-editor/__tests__/actions.test.js b/app/addons/documents/index-editor/__tests__/actions.test.js index e55de6d..cba84f5 100644 --- a/app/addons/documents/index-editor/__tests__/actions.test.js +++ b/app/addons/documents/index-editor/__tests__/actions.test.js @@ -27,6 +27,7 @@ describe('Index Editor Actions', function () { describe('delete view', function () { var designDocs, database, designDoc, designDocCollection, designDocId, viewName; beforeEach(function () { + FauxtonAPI.reduxDispatch = sinon.stub(); database = { safeID: function () { return 'safeid';} }; diff --git a/app/addons/documents/index-editor/actions.js b/app/addons/documents/index-editor/actions.js index 350ea6c..22b9a9b 100644 --- a/app/addons/documents/index-editor/actions.js +++ b/app/addons/documents/index-editor/actions.js @@ -15,7 +15,6 @@ import FauxtonAPI from "../../../core/api"; import Documents from "../resources"; import ActionTypes from "./actiontypes"; import SidebarActions from "../sidebar/actions"; -import SidebarActionTypes from "../sidebar/actiontypes"; function selectReduceChanged (reduceOption) { FauxtonAPI.dispatch({ @@ -94,7 +93,7 @@ function saveView (viewInfo) { var oldDesignDoc = findDesignDoc(viewInfo.designDocs, viewInfo.originalDesignDocName); safeDeleteIndex(oldDesignDoc, viewInfo.designDocs, 'views', viewInfo.originalViewName, { onSuccess: function () { - SidebarActions.updateDesignDocs(viewInfo.designDocs); + SidebarActions.dispatchUpdateDesignDocs(viewInfo.designDocs); } }); } @@ -102,7 +101,7 @@ function saveView (viewInfo) { if (viewInfo.designDocId === 'new-doc') { addDesignDoc(designDoc); } - + SidebarActions.dispatchUpdateDesignDocs(viewInfo.designDocs); FauxtonAPI.dispatch({ type: ActionTypes.VIEW_SAVED }); var fragment = FauxtonAPI.urls('view', 'showView', viewInfo.database.safeID(), designDoc.safeID(), app.utils.safeURLName(viewInfo.viewName)); FauxtonAPI.navigate(fragment, { trigger: true }); @@ -132,7 +131,7 @@ function deleteView (options) { FauxtonAPI.navigate(url); } - SidebarActions.updateDesignDocs(options.designDocs); + SidebarActions.dispatchUpdateDesignDocs(options.designDocs); FauxtonAPI.addNotification({ msg: 'The <code>' + _.escape(options.indexName) + '</code> view has been deleted.', @@ -140,7 +139,7 @@ function deleteView (options) { escape: false, clear: true }); - FauxtonAPI.dispatch({ type: SidebarActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL }); + SidebarActions.dispatchHideDeleteIndexModal(); } return safeDeleteIndex(options.designDoc, options.designDocs, 'views', options.indexName, { onSuccess: onSuccess }); @@ -174,7 +173,7 @@ function cloneView (params) { type: 'success', clear: true }); - SidebarActions.updateDesignDocs(params.designDocs); + SidebarActions.dispatchUpdateDesignDocs(params.designDocs); }, function (xhr) { params.onComplete(); diff --git a/app/addons/documents/index-results/actions/fetch.js b/app/addons/documents/index-results/actions/fetch.js index 2521a36..fc8df4e 100644 --- a/app/addons/documents/index-results/actions/fetch.js +++ b/app/addons/documents/index-results/actions/fetch.js @@ -197,6 +197,6 @@ export const processBulkDeleteResponse = (res, deletedDocs, designDocs, docType) } if (designDocs && hasDesignDocs) { - SidebarActions.updateDesignDocs(designDocs); + SidebarActions.dispatchUpdateDesignDocs(designDocs); } }; diff --git a/app/addons/documents/index-results/containers/QueryOptionsContainer.js b/app/addons/documents/index-results/containers/QueryOptionsContainer.js index 0052577..344e9e4 100644 --- a/app/addons/documents/index-results/containers/QueryOptionsContainer.js +++ b/app/addons/documents/index-results/containers/QueryOptionsContainer.js @@ -51,7 +51,7 @@ const mapStateToProps = ({indexResults, sidebar}, ownProps) => { return { contentVisible: queryOptionsPanel.isVisible, includeDocs: queryOptionsPanel.includeDocs, - showReduce: showReduce(sidebar.designDocs, ownProps.selectedNavItem), + showReduce: showReduce(sidebar.designDocList, ownProps.selectedNavItem), reduce: queryOptionsPanel.reduce, groupLevel: queryOptionsPanel.groupLevel, showByKeys: queryOptionsPanel.showByKeys, diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 85b7611..8a0c05c 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -94,12 +94,13 @@ export const TabsSidebarContent = ({ upperContent, fetchUrl, databaseName, - queryDocs + queryDocs, + selectedNavItem }) => { return ( <div className="with-sidebar tabs-with-sidebar content-area"> <aside id="sidebar-content" className="scrollable"> - <SidebarControllerContainer /> + <SidebarControllerContainer selectedNavItem={selectedNavItem}/> </aside> <section id="dashboard-content" className="flex-layout flex-col"> <div id="dashboard-upper-content"> @@ -126,6 +127,7 @@ TabsSidebarContent.propTypes = { hideFooter: PropTypes.bool, lowerContent: PropTypes.object, upperContent: PropTypes.object, + selectedNavItem: PropTypes.object }; export const DocsTabsSidebarLayout = ({ @@ -173,12 +175,13 @@ export const DocsTabsSidebarLayout = ({ fetchUrl={fetchUrl} databaseName={dbName} queryDocs={queryDocs} + selectedNavItem={selectedNavItem} /> </div> ); }; -export const ChangesSidebarLayout = ({ docURL, database, endpoint, dbName, dropDownLinks }) => { +export const ChangesSidebarLayout = ({ docURL, database, endpoint, dbName, dropDownLinks, selectedNavItem }) => { return ( <div id="dashboard" className="with-sidebar"> <TabsSidebarHeader @@ -193,12 +196,15 @@ export const ChangesSidebarLayout = ({ docURL, database, endpoint, dbName, dropD upperContent={<Changes.ChangesTabContent />} lowerContent={<Changes.ChangesController />} hideFooter={true} + selectedNavItem={selectedNavItem} /> </div> ); }; -export const ViewsTabsSidebarLayout = ({ showEditView, database, docURL, endpoint, dbName, dropDownLinks }) => { +export const ViewsTabsSidebarLayout = ({showEditView, database, docURL, endpoint, + dbName, dropDownLinks, selectedNavItem }) => { + const content = showEditView ? <IndexEditorComponents.EditorController /> : <DesignDocInfoComponents.DesignDocInfo />; return ( <div id="dashboard" className="with-sidebar"> @@ -215,6 +221,7 @@ export const ViewsTabsSidebarLayout = ({ showEditView, database, docURL, endpoin <TabsSidebarContent lowerContent={content} hideFooter={true} + selectedNavItem={selectedNavItem} /> </div> ); diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index ceb33a3..453003a 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -16,7 +16,7 @@ import BaseRoute from './shared-routes'; import ChangesActions from './changes/actions'; import Databases from '../databases/base'; import Resources from './resources'; -import SidebarActions from './sidebar/actions'; +import {SidebarItemSelection} from './sidebar/helpers'; import DesignDocInfoActions from './designdocinfo/actions'; import ComponentsActions from '../components/actions'; import {DocsTabsSidebarLayout, ViewsTabsSidebarLayout, ChangesSidebarLayout} from './layouts'; @@ -53,8 +53,7 @@ var DocumentsRouteObject = BaseRoute.extend({ ddocName: ddoc, designDocInfo: designDocInfo }); - - SidebarActions.selectNavItem('designDoc', { + const selectedNavItem = new SidebarItemSelection('designDoc', { designDocName: ddoc, designDocSection: 'metadata' }); @@ -67,6 +66,7 @@ var DocumentsRouteObject = BaseRoute.extend({ dbName={this.database.id} dropDownLinks={dropDownLinks} database={this.database} + selectedNavItem={selectedNavItem} />; }, @@ -90,10 +90,7 @@ var DocumentsRouteObject = BaseRoute.extend({ tab = 'design-docs'; } - const selectedNavItem = { - navItem: tab - }; - SidebarActions.selectNavItem(selectedNavItem.navItem); + const selectedNavItem = new SidebarItemSelection(tab); ComponentsActions.showDeleteDatabaseModal({showDeleteModal: false, dbId: ''}); const endpoint = this.database.allDocs.urlRef("apiurl", {}); @@ -117,7 +114,7 @@ var DocumentsRouteObject = BaseRoute.extend({ ChangesActions.initChanges({ databaseName: this.database.id }); - SidebarActions.selectNavItem('changes'); + const selectedNavItem = new SidebarItemSelection('changes'); return <ChangesSidebarLayout endpoint={FauxtonAPI.urls('changes', 'apiurl', this.database.id, '')} @@ -125,6 +122,7 @@ var DocumentsRouteObject = BaseRoute.extend({ dbName={this.database.id} dropDownLinks={this.getCrumbs(this.database)} database={this.database} + selectedNavItem={selectedNavItem} />; } diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js index ad19c8c..935ff47 100644 --- a/app/addons/documents/routes-index-editor.js +++ b/app/addons/documents/routes-index-editor.js @@ -15,7 +15,8 @@ import FauxtonAPI from "../../core/api"; import BaseRoute from "./shared-routes"; import ActionsIndexEditor from "./index-editor/actions"; import Databases from "../databases/base"; -import SidebarActions from "./sidebar/actions"; +import SidebarActions from './sidebar/actions'; +import {SidebarItemSelection} from './sidebar/helpers'; import {DocsTabsSidebarLayout, ViewsTabsSidebarLayout} from './layouts'; const IndexEditorAndResults = BaseRoute.extend({ @@ -59,15 +60,12 @@ const IndexEditorAndResults = BaseRoute.extend({ designDocId: '_design/' + ddoc }); - const selectedNavItem = { - navItem: 'designDoc', - params: { - designDocName: ddoc, - designDocSection: 'Views', - indexName: viewName - } - }; - SidebarActions.selectNavItem(selectedNavItem.navItem, selectedNavItem.params); + const selectedNavItem = new SidebarItemSelection('designDoc', { + designDocName: ddoc, + designDocSection: 'Views', + indexName: viewName + }); + SidebarActions.dispatchExpandSelectedItem(selectedNavItem); const url = FauxtonAPI.urls('view', 'server', encodeURIComponent(databaseName), encodeURIComponent(ddoc), encodeURIComponent(viewName)); @@ -107,8 +105,7 @@ const IndexEditorAndResults = BaseRoute.extend({ newDesignDoc: newDesignDoc }); - SidebarActions.selectNavItem(''); - + const selectedNavItem = new SidebarItemSelection(''); const dropDownLinks = this.getCrumbs(this.database); return <ViewsTabsSidebarLayout @@ -117,6 +114,7 @@ const IndexEditorAndResults = BaseRoute.extend({ dbName={this.database.id} dropDownLinks={dropDownLinks} database={this.database} + selectedNavItem={selectedNavItem} />; }, @@ -129,11 +127,12 @@ const IndexEditorAndResults = BaseRoute.extend({ designDocId: '_design/' + ddocName }); - SidebarActions.selectNavItem('designDoc', { + const selectedNavItem = new SidebarItemSelection('designDoc', { designDocName: ddocName, designDocSection: 'Views', indexName: viewName }); + SidebarActions.dispatchExpandSelectedItem(selectedNavItem); const docURL = FauxtonAPI.constants.DOC_URLS.GENERAL; const endpoint = FauxtonAPI.urls('view', 'apiurl', databaseName, ddocName, viewName); @@ -146,6 +145,7 @@ const IndexEditorAndResults = BaseRoute.extend({ dbName={this.database.id} dropDownLinks={dropDownLinks} database={this.database} + selectedNavItem={selectedNavItem} />; } diff --git a/app/addons/documents/routes-mango.js b/app/addons/documents/routes-mango.js index 26120bf..c59b6f5 100644 --- a/app/addons/documents/routes-mango.js +++ b/app/addons/documents/routes-mango.js @@ -15,7 +15,6 @@ import app from "../../app"; import FauxtonAPI from "../../core/api"; import Databases from "../databases/resources"; import Documents from "./shared-resources"; -import SidebarActions from "./sidebar/actions"; import {MangoLayoutContainer} from './mangolayout'; const MangoIndexEditorAndQueryEditor = FauxtonAPI.RouteObject.extend({ @@ -40,8 +39,6 @@ const MangoIndexEditorAndQueryEditor = FauxtonAPI.RouteObject.extend({ }, findUsingIndex: function (database) { - SidebarActions.selectNavItem('mango-query'); - const url = FauxtonAPI.urls( 'allDocs', 'app', encodeURIComponent(this.databaseName), '?limit=' + FauxtonAPI.constants.DATABASES.DOCUMENT_LIMIT ); diff --git a/app/addons/documents/shared-routes.js b/app/addons/documents/shared-routes.js index ef0d2bd..0016955 100644 --- a/app/addons/documents/shared-routes.js +++ b/app/addons/documents/shared-routes.js @@ -44,7 +44,7 @@ var BaseRoute = FauxtonAPI.RouteObject.extend({ options.selectedNavItem = selectedNavItem; } - SidebarActions.newOptions(options); + SidebarActions.dispatchNewOptions(options); }, getCrumbs: function (database) { diff --git a/app/addons/documents/sidebar/SidebarControllerContainer.js b/app/addons/documents/sidebar/SidebarControllerContainer.js index 4da0628..3a3ea26 100644 --- a/app/addons/documents/sidebar/SidebarControllerContainer.js +++ b/app/addons/documents/sidebar/SidebarControllerContainer.js @@ -12,25 +12,97 @@ import { connect } from 'react-redux'; import SidebarComponents from './sidebar'; -import ActionTypes from './actiontypes'; +import Action from './actions'; +import { getDatabase } from './reducers'; -const reduxUpdatedDesignDocList = (designDocs) => { - return { - type: ActionTypes.SIDEBAR_UPDATED_DESIGN_DOCS, - options: { - designDocs: Array.isArray(designDocs) ? designDocs : [] - } + +// returns a simple array of design doc IDs +const getAvailableDesignDocs = (state) => { + const availableDocs = state.designDocs.filter((doc) => { + return !doc.isMangoDoc(); + }); + return _.map(availableDocs, (doc) => { + return doc.id; + }); +}; + +const getDeleteIndexDesignDoc = (state) => { + const designDoc = state.designDocs.find((ddoc) => { + return '_design/' + state.deleteIndexModalDesignDocName === ddoc.id; + }); + + return designDoc ? designDoc.dDocModel() : null; +}; + + +const selectedNavItem = (selectedItem) => { + + // resets previous selection and sets new values + const settings = { + designDocName: '', + designDocSection: '', + indexName: '', + navItem: '', + ...selectedItem }; + return settings; }; -const mapStateToProps = () => { - return {}; +const mapStateToProps = ({ sidebar }, ownProps) => { + return { + database: getDatabase(sidebar), + selectedNav: selectedNavItem(ownProps.selectedNavItem), + designDocs: sidebar.designDocs, + // designDocList: getDesignDocList(sidebar), + designDocList: sidebar.designDocList, + availableDesignDocIds: getAvailableDesignDocs(sidebar), + toggledSections: sidebar.toggledSections, + isLoading: sidebar.loading, + + deleteIndexModalVisible: sidebar.deleteIndexModalVisible, + deleteIndexModalText: sidebar.deleteIndexModalText, + deleteIndexModalOnSubmit: sidebar.deleteIndexModalOnSubmit, + deleteIndexModalIndexName: sidebar.deleteIndexModalIndexName, + deleteIndexModalDesignDoc: getDeleteIndexDesignDoc(sidebar), + + cloneIndexModalVisible: sidebar.cloneIndexModalVisible, + cloneIndexModalTitle: sidebar.cloneIndexModalTitle, + cloneIndexModalSelectedDesignDoc: sidebar.cloneIndexModalSelectedDesignDoc, + cloneIndexModalNewDesignDocName: sidebar.cloneIndexModalNewDesignDocName, + cloneIndexModalOnSubmit: sidebar.cloneIndexModalOnSubmit, + cloneIndexDesignDocProp: sidebar.cloneIndexDesignDocProp, + cloneIndexModalNewIndexName: sidebar.cloneIndexModalNewIndexName, + cloneIndexSourceIndexName: sidebar.cloneIndexModalSourceIndexName, + cloneIndexSourceDesignDocName: sidebar.cloneIndexModalSourceDesignDocName, + cloneIndexModalIndexLabel: sidebar.cloneIndexModalIndexLabel + }; }; const mapDispatchToProps = (dispatch) => { return { - reduxUpdatedDesignDocList: (designDocsList) => { - dispatch(reduxUpdatedDesignDocList(designDocsList)); + toggleContent: (designDoc, indexGroup) => { + dispatch(Action.toggleContent(designDoc, indexGroup)); + }, + hideCloneIndexModal: () => { + dispatch(Action.hideCloneIndexModal()); + }, + hideDeleteIndexModal: () => { + dispatch(Action.hideDeleteIndexModal()); + }, + showDeleteIndexModal: (indexName, designDocName, indexLabel, onDelete) => { + dispatch(Action.showDeleteIndexModal(indexName, designDocName, indexLabel, onDelete)); + }, + showCloneIndexModal: (indexName, designDocName, indexLabel, onSubmit) => { + dispatch(Action.showCloneIndexModal(indexName, designDocName, indexLabel, onSubmit)); + }, + selectDesignDoc: (designDoc) => { + dispatch(Action.selectDesignDoc(designDoc)); + }, + updateNewDesignDocName: (designDocName) => { + dispatch(Action.updateNewDesignDocName(designDocName)); + }, + setNewCloneIndexName: (indexName) => { + dispatch(Action.setNewCloneIndexName(indexName)); } }; }; diff --git a/app/addons/documents/sidebar/__tests__/sidebar.actions.test.js b/app/addons/documents/sidebar/__tests__/sidebar.actions.test.js index e9393c1..a7fbb59 100644 --- a/app/addons/documents/sidebar/__tests__/sidebar.actions.test.js +++ b/app/addons/documents/sidebar/__tests__/sidebar.actions.test.js @@ -20,6 +20,15 @@ FauxtonAPI.router = new FauxtonAPI.Router([]); describe('Sidebar actions', () => { + beforeEach(() => { + FauxtonAPI.reduxState = sinon.stub().returns({ + sidebar:{ + loading: true + } + }); + FauxtonAPI.reduxDispatch = sinon.stub(); + }); + afterEach(() => { restore(FauxtonAPI.navigate); restore(FauxtonAPI.addNotification); @@ -47,7 +56,7 @@ describe('Sidebar actions', () => { } }; - Actions.newOptions(options); + Actions.dispatchNewOptions(options); process.nextTick(() => { assert.ok(notificationSpy.calledOnce); assert.ok(/not exist/.test(notificationSpy.args[0][0].msg)); diff --git a/app/addons/documents/sidebar/__tests__/sidebar.components.test.js b/app/addons/documents/sidebar/__tests__/sidebar.components.test.js index 75733cb..4e306b5 100644 --- a/app/addons/documents/sidebar/__tests__/sidebar.components.test.js +++ b/app/addons/documents/sidebar/__tests__/sidebar.components.test.js @@ -52,7 +52,9 @@ describe('DesignDoc', () => { designDocName={'doc-$-#-.1'} selectedNavInfo={selectedNavInfo} toggledSections={{}} - designDoc={{}} />); + designDoc={{}} + showDeleteIndexModal={() => {}} + showCloneIndexModal={() => {}} />); assert.include(wrapper.find('a.icon .fonticon-plus-circled').at(1).props()['href'], '/doc-%24-%23-.1'); assert.include(wrapper.find('a.toggle-view .accordion-header').props()['href'], '/doc-%24-%23-.1'); @@ -70,7 +72,9 @@ describe('DesignDoc', () => { designDocName={'id#1'} selectedNavInfo={{}} toggledSections={{}} - designDoc={{}} />); + designDoc={{}} + showDeleteIndexModal={() => {}} + showCloneIndexModal={() => {}} />); // NOTE: wrapper.find doesn't work special chars so we use class name instead wrapper.find('div.accordion-list-item').simulate('click', {preventDefault: sinon.stub()}); @@ -89,6 +93,8 @@ describe('DesignDoc', () => { toggledSections={{}} designDoc={{ customProp: { one: 'something' } }} designDocName={'doc-$-#-.1'} + showDeleteIndexModal={() => {}} + showCloneIndexModal={() => {}} />); const subOptions = el.find('.accordion-body li'); @@ -114,6 +120,8 @@ describe('DesignDoc', () => { toggledSections={{}} designDoc={{ customProp: { one: 'something' } }} designDocName={'doc-$-#-.1'} + showDeleteIndexModal={() => {}} + showCloneIndexModal={() => {}} />); const subOptions = el.find('.accordion-body li'); @@ -139,6 +147,8 @@ describe('DesignDoc', () => { designDoc={{}} // note that this is empty designDocName={'doc-$-#-.1'} toggledSections={{}} + showDeleteIndexModal={() => {}} + showCloneIndexModal={() => {}} />); const subOptions = el.find('.accordion-body li'); @@ -159,7 +169,9 @@ describe('DesignDoc', () => { }} designDocName={'doc-$-#-.1'} toggledSections={{}} - designDoc={{}} />); + designDoc={{}} + showDeleteIndexModal={() => {}} + showCloneIndexModal={() => {}} />); assert.equal(el.find('.accordion-body li.active a').text(), 'Metadata'); }); diff --git a/app/addons/documents/sidebar/__tests__/sidebar.reducers.test.js b/app/addons/documents/sidebar/__tests__/sidebar.reducers.test.js new file mode 100644 index 0000000..dc4ab10 --- /dev/null +++ b/app/addons/documents/sidebar/__tests__/sidebar.reducers.test.js @@ -0,0 +1,77 @@ +// 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. + +import sidebar from "../reducers"; +import ActionTypes from "../actiontypes"; +import testUtils from "../../../../../test/mocha/testUtils"; + +const assert = testUtils.assert; + +function isVisible (state, designDoc, indexGroup) { + if (!state.toggledSections[designDoc]) { + return false; + } + if (indexGroup) { + return state.toggledSections[designDoc].indexGroups[indexGroup]; + } + return state.toggledSections[designDoc].visible; +} + +describe('Sidebar Reducer', () => { + + describe('toggle state', () => { + + it('should be visible after being toggled', () => { + const designDoc = 'designDoc'; + const action = { + type: ActionTypes.SIDEBAR_TOGGLE_CONTENT, + designDoc: designDoc + }; + const newState = sidebar(undefined, action); + assert.ok(isVisible(newState, designDoc)); + }); + + it('should not be visible after being toggled twice', () => { + const designDoc = 'designDoc2'; + const action = { + type: ActionTypes.SIDEBAR_TOGGLE_CONTENT, + designDoc: designDoc + }; + let newState = sidebar(undefined, action); + newState = sidebar(newState, action); + assert.notOk(isVisible(newState, designDoc)); + }); + + }); + + describe('toggle state for index', () => { + const designDoc = 'design-doc'; + const indexGroup = 'index'; + const action = { + type: ActionTypes.SIDEBAR_TOGGLE_CONTENT, + designDoc: designDoc, + indexGroup: indexGroup + }; + + it('should toggle the state', () => { + let newState = sidebar(undefined, action); + assert.ok(isVisible(newState, designDoc)); + + newState = sidebar(newState, action); + assert.ok(isVisible(newState, designDoc, indexGroup)); + + newState = sidebar(newState, action); + assert.notOk(isVisible(newState, designDoc, indexGroup)); + }); + + }); +}); diff --git a/app/addons/documents/sidebar/__tests__/sidebar.stores.test.js b/app/addons/documents/sidebar/__tests__/sidebar.stores.test.js deleted file mode 100644 index 227b66c..0000000 --- a/app/addons/documents/sidebar/__tests__/sidebar.stores.test.js +++ /dev/null @@ -1,74 +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. - -import FauxtonAPI from "../../../../core/api"; -import Stores from "../stores"; -import testUtils from "../../../../../test/mocha/testUtils"; -const assert = testUtils.assert; -let dispatchToken; -let store; - -describe('Sidebar Store', () => { - beforeEach(() => { - store = new Stores.SidebarStore(); - dispatchToken = FauxtonAPI.dispatcher.register(store.dispatch.bind(store)); - }); - - afterEach(() => { - FauxtonAPI.dispatcher.unregister(dispatchToken); - }); - - describe('toggle state', () => { - - it('should not be visible if never toggled', () => { - assert.notOk(store.isVisible('designDoc')); - }); - - it('should be visible after being toggled', () => { - var designDoc = 'designDoc'; - store.toggleContent(designDoc); - assert.ok(store.isVisible(designDoc)); - }); - - it('should not be visible after being toggled twice', () => { - var designDoc = 'designDoc'; - store.toggleContent(designDoc); - store.toggleContent(designDoc); - assert.notOk(store.isVisible(designDoc)); - }); - - }); - - describe('toggle state for index', () => { - var designDoc = 'design-doc'; - - beforeEach(() => { - store.toggleContent(designDoc); - }); - - it('should be hidden if never toggled', () => { - assert.notOk(store.isVisible(designDoc, 'index')); - }); - - it('should be if toggled', () => { - store.toggleContent(designDoc, 'index'); - assert.ok(store.isVisible(designDoc, 'index')); - }); - - it('should be hidden after being toggled twice', () => { - store.toggleContent(designDoc, 'index'); - store.toggleContent(designDoc, 'index'); - assert.notOk(store.isVisible(designDoc, 'index')); - }); - - }); -}); diff --git a/app/addons/documents/sidebar/actions.js b/app/addons/documents/sidebar/actions.js index ab93c2c..7564b3c 100644 --- a/app/addons/documents/sidebar/actions.js +++ b/app/addons/documents/sidebar/actions.js @@ -12,18 +12,23 @@ import FauxtonAPI from "../../../core/api"; import ActionTypes from "./actiontypes"; -import Stores from "./stores"; -var store = Stores.sidebarStore; -function newOptions (options) { - if (options.database.safeID() !== store.getDatabaseName()) { - FauxtonAPI.dispatch({ +const _getDatabaseName = ({sidebar}) => { + if (!sidebar || sidebar.loading) { + return ''; + } + return sidebar.database.safeID(); +}; + +const dispatchNewOptions = (options) => { + if (options.database.safeID() !== _getDatabaseName(FauxtonAPI.reduxState())) { + FauxtonAPI.reduxDispatch({ type: ActionTypes.SIDEBAR_FETCHING }); } options.designDocs.fetch().then(() => { - FauxtonAPI.dispatch({ + FauxtonAPI.reduxDispatch({ type: ActionTypes.SIDEBAR_NEW_OPTIONS, options: options }); @@ -40,56 +45,48 @@ function newOptions (options) { clear: true }); }); -} +}; -function updateDesignDocs (designDocs) { - FauxtonAPI.dispatch({ +const dispatchUpdateDesignDocs = (designDocs) => { + FauxtonAPI.reduxDispatch({ type: ActionTypes.SIDEBAR_FETCHING }); designDocs.fetch().then(function () { - FauxtonAPI.dispatch({ + FauxtonAPI.reduxDispatch({ type: ActionTypes.SIDEBAR_UPDATED_DESIGN_DOCS, options: { designDocs: designDocs } }); }); -} +}; + +const dispatchHideDeleteIndexModal = () => { + FauxtonAPI.reduxDispatch({ + type: ActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL + }); +}; + +const dispatchExpandSelectedItem = (selectedNavItem) => { + FauxtonAPI.reduxDispatch({ + type: ActionTypes.SIDEBAR_EXPAND_SELECTED_ITEM, + options: { + selectedNavItem: selectedNavItem + } + }); +}; -function toggleContent (designDoc, indexGroup) { - FauxtonAPI.dispatch({ +const toggleContent = (designDoc, indexGroup) => (dispatch) => { + dispatch({ type: ActionTypes.SIDEBAR_TOGGLE_CONTENT, designDoc: designDoc, indexGroup: indexGroup }); -} - -// This selects any item in the sidebar, including nested nav items to ensure the appropriate item is visible -// and highlighted. Params: -// - `navItem`: 'permissions', 'changes', 'all-docs', 'compact', 'mango-query', 'designDoc' (or anything thats been -// extended) -// - `params`: optional object if you passed designDoc as the first param. This lets you specify which sub-page -// should be selected, e.g. -// Actions.selectNavItem('designDoc', { designDocName: 'my-design-doc', section: 'metadata' }); -// Actions.selectNavItem('designDoc', { designDocName: 'my-design-doc', section: 'Views', indexName: 'my-view' }); -function selectNavItem (navItem, params) { - const settings = { - designDocName: '', - designDocSection: '', - indexName: '', - ...params - }; - settings.navItem = navItem; - - FauxtonAPI.dispatch({ - type: ActionTypes.SIDEBAR_SET_SELECTED_NAV_ITEM, - options: settings - }); -} +}; -function showDeleteIndexModal (indexName, designDocName, indexLabel, onDelete) { - FauxtonAPI.dispatch({ +const showDeleteIndexModal = (indexName, designDocName, indexLabel, onDelete) => (dispatch) => { + dispatch({ type: ActionTypes.SIDEBAR_SHOW_DELETE_INDEX_MODAL, options: { indexName: indexName, @@ -98,14 +95,16 @@ function showDeleteIndexModal (indexName, designDocName, indexLabel, onDelete) { onDelete: onDelete } }); -} +}; -function hideDeleteIndexModal () { - FauxtonAPI.dispatch({ type: ActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL }); -} +const hideDeleteIndexModal = () => (dispatch) => { + dispatch({ + type: ActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL + }); +}; -function showCloneIndexModal (indexName, designDocName, indexLabel, onSubmit) { - FauxtonAPI.dispatch({ +const showCloneIndexModal = (indexName, designDocName, indexLabel, onSubmit) => (dispatch) => { + dispatch({ type: ActionTypes.SIDEBAR_SHOW_CLONE_INDEX_MODAL, options: { sourceIndexName: indexName, @@ -115,50 +114,52 @@ function showCloneIndexModal (indexName, designDocName, indexLabel, onSubmit) { cloneIndexModalTitle: 'Clone ' + indexLabel } }); -} +}; -function hideCloneIndexModal () { - FauxtonAPI.dispatch({ type: ActionTypes.SIDEBAR_HIDE_CLONE_INDEX_MODAL }); -} +const hideCloneIndexModal = () => (dispatch) => { + dispatch({ + type: ActionTypes.SIDEBAR_HIDE_CLONE_INDEX_MODAL + }); +}; -function updateNewDesignDocName (designDocName) { - FauxtonAPI.dispatch({ +const updateNewDesignDocName = (designDocName) => (dispatch) => { + dispatch({ type: ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_NEW_NAME_UPDATED, options: { value: designDocName } }); -} +}; -function selectDesignDoc (designDoc) { - FauxtonAPI.dispatch({ +const selectDesignDoc = (designDoc) => (dispatch) => { + dispatch({ type: ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_CHANGE, options: { value: designDoc } }); -} +}; -function setNewCloneIndexName (indexName) { - FauxtonAPI.dispatch({ +const setNewCloneIndexName = (indexName) => (dispatch) => { + dispatch({ type: ActionTypes.SIDEBAR_CLONE_MODAL_UPDATE_INDEX_NAME, options: { value: indexName } }); -} - +}; export default { - newOptions: newOptions, - updateDesignDocs: updateDesignDocs, - toggleContent: toggleContent, - selectNavItem: selectNavItem, - showDeleteIndexModal: showDeleteIndexModal, - hideDeleteIndexModal: hideDeleteIndexModal, - showCloneIndexModal: showCloneIndexModal, - hideCloneIndexModal: hideCloneIndexModal, - updateNewDesignDocName: updateNewDesignDocName, - selectDesignDoc: selectDesignDoc, - setNewCloneIndexName: setNewCloneIndexName + dispatchNewOptions, + dispatchUpdateDesignDocs, + toggleContent, + showDeleteIndexModal, + hideDeleteIndexModal, + dispatchHideDeleteIndexModal, + showCloneIndexModal, + hideCloneIndexModal, + updateNewDesignDocName, + selectDesignDoc, + setNewCloneIndexName, + dispatchExpandSelectedItem }; diff --git a/app/addons/documents/sidebar/actiontypes.js b/app/addons/documents/sidebar/actiontypes.js index f08d4bc..666b8a9 100644 --- a/app/addons/documents/sidebar/actiontypes.js +++ b/app/addons/documents/sidebar/actiontypes.js @@ -11,7 +11,7 @@ // the License. export default { - SIDEBAR_SET_SELECTED_NAV_ITEM: 'SIDEBAR_SET_SELECTED_NAV_ITEM', + SIDEBAR_EXPAND_SELECTED_ITEM: 'SIDEBAR_EXPAND_SELECTED_ITEM', SIDEBAR_NEW_OPTIONS: 'SIDEBAR_NEW_OPTIONS', SIDEBAR_TOGGLE_CONTENT: 'SIDEBAR_TOGGLE_CONTENT', SIDEBAR_FETCHING: 'SIDEBAR_FETCHING', diff --git a/app/addons/documents/sidebar/components/CloneIndexModal.js b/app/addons/documents/sidebar/components/CloneIndexModal.js new file mode 100644 index 0000000..220e8ab --- /dev/null +++ b/app/addons/documents/sidebar/components/CloneIndexModal.js @@ -0,0 +1,114 @@ +// 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. + +import PropTypes from 'prop-types'; +import React from 'react'; +import { Modal } from 'react-bootstrap'; +import ReactDOM from 'react-dom'; +import FauxtonAPI from '../../../../core/api'; +import IndexEditorComponents from '../../index-editor/components'; + +const { DesignDocSelector } = IndexEditorComponents; + +export default class CloneIndexModal extends React.Component { + static propTypes = { + visible: PropTypes.bool.isRequired, + title: PropTypes.string, + close: PropTypes.func.isRequired, + submit: PropTypes.func.isRequired, + designDocArray: PropTypes.array.isRequired, + selectedDesignDoc: PropTypes.string.isRequired, + newDesignDocName: PropTypes.string.isRequired, + newIndexName: PropTypes.string.isRequired, + indexLabel: PropTypes.string.isRequired, + selectDesignDoc: PropTypes.func.isRequired, + updateNewDesignDocName: PropTypes.func.isRequired, + setNewCloneIndexName: PropTypes.func.isRequired + }; + + static defaultProps = { + title: 'Clone Index', + visible: false + }; + + constructor(props) { + super(props); + this.props.setNewCloneIndexName(''); + } + + submit = () => { + if (!this.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 = (e) => { + if (e) { + e.preventDefault(); + } + this.props.close(); + }; + + setNewIndexName = (e) => { + this.props.setNewCloneIndexName(e.target.value); + }; + + render() { + 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={node => this.designDocSelector = node} + designDocList={this.props.designDocArray} + selectedDesignDocName={this.props.selectedDesignDoc} + newDesignDocName={this.props.newDesignDocName} + onSelectDesignDoc={this.props.selectDesignDoc} + onChangeNewDesignDocName={this.props.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="New view name" /> + </div> + </form> + + </Modal.Body> + <Modal.Footer> + <a href="#" className="cancel-link" onClick={this.close} data-bypass="true">Cancel</a> + <button onClick={this.submit} data-bypass="true" className="btn btn-primary save"> + <i className="icon fonticon-ok-circled" /> Clone {this.props.indexLabel}</button> + </Modal.Footer> + </Modal> + ); + } +} diff --git a/app/addons/documents/sidebar/components/DesignDoc.js b/app/addons/documents/sidebar/components/DesignDoc.js new file mode 100644 index 0000000..c0cc2e5 --- /dev/null +++ b/app/addons/documents/sidebar/components/DesignDoc.js @@ -0,0 +1,158 @@ +// 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. + +import PropTypes from 'prop-types'; +import React from 'react'; +import { Collapse } from 'react-bootstrap'; +import ReactDOM from 'react-dom'; +import FauxtonAPI from '../../../../core/api'; +import Components from '../../../components/react-components'; +import IndexEditorActions from '../../index-editor/actions'; +import IndexSection from './IndexSection'; + +const { MenuDropDown } = Components; + +export default class DesignDoc extends React.Component { + static propTypes = { + database: PropTypes.object.isRequired, + sidebarListTypes: PropTypes.array.isRequired, + isExpanded: PropTypes.bool.isRequired, + selectedNavInfo: PropTypes.object.isRequired, + toggledSections: PropTypes.object.isRequired, + designDocName: PropTypes.string.isRequired, + showDeleteIndexModal: PropTypes.func.isRequired, + showCloneIndexModal: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + this.state = { + updatedSidebarListTypes: this.props.sidebarListTypes + }; + if (_.isEmpty(this.state.updatedSidebarListTypes) || + (_.has(this.state.updatedSidebarListTypes[0], 'selector') && this.state.updatedSidebarListTypes[0].selector !== 'views')) { + + const newList = this.state.updatedSidebarListTypes; + newList.unshift({ + selector: 'views', + name: 'Views', + urlNamespace: 'view', + indexLabel: 'view', + onDelete: IndexEditorActions.deleteView, + onClone: IndexEditorActions.cloneView, + onEdit: IndexEditorActions.gotoEditViewPage + }); + this.state = { updatedSidebarListTypes: newList }; + } + } + + indexList = () => { + return _.map(this.state.updatedSidebarListTypes, (index, key) => { + const expanded = _.has(this.props.toggledSections, index.name) && this.props.toggledSections[index.name]; + + // if an index in this list is selected, pass that down + let 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])} + showDeleteIndexModal={this.props.showDeleteIndexModal} + showCloneIndexModal={this.props.showCloneIndexModal} /> + ); + }); + }; + + toggle = (e) => { + e.preventDefault(); + this.props.toggle(this.props.designDocName); + }; + + getNewButtonLinks = () => { + const newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', encodeURIComponent(this.props.database.id)); + const designDocName = this.props.designDocName; + + const addNewLinks = _.reduce(FauxtonAPI.getExtensions('sidebar:links'), function (menuLinks, link) { + menuLinks.push({ + title: link.title, + url: '#' + newUrlPrefix + '/' + link.url + '/' + encodeURIComponent(designDocName), + icon: 'fonticon-plus-circled' + }); + return menuLinks; + }, [{ + title: 'New View', + url: '#' + FauxtonAPI.urls('new', 'addView', encodeURIComponent(this.props.database.id), encodeURIComponent(designDocName)), + icon: 'fonticon-plus-circled' + }]); + + return [{ + title: 'Add New', + links: addNewLinks + }]; + }; + + render () { + const buttonLinks = this.getNewButtonLinks(); + let toggleClassNames = 'design-doc-section accordion-header'; + let toggleBodyClassNames = 'design-doc-body accordion-body collapse'; + + if (this.props.isExpanded) { + toggleClassNames += ' down'; + toggleBodyClassNames += ' in'; + } + const designDocName = this.props.designDocName; + const designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', encodeURIComponent(this.props.database.id), designDocName); + const 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'> + <MenuDropDown links={buttonLinks} /> + </div> + </div> + <Collapse in={this.props.isExpanded}> + <ul className={toggleBodyClassNames} id={this.props.designDocName}> + <li className={metadataRowClass}> + <a href={"#/" + designDocMetaUrl} className="toggle-view accordion-header"> + Metadata + </a> + </li> + {this.indexList()} + </ul> + </Collapse> + </li> + ); + } +} diff --git a/app/addons/documents/sidebar/components/DesignDocList.js b/app/addons/documents/sidebar/components/DesignDocList.js new file mode 100644 index 0000000..79deace --- /dev/null +++ b/app/addons/documents/sidebar/components/DesignDocList.js @@ -0,0 +1,82 @@ +// 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. + +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import FauxtonAPI from '../../../../core/api'; +import DesignDoc from './DesignDoc'; + +export default class DesignDocList extends React.Component { + static propTypes = { + database: PropTypes.object.isRequired, + toggle: PropTypes.func.isRequired, + designDocs: PropTypes.array, + toggledSections: PropTypes.object, + selectedNav: PropTypes.shape({ + designDocName: PropTypes.string, + designDocSection: PropTypes.string, + indexName: PropTypes.string, + navItem: PropTypes.string + }).isRequired, + showDeleteIndexModal: PropTypes.func.isRequired, + showCloneIndexModal: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + const list = FauxtonAPI.getExtensions('sidebar:list'); + this.sidebarListTypes = _.isUndefined(list) ? [] : list; + } + + designDocList = () => { + return _.map(this.props.designDocs, (designDoc, key) => { + const ddName = decodeURIComponent(designDoc.safeId); + + // only pass down the selected nav info and toggle info if they're relevant for this particular design doc + let expanded = false, + toggledSections = {}; + if (_.has(this.props.toggledSections, ddName)) { + expanded = this.props.toggledSections[ddName].visible; + toggledSections = this.props.toggledSections[ddName].indexGroups; + } + + let 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} + showDeleteIndexModal={this.props.showDeleteIndexModal} + showCloneIndexModal={this.props.showCloneIndexModal} /> + ); + }); + }; + + render() { + return ( + <ul className="nav nav-list"> + {this.designDocList()} + </ul> + ); + } +} diff --git a/app/addons/documents/sidebar/components/IndexSection.js b/app/addons/documents/sidebar/components/IndexSection.js new file mode 100644 index 0000000..cbee3a8 --- /dev/null +++ b/app/addons/documents/sidebar/components/IndexSection.js @@ -0,0 +1,151 @@ +// 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. + +import PropTypes from 'prop-types'; +import React from 'react'; +import { Collapse, OverlayTrigger, Popover } from 'react-bootstrap'; +import ReactDOM from 'react-dom'; +import FauxtonAPI from '../../../../core/api'; + +export default class IndexSection extends React.Component { + static propTypes = { + urlNamespace: PropTypes.string.isRequired, + indexLabel: PropTypes.string.isRequired, + database: PropTypes.object.isRequired, + designDocName: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + isExpanded: PropTypes.bool.isRequired, + selectedIndex: PropTypes.string.isRequired, + onDelete: PropTypes.func.isRequired, + onClone: PropTypes.func.isRequired, + showDeleteIndexModal: PropTypes.func.isRequired, + showCloneIndexModal: PropTypes.func.isRequired + }; + + state = { + placement: 'bottom' + }; + + // this dynamically changes the placement of the menu (top/bottom) to prevent it going offscreen and causing some + // unsightly shifting + setPlacement = (rowId) => { + const rowTop = document.getElementById(rowId).getBoundingClientRect().top; + const toggleHeight = 150; // the height of the menu overlay, arrow, view row + const placement = (rowTop + toggleHeight > window.innerHeight) ? 'top' : 'bottom'; + this.setState({ placement: placement }); + }; + + createItems = () => { + + // sort the indexes alphabetically + const sortedItems = this.props.items.sort(); + + return _.map(sortedItems, (indexName, index) => { + const href = FauxtonAPI.urls(this.props.urlNamespace, 'app', encodeURIComponent(this.props.database.id), encodeURIComponent(this.props.designDocName)); + const className = (this.props.selectedIndex === indexName) ? 'active' : ''; + + return ( + <li className={className} key={index}> + <a + id={this.props.designDocName + '_' + indexName} + href={"#/" + href + encodeURIComponent(indexName)} + className="toggle-view"> + {indexName} + </a> + <OverlayTrigger + trigger="click" + onEnter={this.setPlacement.bind(this, this.props.designDocName + '_' + indexName)} + placement={this.state.placement} + rootClose={true} + ref={overlay => this.itemOverlay = overlay} + 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> + ); + }); + }; + + indexAction = (action, params, e) => { + e.preventDefault(); + + this.itemOverlay.hide(); + + switch (action) { + case 'delete': + this.props.showDeleteIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onDelete); + break; + case 'clone': + this.props.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 = (e) => { + e.preventDefault(); + this.props.toggle(this.props.designDocName, this.props.title); + }; + + render() { + + // 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; + } + + let toggleClassNames = 'accordion-header index-group-header'; + let toggleBodyClassNames = 'index-list accordion-body collapse'; + if (this.props.isExpanded) { + toggleClassNames += ' down'; + toggleBodyClassNames += ' in'; + } + + const title = this.props.title; + const designDocName = this.props.designDocName; + const 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> + <Collapse in={this.props.isExpanded}> + <ul className={toggleBodyClassNames}> + {this.createItems()} + </ul> + </Collapse> + </li> + ); + } +} diff --git a/app/addons/documents/sidebar/components/MainSidebar.js b/app/addons/documents/sidebar/components/MainSidebar.js new file mode 100644 index 0000000..14c40c0 --- /dev/null +++ b/app/addons/documents/sidebar/components/MainSidebar.js @@ -0,0 +1,98 @@ +// 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. + +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import app from "../../../../app"; +import FauxtonAPI from '../../../../core/api'; +import DocumentHelper from "../../../documents/helpers"; +import Components from '../../../components/react-components'; + +const { MenuDropDown } = Components; + +export default class MainSidebar extends React.Component { + static propTypes = { + selectedNavItem: PropTypes.string.isRequired + }; + + getNewButtonLinks = () => { // these are links for the sidebar '+' on All Docs and All Design Docs + return DocumentHelper.getNewButtonLinks(this.props.databaseName); + }; + + buildDocLinks = () => { + const base = FauxtonAPI.urls('base', 'app', this.props.databaseName); + return FauxtonAPI.getExtensions('docLinks').map((link) => { + return ( + <li key={link.url} className={this.getNavItemClass(link.url)}> + <a id={link.url} href={base + link.url}>{link.title}</a> + </li> + ); + }); + }; + + getNavItemClass = (navItem) => { + return (navItem === this.props.selectedNavItem) ? 'active' : ''; + }; + + render() { + const docLinks = this.buildDocLinks(); + const dbEncoded = FauxtonAPI.url.encode(this.props.databaseName); + const changesUrl = '#' + FauxtonAPI.urls('changes', 'app', dbEncoded, ''); + const permissionsUrl = '#' + FauxtonAPI.urls('permissions', 'app', dbEncoded); + const databaseUrl = FauxtonAPI.urls('allDocs', 'app', dbEncoded, ''); + const mangoQueryUrl = FauxtonAPI.urls('mango', 'query-app', dbEncoded); + const runQueryWithMangoText = app.i18n.en_US['run-query-with-mango']; + const 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"> + <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"> + <MenuDropDown links={buttonLinks} /> + </div> + </li> + </ul> + ); + } +} diff --git a/app/addons/documents/sidebar/components/SidebarController.js b/app/addons/documents/sidebar/components/SidebarController.js new file mode 100644 index 0000000..d9a5484 --- /dev/null +++ b/app/addons/documents/sidebar/components/SidebarController.js @@ -0,0 +1,146 @@ +// 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. + +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import ComponentsActions from "../../../components/actions"; +import Components from '../../../components/react-components'; +import ComponentsStore from '../../../components/stores'; +import GeneralComponents from '../../../fauxton/components'; +import CloneIndexModal from './CloneIndexModal'; +import DesignDocList from './DesignDocList'; +import MainSidebar from './MainSidebar'; + +const { DeleteDatabaseModal, LoadLines } = Components; +const { ConfirmationModal } = GeneralComponents; +const { deleteDbModalStore } = ComponentsStore; + +export default class SidebarController extends React.Component { + + static propTypes = { + selectedNav: PropTypes.shape({ + designDocName: PropTypes.string, + designDocSection: PropTypes.string, + indexName: PropTypes.string, + navItem: PropTypes.string + }).isRequired + }; + + constructor(props) { + super(props); + this.state = this.getDeleteDbStoreState(); + this.deleteIndex = this.deleteIndex.bind(this); + this.cloneIndex = this.cloneIndex.bind(this); + } + + componentDidMount() { + deleteDbModalStore.on('change', this.onChange, this); + } + + componentWillUnmount() { + deleteDbModalStore.off('change', this.onChange, this); + } + + getDeleteDbStoreState() { + return { + deleteDbModalProperties: deleteDbModalStore.getShowDeleteDatabaseModal() + }; + } + + onChange = () => { + const newState = this.getDeleteDbStoreState(); + this.setState(newState); + }; + + showDeleteDatabaseModal = (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 = () => { + + // 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 + const isOnIndex = this.props.selectedNav.navItem === 'designDoc' && + ('_design/' + this.props.selectedNav.designDocName) === this.props.deleteIndexModalDesignDoc.id && + this.props.selectedNav.indexName === this.props.deleteIndexModalIndexName; + + this.props.deleteIndexModalOnSubmit({ + isOnIndex: isOnIndex, + indexName: this.props.deleteIndexModalIndexName, + designDoc: this.props.deleteIndexModalDesignDoc, + designDocs: this.props.designDocs, + database: this.props.database + }); + }; + + cloneIndex = () => { + this.props.cloneIndexModalOnSubmit({ + sourceIndexName: this.props.cloneIndexSourceIndexName, + sourceDesignDocName: this.props.cloneIndexSourceDesignDocName, + targetDesignDocName: this.props.cloneIndexModalSelectedDesignDoc, + newDesignDocName: this.props.cloneIndexModalNewDesignDocName, + newIndexName: this.props.cloneIndexModalNewIndexName, + designDocs: this.props.designDocs, + database: this.props.database, + onComplete: this.props.hideCloneIndexModal + }); + }; + + render() { + if (this.props.isLoading) { + return <LoadLines />; + } + + return ( + <nav className="sidenav"> + <MainSidebar + selectedNavItem={this.props.selectedNav.navItem} + databaseName={this.props.database.id} /> + <DesignDocList + selectedNav={this.props.selectedNav} + toggle={this.props.toggleContent} + toggledSections={this.props.toggledSections} + designDocs={this.props.designDocList} + database={this.props.database} + showDeleteIndexModal={this.props.showDeleteIndexModal} + showCloneIndexModal={this.props.showCloneIndexModal} /> + <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.props.deleteIndexModalVisible} + text={this.props.deleteIndexModalText} + onClose={this.props.hideDeleteIndexModal} + onSubmit={this.deleteIndex} /> + <CloneIndexModal + visible={this.props.cloneIndexModalVisible} + title={this.props.cloneIndexModalTitle} + close={this.props.hideCloneIndexModal} + submit={this.cloneIndex} + designDocArray={this.props.availableDesignDocIds} + selectedDesignDoc={this.props.cloneIndexModalSelectedDesignDoc} + newDesignDocName={this.props.cloneIndexModalNewDesignDocName} + newIndexName={this.props.cloneIndexModalNewIndexName} + indexLabel={this.props.cloneIndexModalIndexLabel} + selectDesignDoc={this.props.selectDesignDoc} + updateNewDesignDocName={this.props.updateNewDesignDocName} + setNewCloneIndexName={this.props.setNewCloneIndexName} /> + </nav> + ); + } +} diff --git a/app/addons/documents/sidebar/helpers.js b/app/addons/documents/sidebar/helpers.js new file mode 100644 index 0000000..5950208 --- /dev/null +++ b/app/addons/documents/sidebar/helpers.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. + + +/** + * Represents the selected item in the sidebar, including nested nav items to ensure the appropriate item + * is visible and highlighted. + */ +export class SidebarItemSelection { + + /** + * Creates a new sidebar selection. + * + * @param {string} navItem 'permissions', 'changes', 'all-docs', 'compact', 'mango-query', 'designDoc' + * (or anything thats beenextended) + * @param {string} [params] (optional) If you passed 'designDoc' as the first param. This lets you + * specify which sub-item should be selected, e.g.: + * Actions.selectNavItem('designDoc', { designDocName: 'my-design-doc', section: 'metadata' }); + * Actions.selectNavItem('designDoc', { designDocName: 'my-design-doc', section: 'Views', indexName: 'my-view' }); + */ + constructor(navItem, params) { + this.navItem = navItem; + if (params) { + const {designDocName, designDocSection, indexName} = params; + this.designDocName = designDocName ? designDocName : ''; + this.designDocSection = designDocSection ? designDocSection : ''; + this.indexName = indexName ? indexName : ''; + } + } +} + diff --git a/app/addons/documents/sidebar/reducers.js b/app/addons/documents/sidebar/reducers.js index 6359823..f1fa140 100644 --- a/app/addons/documents/sidebar/reducers.js +++ b/app/addons/documents/sidebar/reducers.js @@ -10,19 +10,223 @@ // License for the specific language governing permissions and limitations under // the License. -import ActionTypes from './actiontypes'; +import React from "react"; +import app from "../../../app"; +import ActionTypes from "./actiontypes"; const initialState = { - designDocs: [] + designDocs: new Backbone.Collection(), + designDocList: [], + selected: { + navItem: 'all-docs', + designDocName: '', + designDocSection: '', // 'metadata' / name of index group ("Views", etc.) + indexName: '' + }, + loading: true, + toggledSections: {}, + + deleteIndexModalVisible: false, + deleteIndexModalDesignDocName: '', + deleteIndexModalText: '', + deleteIndexModalIndexName: '', + deleteIndexModalOnSubmit: () => {}, + + cloneIndexModalVisible: false, + cloneIndexDesignDocProp: '', + cloneIndexModalTitle: '', + cloneIndexModalSelectedDesignDoc: '', + cloneIndexModalNewDesignDocName: '', + cloneIndexModalNewIndexName: '', + cloneIndexModalSourceIndexName: '', + cloneIndexModalSourceDesignDocName: '', + cloneIndexModalIndexLabel: '', + cloneIndexModalOnSubmit: () => {} }; -export default function resultsState(state = initialState, action) { +function setNewOptions(state, options) { + const newState = { + ...state, + database: options.database, + designDocs: options.designDocs, + designDocList: getDesignDocList(options.designDocs), + 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) { + newState.selected = { + navItem: options.selectedNavItem, + designDocName: '', + designDocSection: '', + indexName: '' + }; + } + + return newState; +} + +function toggleContent(state, designDoc, indexGroup) { + // used to toggle both design docs, and any index groups within them + const newState = { + ...state + }; + + if (!state.toggledSections[designDoc]) { + newState.toggledSections[designDoc] = { + visible: true, + indexGroups: {} + }; + return newState; + } + + if (indexGroup) { + const expanded = state.toggledSections[designDoc].indexGroups[indexGroup]; + + if (_.isUndefined(expanded)) { + newState.toggledSections[designDoc].indexGroups[indexGroup] = true; + } else { + newState.toggledSections[designDoc].indexGroups[indexGroup] = !expanded; + } + return newState; + } + + newState.toggledSections[designDoc].visible = !state.toggledSections[designDoc].visible; + + return newState; +} + +function expandSelectedItem(state, {selectedNavItem}) { + const newState = { + ...state + }; + + if (selectedNavItem.designDocName) { + if (!_.has(state.toggledSections, selectedNavItem.designDocName)) { + newState.toggledSections[selectedNavItem.designDocName] = { + visible: true, + indexGroups: {} + }; + } + newState.toggledSections[selectedNavItem.designDocName].visible = true; + + if (selectedNavItem.designDocSection) { + newState.toggledSections[selectedNavItem.designDocName].indexGroups[selectedNavItem.designDocSection] = true; + } + } + return newState; +} + +function getDesignDocList (designDocs) { + if (!designDocs) { + return []; + } + let docs = designDocs.toJSON(); + docs = _.filter(docs, (doc) => { + if (_.has(doc.doc, 'language')) { + return doc.doc.language !== 'query'; + } + return true; + }); + + const ddocsList = docs.map((doc) => { + doc.safeId = app.utils.safeURLName(doc._id.replace(/^_design\//, '')); + return _.extend(doc, doc.doc); + }); + return ddocsList; +} + +export const getDatabase = (state) => { + if (state.loading) { + return {}; + } + return state.database; +}; + +export default function sidebar(state = initialState, action) { + const { options } = action; switch (action.type) { + case ActionTypes.SIDEBAR_EXPAND_SELECTED_ITEM: + return expandSelectedItem(state, options); + + case ActionTypes.SIDEBAR_NEW_OPTIONS: + return setNewOptions(state, options); + + case ActionTypes.SIDEBAR_TOGGLE_CONTENT: + return toggleContent(state, action.designDoc, action.indexGroup); + + case ActionTypes.SIDEBAR_FETCHING: + return { + ...state, + loading: true + }; + + case ActionTypes.SIDEBAR_SHOW_DELETE_INDEX_MODAL: + return { + ...state, + deleteIndexModalIndexName: options.indexName, + deleteIndexModalDesignDocName: options.designDocName, + deleteIndexModalVisible: true, + deleteIndexModalText: ( + <div> + Are you sure you want to delete the <code>{options.indexName}</code> {options.indexLabel}? + </div> + ), + deleteIndexModalOnSubmit: options.onDelete + }; + + + case ActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL: + return { + ...state, + deleteIndexModalVisible: false + }; + + case ActionTypes.SIDEBAR_SHOW_CLONE_INDEX_MODAL: + return { + ...state, + cloneIndexModalIndexLabel: options.indexLabel, + cloneIndexModalTitle: options.cloneIndexModalTitle, + cloneIndexModalSourceIndexName: options.sourceIndexName, + cloneIndexModalSourceDesignDocName: options.sourceDesignDocName, + cloneIndexModalSelectedDesignDoc: '_design/' + options.sourceDesignDocName, + cloneIndexDesignDocProp: '', + cloneIndexModalVisible: true, + cloneIndexModalOnSubmit: options.onSubmit + }; + + case ActionTypes.SIDEBAR_HIDE_CLONE_INDEX_MODAL: + return { + ...state, + cloneIndexModalVisible: false + }; + + case ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_CHANGE: + return { + ...state, + cloneIndexModalSelectedDesignDoc: options.value + }; + + case ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_NEW_NAME_UPDATED: + return { + ...state, + cloneIndexModalNewDesignDocName: options.value + }; + + case ActionTypes.SIDEBAR_CLONE_MODAL_UPDATE_INDEX_NAME: + return { + ...state, + cloneIndexModalNewIndexName: options.value + }; + case ActionTypes.SIDEBAR_UPDATED_DESIGN_DOCS: - return Object.assign({}, state, { - designDocs: action.options.designDocs - }); + return { + ...state, + designDocs: options.designDocs, + designDocList: getDesignDocList(options.designDocs), + loading: false + }; default: return state; diff --git a/app/addons/documents/sidebar/sidebar.js b/app/addons/documents/sidebar/sidebar.js index 60624fd..a20420b 100644 --- a/app/addons/documents/sidebar/sidebar.js +++ b/app/addons/documents/sidebar/sidebar.js @@ -10,639 +10,12 @@ // License for the specific language governing permissions and limitations under // the License. -import PropTypes from 'prop-types'; -import React from "react"; -import ReactDOM from "react-dom"; -import app from "../../../app"; -import FauxtonAPI from "../../../core/api"; -import Stores from "./stores"; -import Actions from "./actions"; -import Components from "../../components/react-components"; -import ComponentsStore from "../../components/stores"; -import ComponentsActions from "../../components/actions"; -import IndexEditorActions from "../index-editor/actions"; -import IndexEditorComponents from "../index-editor/components"; -import GeneralComponents from "../../fauxton/components"; -import DocumentHelper from "../../documents/helpers"; -import { Collapse, OverlayTrigger, Popover, Modal } from "react-bootstrap"; -import "../../../../assets/js/plugins/prettify"; - -const store = Stores.sidebarStore; -const { DeleteDatabaseModal, LoadLines, MenuDropDown } = Components; -const { DesignDocSelector } = IndexEditorComponents; -const { ConfirmationModal } = GeneralComponents; -const { deleteDbModalStore } = ComponentsStore; - -class MainSidebar extends React.Component { - static propTypes = { - selectedNavItem: PropTypes.string.isRequired - }; - - getNewButtonLinks = () => { // these are links for the sidebar '+' on All Docs and All Design Docs - return DocumentHelper.getNewButtonLinks(this.props.databaseName); - }; - - buildDocLinks = () => { - const base = FauxtonAPI.urls('base', 'app', this.props.databaseName); - return FauxtonAPI.getExtensions('docLinks').map((link) => { - return ( - <li key={link.url} className={this.getNavItemClass(link.url)}> - <a id={link.url} href={base + link.url}>{link.title}</a> - </li> - ); - }); - }; - - getNavItemClass = (navItem) => { - return (navItem === this.props.selectedNavItem) ? 'active' : ''; - }; - - render() { - const docLinks = this.buildDocLinks(); - const dbEncoded = FauxtonAPI.url.encode(this.props.databaseName); - const changesUrl = '#' + FauxtonAPI.urls('changes', 'app', dbEncoded, ''); - const permissionsUrl = '#' + FauxtonAPI.urls('permissions', 'app', dbEncoded); - const databaseUrl = FauxtonAPI.urls('allDocs', 'app', dbEncoded, ''); - const mangoQueryUrl = FauxtonAPI.urls('mango', 'query-app', dbEncoded); - const runQueryWithMangoText = app.i18n.en_US['run-query-with-mango']; - const 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"> - <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"> - <MenuDropDown links={buttonLinks} /> - </div> - </li> - </ul> - ); - } -} - -class IndexSection extends React.Component { - static propTypes = { - urlNamespace: PropTypes.string.isRequired, - indexLabel: PropTypes.string.isRequired, - database: PropTypes.object.isRequired, - designDocName: PropTypes.string.isRequired, - items: PropTypes.array.isRequired, - isExpanded: PropTypes.bool.isRequired, - selectedIndex: PropTypes.string.isRequired, - onDelete: PropTypes.func.isRequired, - onClone: PropTypes.func.isRequired - }; - - state = { - placement: 'bottom' - }; - - // this dynamically changes the placement of the menu (top/bottom) to prevent it going offscreen and causing some - // unsightly shifting - setPlacement = (rowId) => { - const rowTop = document.getElementById(rowId).getBoundingClientRect().top; - const toggleHeight = 150; // the height of the menu overlay, arrow, view row - const placement = (rowTop + toggleHeight > window.innerHeight) ? 'top' : 'bottom'; - this.setState({ placement: placement }); - }; - - createItems = () => { - - // sort the indexes alphabetically - const sortedItems = this.props.items.sort(); - - return _.map(sortedItems, (indexName, index) => { - const href = FauxtonAPI.urls(this.props.urlNamespace, 'app', encodeURIComponent(this.props.database.id), encodeURIComponent(this.props.designDocName)); - const className = (this.props.selectedIndex === indexName) ? 'active' : ''; - - return ( - <li className={className} key={index}> - <a - id={this.props.designDocName + '_' + indexName} - href={"#/" + href + encodeURIComponent(indexName)} - className="toggle-view"> - {indexName} - </a> - <OverlayTrigger - trigger="click" - onEnter={this.setPlacement.bind(this, this.props.designDocName + '_' + indexName)} - placement={this.state.placement} - rootClose={true} - ref={overlay => this.itemOverlay = overlay} - 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> - ); - }); - }; - - indexAction = (action, params, e) => { - e.preventDefault(); - - this.itemOverlay.hide(); - - 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 = (e) => { - e.preventDefault(); - this.props.toggle(this.props.designDocName, this.props.title); - }; - - render() { - - // 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; - } - - let toggleClassNames = 'accordion-header index-group-header'; - let toggleBodyClassNames = 'index-list accordion-body collapse'; - if (this.props.isExpanded) { - toggleClassNames += ' down'; - toggleBodyClassNames += ' in'; - } - - const title = this.props.title; - const designDocName = this.props.designDocName; - const 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> - <Collapse in={this.props.isExpanded}> - <ul className={toggleBodyClassNames}> - {this.createItems()} - </ul> - </Collapse> - </li> - ); - } -} - -class DesignDoc extends React.Component { - static propTypes = { - database: PropTypes.object.isRequired, - sidebarListTypes: PropTypes.array.isRequired, - isExpanded: PropTypes.bool.isRequired, - selectedNavInfo: PropTypes.object.isRequired, - toggledSections: PropTypes.object.isRequired, - designDocName: PropTypes.string.isRequired - }; - - state = { - updatedSidebarListTypes: this.props.sidebarListTypes - }; - - UNSAFE_componentWillMount() { - if (_.isEmpty(this.state.updatedSidebarListTypes) || - (_.has(this.state.updatedSidebarListTypes[0], 'selector') && this.state.updatedSidebarListTypes[0].selector !== 'views')) { - - const 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 = () => { - return _.map(this.state.updatedSidebarListTypes, (index, key) => { - const expanded = _.has(this.props.toggledSections, index.name) && this.props.toggledSections[index.name]; - - // if an index in this list is selected, pass that down - let 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])} /> - ); - }); - }; - - toggle = (e) => { - e.preventDefault(); - this.props.toggle(this.props.designDocName); - }; - - getNewButtonLinks = () => { - const newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', encodeURIComponent(this.props.database.id)); - const designDocName = this.props.designDocName; - - const addNewLinks = _.reduce(FauxtonAPI.getExtensions('sidebar:links'), function (menuLinks, link) { - menuLinks.push({ - title: link.title, - url: '#' + newUrlPrefix + '/' + link.url + '/' + encodeURIComponent(designDocName), - icon: 'fonticon-plus-circled' - }); - return menuLinks; - }, [{ - title: 'New View', - url: '#' + FauxtonAPI.urls('new', 'addView', encodeURIComponent(this.props.database.id), encodeURIComponent(designDocName)), - icon: 'fonticon-plus-circled' - }]); - - return [{ - title: 'Add New', - links: addNewLinks - }]; - }; - - render () { - const buttonLinks = this.getNewButtonLinks(); - let toggleClassNames = 'design-doc-section accordion-header'; - let toggleBodyClassNames = 'design-doc-body accordion-body collapse'; - - if (this.props.isExpanded) { - toggleClassNames += ' down'; - toggleBodyClassNames += ' in'; - } - const designDocName = this.props.designDocName; - const designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.database.id, designDocName); - const 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'> - <MenuDropDown links={buttonLinks} /> - </div> - </div> - <Collapse in={this.props.isExpanded}> - <ul className={toggleBodyClassNames} id={this.props.designDocName}> - <li className={metadataRowClass}> - <a href={"#/" + designDocMetaUrl} className="toggle-view accordion-header"> - Metadata - </a> - </li> - {this.indexList()} - </ul> - </Collapse> - </li> - ); - } -} - -class DesignDocList extends React.Component { - UNSAFE_componentWillMount() { - const list = FauxtonAPI.getExtensions('sidebar:list'); - this.sidebarListTypes = _.isUndefined(list) ? [] : list; - } - - designDocList = () => { - return _.map(this.props.designDocs, (designDoc, key) => { - const ddName = decodeURIComponent(designDoc.safeId); - - // only pass down the selected nav info and toggle info if they're relevant for this particular design doc - let expanded = false, - toggledSections = {}; - if (_.has(this.props.toggledSections, ddName)) { - expanded = this.props.toggledSections[ddName].visible; - toggledSections = this.props.toggledSections[ddName].indexGroups; - } - - let 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} /> - ); - }); - }; - - render() { - return ( - <ul className="nav nav-list"> - {this.designDocList()} - </ul> - ); - } -} - -class SidebarController extends React.Component { - getStoreState = () => { - return { - database: store.getDatabase(), - selectedNav: store.getSelected(), - designDocs: store.getDesignDocs(), - designDocList: store.getDesignDocList(), - availableDesignDocIds: store.getAvailableDesignDocs(), - toggledSections: store.getToggledSections(), - isLoading: store.isLoading(), - 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() - }; - }; - - componentDidMount() { - store.on('change', this.onChange, this); - deleteDbModalStore.on('change', this.onChange, this); - } - - componentWillUnmount() { - store.off('change', this.onChange); - deleteDbModalStore.off('change', this.onChange, this); - } - - onChange = () => { - - const newState = this.getStoreState(); - // Workaround to signal Redux store that the design doc list was updated - // which is currently required by QueryOptionsContainer - // It should be removed once Sidebar components are refactored to use Redux - if (this.props.reduxUpdatedDesignDocList) { - this.props.reduxUpdatedDesignDocList(newState.designDocList); - } - - this.setState(newState); - }; - - showDeleteDatabaseModal = (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 = () => { - - // 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 - const 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 = () => { - 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 - }); - }; - - state = this.getStoreState(); - - render() { - 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> - ); - } -} - -class CloneIndexModal extends React.Component { - static propTypes = { - visible: PropTypes.bool.isRequired, - title: PropTypes.string, - close: PropTypes.func.isRequired, - submit: PropTypes.func.isRequired, - designDocArray: PropTypes.array.isRequired, - selectedDesignDoc: PropTypes.string.isRequired, - newDesignDocName: PropTypes.string.isRequired, - newIndexName: PropTypes.string.isRequired, - indexLabel: PropTypes.string.isRequired - }; - - static defaultProps = { - title: 'Clone Index', - visible: false - }; - - submit = () => { - if (!this.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 = (e) => { - if (e) { - e.preventDefault(); - } - this.props.close(); - }; - - setNewIndexName = (e) => { - Actions.setNewCloneIndexName(e.target.value); - }; - - render() { - 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={node => this.designDocSelector = node} - 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="New view name" /> - </div> - </form> - - </Modal.Body> - <Modal.Footer> - <a href="#" className="cancel-link" onClick={this.close} data-bypass="true">Cancel</a> - <button onClick={this.submit} data-bypass="true" className="btn btn-primary save"> - <i className="icon fonticon-ok-circled" /> Clone {this.props.indexLabel}</button> - </Modal.Footer> - </Modal> - ); - } -} +import CloneIndexModal from './components/CloneIndexModal'; +import DesignDoc from './components/DesignDoc'; +import SidebarController from './components/SidebarController'; export default { - SidebarController: SidebarController, - DesignDoc: DesignDoc, - CloneIndexModal: CloneIndexModal + SidebarController, + DesignDoc, + CloneIndexModal }; diff --git a/app/addons/documents/sidebar/stores.js b/app/addons/documents/sidebar/stores.js deleted file mode 100644 index 37ece86..0000000 --- a/app/addons/documents/sidebar/stores.js +++ /dev/null @@ -1,337 +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. - -import app from "../../../app"; -import FauxtonAPI from "../../../core/api"; -import React from "react"; -import ActionTypes from "./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((ddoc) => { - return '_design/' + this._deleteIndexModalDesignDocName === ddoc.id; - }); - - 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_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); - this._loading = false; - break; - - default: - return; - // do nothing - } - - this.triggerChange(); - } - -}); - -Stores.sidebarStore = new Stores.SidebarStore(); -Stores.sidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.sidebarStore.dispatch.bind(Stores.sidebarStore)); - -export default Stores; diff --git a/app/addons/permissions/layout.js b/app/addons/permissions/layout.js index 2569911..3534733 100644 --- a/app/addons/permissions/layout.js +++ b/app/addons/permissions/layout.js @@ -13,9 +13,11 @@ import React from 'react'; import {TabsSidebarHeader} from '../documents/layouts'; import PermissionsContainer from './container/PermissionsContainer'; -import SidebarComponents from "../documents/sidebar/sidebar"; +import SidebarControllerContainer from "../documents/sidebar/SidebarControllerContainer"; +import {SidebarItemSelection} from '../documents/sidebar/helpers'; export const PermissionsLayout = ({docURL, database, endpoint, dbName, dropDownLinks}) => { + const selectedNavItem = new SidebarItemSelection('permissions'); return ( <div id="dashboard" className="with-sidebar"> <TabsSidebarHeader @@ -29,7 +31,7 @@ export const PermissionsLayout = ({docURL, database, endpoint, dbName, dropDownL /> <div className="with-sidebar tabs-with-sidebar content-area"> <aside id="sidebar-content" className="scrollable"> - <SidebarComponents.SidebarController /> + <SidebarControllerContainer selectedNavItem={selectedNavItem}/> </aside> <section id="dashboard-content" className="flex-layout flex-col"> <PermissionsContainer url={endpoint} /> diff --git a/app/main.js b/app/main.js index 1176692..4089e77 100644 --- a/app/main.js +++ b/app/main.js @@ -28,6 +28,12 @@ const store = createStore( combineReducers(FauxtonAPI.reducers), applyMiddleware(...FauxtonAPI.middlewares) ); +FauxtonAPI.reduxDispatch = (action) => { + store.dispatch(action); +}; +FauxtonAPI.reduxState = () => { + return store.getState(); +}; app.addons = LoadAddons; FauxtonAPI.router = app.router = new FauxtonAPI.Router(app.addons);