Sidebar redux refactoring (#1126)
* Split components into separate files
* Refactoring to use redux
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 @@
beforeEach(() => {
notificationSpy = sinon.spy(FauxtonAPI, 'addNotification');
- sidebarSpy = sinon.stub(SidebarActions, 'updateDesignDocs');
+ sidebarSpy = sinon.stub(SidebarActions, 'dispatchUpdateDesignDocs');
});
afterEach(() => {
@@ -325,7 +325,7 @@
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 @@
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 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 @@
}
render () {
- const database = store.getDatabase();
const {
hasResults,
isListDeletable,
@@ -34,7 +31,8 @@
allDocumentsSelected,
hasSelectedItem,
toggleSelectAll,
- isLoading
+ isLoading,
+ databaseName
} = this.props;
// Determine if we need to display the bulk action selector
@@ -56,10 +54,10 @@
}
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 @@
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('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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
hideFooter: PropTypes.bool,
lowerContent: PropTypes.object,
upperContent: PropTypes.object,
+ selectedNavItem: PropTypes.object
};
export const DocsTabsSidebarLayout = ({
@@ -173,12 +175,13 @@
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 @@
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 @@
<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 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 @@
ddocName: ddoc,
designDocInfo: designDocInfo
});
-
- SidebarActions.selectNavItem('designDoc', {
+ const selectedNavItem = new SidebarItemSelection('designDoc', {
designDocName: ddoc,
designDocSection: 'metadata'
});
@@ -67,6 +66,7 @@
dbName={this.database.id}
dropDownLinks={dropDownLinks}
database={this.database}
+ selectedNavItem={selectedNavItem}
/>;
},
@@ -90,10 +90,7 @@
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 @@
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 @@
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 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 @@
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 @@
newDesignDoc: newDesignDoc
});
- SidebarActions.selectNavItem('');
-
+ const selectedNavItem = new SidebarItemSelection('');
const dropDownLinks = this.getCrumbs(this.database);
return <ViewsTabsSidebarLayout
@@ -117,6 +114,7 @@
dbName={this.database.id}
dropDownLinks={dropDownLinks}
database={this.database}
+ selectedNavItem={selectedNavItem}
/>;
},
@@ -129,11 +127,12 @@
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 @@
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 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 @@
},
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 @@
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 mapStateToProps = () => {
- return {};
+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 = ({ 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 @@
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 @@
}
};
- 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 @@
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 @@
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 @@
toggledSections={{}}
designDoc={{ customProp: { one: 'something' } }}
designDocName={'doc-$-#-.1'}
+ showDeleteIndexModal={() => {}}
+ showCloneIndexModal={() => {}}
/>);
const subOptions = el.find('.accordion-body li');
@@ -114,6 +120,8 @@
toggledSections={{}}
designDoc={{ customProp: { one: 'something' } }}
designDocName={'doc-$-#-.1'}
+ showDeleteIndexModal={() => {}}
+ showCloneIndexModal={() => {}}
/>);
const subOptions = el.find('.accordion-body li');
@@ -139,6 +147,8 @@
designDoc={{}} // note that this is empty
designDocName={'doc-$-#-.1'}
toggledSections={{}}
+ showDeleteIndexModal={() => {}}
+ showCloneIndexModal={() => {}}
/>);
const subOptions = el.find('.accordion-body li');
@@ -159,7 +169,9 @@
}}
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 @@
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
}
});
});
-}
+};
-function toggleContent (designDoc, indexGroup) {
- FauxtonAPI.dispatch({
+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
+ }
+ });
+};
+
+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 @@
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 @@
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 @@
/>
<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 @@
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);