This is an automated email from the ASF dual-hosted git repository. amaranhao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git
The following commit(s) were added to refs/heads/master by this push: new d9c4efe Update documents/index-editor to use Redux (#1151) d9c4efe is described below commit d9c4efe8e843e46c2ba7e80ae1544ac016b2845d Author: Antonio Maranhao <30349380+antonio-maran...@users.noreply.github.com> AuthorDate: Tue Nov 6 20:04:25 2018 -0500 Update documents/index-editor to use Redux (#1151) * Use redux * Update tests --- app/addons/documents/base.js | 4 +- .../index-editor/__tests__/actions.test.js | 46 ++- ...Index.components.test.js => components.test.js} | 155 +++++----- .../index-editor/__tests__/reducers.test.js | 299 ++++++++++++++++++ .../index-editor/__tests__/stores.test.js | 337 --------------------- app/addons/documents/index-editor/actions.js | 187 ++++++------ app/addons/documents/index-editor/components.js | 6 +- .../index-editor/components/DesignDocSelector.js | 8 +- .../index-editor/components/IndexEditor.js | 112 +++---- .../components/IndexEditorContainer.js | 78 +++++ .../index-editor/components/ReduceEditor.js | 62 ++-- app/addons/documents/index-editor/reducers.js | 196 ++++++++++++ app/addons/documents/index-editor/stores.js | 264 ---------------- app/addons/documents/layouts.js | 2 +- app/addons/documents/routes-index-editor.js | 20 +- 15 files changed, 849 insertions(+), 927 deletions(-) diff --git a/app/addons/documents/base.js b/app/addons/documents/base.js index cff9a82..21515eb 100644 --- a/app/addons/documents/base.js +++ b/app/addons/documents/base.js @@ -22,6 +22,7 @@ import partitionKeyReducers from "./partition-key/reducers"; import revisionBrowserReducers from './rev-browser/reducers'; import docEditorReducers from './doc-editor/reducers'; import changesReducers from './changes/reducers'; +import indexEditorReducers from './index-editor/reducers'; import "./assets/less/documents.less"; FauxtonAPI.addReducers({ @@ -32,7 +33,8 @@ FauxtonAPI.addReducers({ partitionKey: partitionKeyReducers, docEditor: docEditorReducers, changes: changesReducers, - designDocInfo: designDocInfoReducers + designDocInfo: designDocInfoReducers, + indexEditor: indexEditorReducers }); function getQueryParam (query) { diff --git a/app/addons/documents/index-editor/__tests__/actions.test.js b/app/addons/documents/index-editor/__tests__/actions.test.js index cba84f5..c01e469 100644 --- a/app/addons/documents/index-editor/__tests__/actions.test.js +++ b/app/addons/documents/index-editor/__tests__/actions.test.js @@ -10,22 +10,20 @@ // License for the specific language governing permissions and limitations under // the License. -import FauxtonAPI from "../../../../core/api"; -import Actions from "../actions"; -import Documents from "../../resources"; -import testUtils from "../../../../../test/mocha/testUtils"; -import sinon from "sinon"; -import "../../../documents/base"; -var assert = testUtils.assert; -var restore = testUtils.restore; - +import FauxtonAPI from '../../../../core/api'; +import Actions from '../actions'; +import Documents from '../../resources'; +import testUtils from '../../../../../test/mocha/testUtils'; +import sinon from 'sinon'; +import '../../../documents/base'; + +const restore = testUtils.restore; FauxtonAPI.router = new FauxtonAPI.Router([]); - describe('Index Editor Actions', function () { describe('delete view', function () { - var designDocs, database, designDoc, designDocCollection, designDocId, viewName; + let designDocs, database, designDoc, designDocCollection, designDocId, viewName; beforeEach(function () { FauxtonAPI.reduxDispatch = sinon.stub(); database = { @@ -61,7 +59,7 @@ describe('Index Editor Actions', function () { designDoc.save = function () { return FauxtonAPI.Promise.resolve(); }; - var saveSpy = sinon.spy(designDoc, 'save'); + const saveSpy = sinon.spy(designDoc, 'save'); designDocs.fetch = function () { return FauxtonAPI.Promise.resolve(); }; @@ -73,7 +71,7 @@ describe('Index Editor Actions', function () { designDoc: designDoc }); - assert.ok(saveSpy.calledOnce); + sinon.assert.calledOnce(saveSpy); }); it('deletes design doc if has no other views', function () { @@ -82,7 +80,7 @@ describe('Index Editor Actions', function () { designDoc.destroy = function () { return FauxtonAPI.Promise.resolve(); }; - var destroySpy = sinon.spy(designDoc, 'destroy'); + const destroySpy = sinon.spy(designDoc, 'destroy'); designDocs.remove = function () {}; designDocs.fetch = function () { return FauxtonAPI.Promise.resolve(); @@ -95,11 +93,11 @@ describe('Index Editor Actions', function () { designDoc: designDoc }); - assert.ok(destroySpy.calledOnce); + sinon.assert.calledOnce(destroySpy); }); it('navigates to all docs if was on view', function () { - var spy = sinon.spy(FauxtonAPI, 'navigate'); + const spy = sinon.spy(FauxtonAPI, 'navigate'); designDoc.save = function () { return FauxtonAPI.Promise.resolve(); @@ -114,8 +112,8 @@ describe('Index Editor Actions', function () { designDoc: designDoc, isOnIndex: true }).then(() => { - assert.ok(spy.getCall(0).args[0].match(/_all_docs/)); - assert.ok(spy.calledOnce); + sinon.assert.calledWithMatch(spy, /_all_docs/); + sinon.assert.calledOnce(spy); }); }); @@ -124,27 +122,27 @@ describe('Index Editor Actions', function () { const ddocModel = new Documents.Doc(ddoc, { database: database }); ddocModel.setDdocView('testview', '() => {}', '() => {}'); - assert.deepEqual(ddocModel.get('views'), { + expect(ddocModel.get('views')).toEqual({ testview: { map: '() => {}', reduce: '() => {}' } }); - assert.equal(ddocModel.get('language'), 'javascript'); + expect(ddocModel.get('language')).toBe('javascript'); }); it('removes old view only when editting', function () { const viewInfo = { - newView: false, + isNewView: false, originalDesignDocName: 'test', designDocId: 'test', originalViewName: 'foo', viewName: 'bar' }; - assert.isTrue(Actions.shouldRemoveDdocView(viewInfo)); + expect(Actions.shouldRemoveDdocView(viewInfo)).toBe(true); - viewInfo.newView = true; - assert.isFalse(Actions.shouldRemoveDdocView(viewInfo)); + viewInfo.isNewView = true; + expect(Actions.shouldRemoveDdocView(viewInfo)).toBe(false); }); }); }); diff --git a/app/addons/documents/index-editor/__tests__/viewIndex.components.test.js b/app/addons/documents/index-editor/__tests__/components.test.js similarity index 62% rename from app/addons/documents/index-editor/__tests__/viewIndex.components.test.js rename to app/addons/documents/index-editor/__tests__/components.test.js index 0c07d41..fde338c 100644 --- a/app/addons/documents/index-editor/__tests__/viewIndex.components.test.js +++ b/app/addons/documents/index-editor/__tests__/components.test.js @@ -9,81 +9,42 @@ // 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 Resources from "../../resources"; -import Views from "../components"; -import Actions from "../actions"; -import utils from "../../../../../test/mocha/testUtils"; -import React from "react"; -import ReactDOM from "react-dom"; + +import React from 'react'; import {mount} from 'enzyme'; -import sinon from "sinon"; +import sinon from 'sinon'; +import FauxtonAPI from '../../../../core/api'; +import Views from '../components'; import '../../base'; -FauxtonAPI.router = new FauxtonAPI.Router([]); - -const { assert } = utils; - - -const resetStore = (designDocs) => { - Actions.editIndex({ - database: { id: 'rockos-db' }, - newView: false, - viewName: 'test-view', - designDocs: getDesignDocsCollection(designDocs), - designDocId: designDocs[0]._id - }); -}; - -const getDesignDocsCollection = (designDocs) => { - designDocs = designDocs.map(function (doc) { - return Resources.Doc.prototype.parse(doc); - }); - - return new Resources.AllDocs(designDocs, { - params: { limit: 10 }, - database: { - safeID: () => { return 'id'; } - } - }); -}; +FauxtonAPI.router = new FauxtonAPI.Router([]); -describe('reduce editor', () => { - let reduceEl; - +describe('ReduceEditor', () => { describe('getReduceValue', () => { + const defaultProps = { + reduceOptions: [], + hasReduce: false, + hasCustomReduce: false, + reduce: null, + reduceSelectedOption: 'NONE', + updateReduceCode: () => {}, + selectReduceChanged: () => {} + }; it('returns null for none', () => { - const designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: '() => {};' - } - } - }; - - resetStore([designDoc]); - - reduceEl = mount(<Views.ReduceEditor/>); - assert.ok(_.isNull(reduceEl.instance().getReduceValue())); + const reduceEl = mount(<Views.ReduceEditor + {...defaultProps} + />); + expect(reduceEl.instance().getReduceValue()).toBeNull(); }); it('returns built in for built in reduce', () => { - const designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: '() => {};', - reduce: '_sum' - } - } - }; - - resetStore([designDoc]); - - reduceEl = mount(<Views.ReduceEditor/>); - assert.equal(reduceEl.instance().getReduceValue(), '_sum'); + const reduceEl = mount(<Views.ReduceEditor + {...defaultProps} + reduce='_sum' + hasReduce={true} + />); + expect(reduceEl.instance().getReduceValue()).toBe('_sum'); }); }); @@ -107,7 +68,7 @@ describe('DesignDocSelector component', () => { value: '_design/test-doc' } }); - assert.ok(spy.calledOnce); + sinon.assert.calledOnce(spy); }); it('shows new design doc field when set to new-doc', () => { @@ -119,7 +80,7 @@ describe('DesignDocSelector component', () => { onChangeNewDesignDocName={() => {}} />); - assert.equal(selectorEl.find('#new-ddoc-section').length, 1); + expect(selectorEl.find('#new-ddoc-section').length).toBe(1); }); it('hides new design doc field when design doc selected', () => { @@ -131,7 +92,7 @@ describe('DesignDocSelector component', () => { onChangeNewDesignDocName={() => {}} />); - assert.equal(selectorEl.find('#new-ddoc-section').length, 0); + expect(selectorEl.find('#new-ddoc-section').length).toBe(0); }); it('always passes validation when design doc selected', () => { @@ -143,7 +104,7 @@ describe('DesignDocSelector component', () => { onChangeNewDesignDocName={() => {}} />); - assert.equal(selectorEl.instance().validate(), true); + expect(selectorEl.instance().validate()).toBe(true); }); it('fails validation if new doc name entered/not entered', () => { @@ -157,7 +118,7 @@ describe('DesignDocSelector component', () => { />); // it shouldn't validate at this point: no new design doc name has been entered - assert.equal(selectorEl.instance().validate(), false); + expect(selectorEl.instance().validate()).toBe(false); }); it('passes validation if new doc name entered/not entered', () => { @@ -169,7 +130,7 @@ describe('DesignDocSelector component', () => { onSelectDesignDoc={() => { }} onChangeNewDesignDocName={() => {}} />); - assert.equal(selectorEl.instance().validate(), true); + expect(selectorEl.instance().validate()).toBe(true); }); @@ -181,7 +142,7 @@ describe('DesignDocSelector component', () => { onSelectDesignDoc={() => { }} onChangeNewDesignDocName={() => {}} />); - assert.equal(selectorEl.find('.help-link').length, 0); + expect(selectorEl.find('.help-link').length).toBe(0); }); it('includes help doc link when supplied', () => { @@ -194,27 +155,51 @@ describe('DesignDocSelector component', () => { docLink={docLink} onChangeNewDesignDocName={() => {}} />); - assert.equal(selectorEl.find('.help-link').length, 1); - assert.equal(selectorEl.find('.help-link').prop('href'), docLink); + expect(selectorEl.find('.help-link').length).toBe(1); + expect(selectorEl.find('.help-link').prop('href')).toBe(docLink); }); }); - -describe('Editor', () => { - let editorEl; - - beforeEach(() => { - editorEl = mount(<Views.EditorController />); - }); +describe('IndexEditor', () => { + const defaultProps = { + isLoading: false, + isNewView: false, + isNewDesignDoc: false, + viewName: '', + database: {}, + originalViewName: '', + originalDesignDocName: '', + designDoc: {}, + designDocId: '', + designDocList: [], + map: '', + reduce: '', + designDocs: {}, + updateNewDesignDocName: () => {}, + updateMapCode: () => {}, + selectDesignDoc: () => {}, + onChangeNewDesignDocName: () => {}, + reduceOptions: [], + reduceSelectedOption: 'NONE', + hasReduce: false, + hasCustomReduce: false, + updateReduceCode: () => {}, + selectReduceChanged: () => {} + }; it('calls changeViewName on view name change', () => { - const viewName = 'new-name'; - const spy = sinon.spy(Actions, 'changeViewName'); + const spy = sinon.spy(); + const editorEl = mount(<Views.IndexEditor + {...defaultProps} + viewName='new-name' + changeViewName={spy} + />); + editorEl.find('#index-name').simulate('change', { target: { - value: viewName + value: 'newViewName' } }); - assert.ok(spy.calledWith(viewName)); + sinon.assert.calledWith(spy, 'newViewName'); }); }); diff --git a/app/addons/documents/index-editor/__tests__/reducers.test.js b/app/addons/documents/index-editor/__tests__/reducers.test.js new file mode 100644 index 0000000..ee60b4e --- /dev/null +++ b/app/addons/documents/index-editor/__tests__/reducers.test.js @@ -0,0 +1,299 @@ +// 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 Documents from '../../../documents/resources'; +import reducer, { hasCustomReduce, getDesignDocIds } from '../reducers'; +import ActionTypes from '../actiontypes'; +import '../../base'; + +describe('IndexEditor Reducer', () => { + describe('map editor', () => { + it('new view is assigned the default map code', () => { + const action = { + type: ActionTypes.EDIT_NEW_INDEX, + options: { + isNewView: true, + //designDocs: {find: () => { return { dDocModel: () => {}}; }} + } + }; + const newState = reducer(undefined, action); + expect(newState.view.map).toBe('function (doc) {\n emit(doc._id, 1);\n}'); + }); + }); + + describe('reduce editor', () => { + describe('hasCustomReduce', () => { + it('is false for no reduce', () => { + const designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: '() => {};' + } + } + }; + const designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: () => { return 'id';} + } + }); + const action = { + type: ActionTypes.EDIT_NEW_INDEX, + options: { + isNewView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }; + const newState = reducer(undefined, action); + expect(hasCustomReduce(newState)).toBe(false); + }); + + it('is false for built in reduce', () => { + const designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: '() => {};', + reduce: '_sum' + } + } + }; + const designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: () => { return 'id';} + } + }); + const action = { + type: ActionTypes.EDIT_NEW_INDEX, + options: { + isNewView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }; + const newState = reducer(undefined, action); + expect(hasCustomReduce(newState)).toBe(false); + }); + + it('is true for custom reduce', () => { + const designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: '() => {};', + reduce: 'function (reduce) { reduce(); }' + } + } + }; + const designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: () => { return 'id';} + } + }); + const action = { + type: ActionTypes.EDIT_NEW_INDEX, + options: { + isNewView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }; + + const newState = reducer(undefined, action); + expect(hasCustomReduce(newState)).toBe(true); + }); + }); + + describe('SELECT_REDUCE_CHANGE', () => { + const designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: '() => {};' + } + } + }; + const designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: () => { return 'id';} + } + }); + const editAction = { + type: ActionTypes.EDIT_NEW_INDEX, + options: { + isNewView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }; + + it('NONE returns null reduce', () => { + let newState = reducer(undefined, editAction); + const selectReduceaction = { + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: 'NONE' + }; + newState = reducer(newState, selectReduceaction); + expect(newState.view.reduce).toBe(''); + }); + + it('builtin returns builtin reduce', () => { + let newState = reducer(undefined, editAction); + const selectReduceAction = { + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: '_sum' + }; + newState = reducer(newState, selectReduceAction); + expect(newState.view.reduce).toBe('_sum'); + }); + + it('custom returns custom reduce', () => { + let newState = reducer(undefined, editAction); + const selectReduceAction = { + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: 'CUSTOM' + }; + newState = reducer(newState, selectReduceAction); + expect(newState.view.reduce).toBe('function (keys, values, rereduce) {\n if (rereduce) {\n return sum(values);\n } else {\n return values.length;\n }\n}'); + }); + }); + }); + + describe('design doc selector', () => { + const designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'boom' + } + } + }; + + const mangoDoc = { + "_id": "_design/123mango", + "id": "_design/123mango", + "key": "_design/123mango", + "value": { + "rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80" + }, + "doc": { + "_id": "_design/123mango", + "_rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80", + "views": { + "test-view": { + "map": "function(doc) {\n emit(doc._id, 2);\n}" + }, + "new-view": { + "map": "function(doc) {\n if (doc.class === \"mammal\" && doc.diet === \"herbivore\")\n emit(doc._id, 1);\n}", + "reduce": "_sum" + } + }, + "language": "query", + "indexes": { + "newSearch": { + "analyzer": "standard", + "index": "function(doc){\n index(\"default\", doc._id);\n}" + } + } + } + }; + + const designDocArray = _.map([designDoc, mangoDoc], (doc) => { + return Documents.Doc.prototype.parse(doc); + }); + + const designDocs = new Documents.AllDocs(designDocArray, { + params: { limit: 10 }, + database: { + safeID: () => { return 'id'; } + } + }); + + const editAction = { + type: ActionTypes.EDIT_INDEX, + options: { + isNewView: false, + viewName: 'test-view', + designDocs: designDocs, + designDocId: designDoc._id + } + }; + + it('DESIGN_DOC_CHANGE changes design doc id', () => { + let newState = reducer(undefined, editAction); + const designDocId = 'another-one'; + const ddocChangeAction = { + type: ActionTypes.DESIGN_DOC_CHANGE, + options: { + value: designDocId + } + }; + newState = reducer(newState, ddocChangeAction); + expect(newState.designDocId).toBe(designDocId); + }); + + it('only filters mango docs', () => { + const newState = reducer(undefined, editAction); + const designDocs = getDesignDocIds(newState); + + expect(designDocs.length).toBe(1); + expect(designDocs[0]).toBe('_design/test-doc'); + }); + }); + + describe('EDIT_INDEX', () => { + const designDoc = { + _id: '_design/test-doc', + views: { + 'test-view': { + map: 'boom' + } + } + }; + const designDocs = new Documents.AllDocs([designDoc], { + params: { limit: 10 }, + database: { + safeID: () => { return 'id';} + } + }); + + it('can set reduce for new design doc', () => { + const editAction = { + type: ActionTypes.EDIT_INDEX, + options: { + isNewView: true, + isNewDesignDoc: true, + viewName: 'test-view', + designDocs: designDocs, + designDocId: undefined + } + }; + let newState = reducer(undefined, editAction); + + const selectReduceAction = { + type: ActionTypes.SELECT_REDUCE_CHANGE, + reduceSelectedOption: '_sum' + }; + newState = reducer(newState, selectReduceAction); + expect(newState.view.reduce).toBe('_sum'); + }); + }); +}); diff --git a/app/addons/documents/index-editor/__tests__/stores.test.js b/app/addons/documents/index-editor/__tests__/stores.test.js deleted file mode 100644 index 2e1e6e7..0000000 --- a/app/addons/documents/index-editor/__tests__/stores.test.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 FauxtonAPI from "../../../../core/api"; -import Stores from "../stores"; -import ActionTypes from "../actiontypes"; -import Documents from "../../../documents/resources"; -import testUtils from "../../../../../test/mocha/testUtils"; -import '../../base'; -const assert = testUtils.assert; -let store; -let dispatchToken; - -describe('IndexEditorStore', () => { - - beforeEach(() => { - store = new Stores.IndexEditorStore(); - dispatchToken = FauxtonAPI.dispatcher.register(store.dispatch.bind(store)); - }); - - afterEach(() => { - FauxtonAPI.dispatcher.unregister(dispatchToken); - }); - - describe('map editor', () => { - - describe('new view', () => { - - beforeEach(() => { - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_NEW_INDEX, - options: { - newView: true - } - }); - }); - - it('returns default map', () => { - assert.equal(store.getMap(), 'function (doc) {\n emit(doc._id, 1);\n}'); - }); - }); - - }); - - describe('reduce editor', () => { - describe('has custom reduce', () => { - it('is false for no reduce', () => { - const designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: '() => {};' - } - } - }; - - const designDocs = new Documents.AllDocs([designDoc], { - params: { limit: 10 }, - database: { - safeID: () => { return 'id';} - } - }); - - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_NEW_INDEX, - options: { - newView: false, - viewName: 'test-view', - designDocs: designDocs, - designDocId: designDoc._id - } - }); - - assert.notOk(store.hasCustomReduce()); - }); - - it('is false for built in reduce', () => { - const designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: '() => {};', - reduce: '_sum' - } - } - }; - - const designDocs = new Documents.AllDocs([designDoc], { - params: { limit: 10 }, - database: { - safeID: () => { return 'id';} - } - }); - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_NEW_INDEX, - options: { - newView: false, - viewName: 'test-view', - designDocs: designDocs, - designDocId: designDoc._id - } - }); - - assert.notOk(store.hasCustomReduce()); - }); - - it('is true for custom reduce', () => { - const designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: '() => {};', - reduce: 'function (reduce) { reduce(); }' - } - } - }; - - const designDocs = new Documents.AllDocs([designDoc], { - params: { limit: 10 }, - database: { - safeID: () => { return 'id';} - } - }); - - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_NEW_INDEX, - options: { - newView: false, - viewName: 'test-view', - designDocs: designDocs, - designDocId: designDoc._id - } - }); - - assert.ok(store.hasCustomReduce()); - }); - - }); - - //show default reduce - describe('SELECT_REDUCE_CHANGE', () => { - - beforeEach(() => { - const designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: '() => {};' - } - } - }; - - const designDocs = new Documents.AllDocs([designDoc], { - params: { limit: 10 }, - database: { - safeID: () => { return 'id';} - } - }); - - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_NEW_INDEX, - options: { - newView: false, - viewName: 'test-view', - designDocs: designDocs, - designDocId: designDoc._id - } - }); - }); - - it('NONE returns null reduce', () => { - FauxtonAPI.dispatch({ - type: ActionTypes.SELECT_REDUCE_CHANGE, - reduceSelectedOption: 'NONE' - }); - assert.ok(_.isNull(store.getReduce())); - }); - - it('builtin returns bultin reduce', () => { - FauxtonAPI.dispatch({ - type: ActionTypes.SELECT_REDUCE_CHANGE, - reduceSelectedOption: '_sum' - }); - assert.equal(store.getReduce(), '_sum'); - }); - - it('custom returns custom reduce', () => { - FauxtonAPI.dispatch({ - type: ActionTypes.SELECT_REDUCE_CHANGE, - reduceSelectedOption: 'CUSTOM' - }); - assert.equal(store.getReduce(), 'function (keys, values, rereduce) {\n if (rereduce) {\n return sum(values);\n } else {\n return values.length;\n }\n}'); - }); - }); - }); - - - describe('design doc selector', () => { - let designDoc; - - beforeEach(() => { - designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: 'boom' - } - } - }; - - const mangoDoc = { - "_id": "_design/123mango", - "id": "_design/123mango", - "key": "_design/123mango", - "value": { - "rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80" - }, - "doc": { - "_id": "_design/123mango", - "_rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80", - "views": { - "test-view": { - "map": "function(doc) {\n emit(doc._id, 2);\n}" - }, - "new-view": { - "map": "function(doc) {\n if (doc.class === \"mammal\" && doc.diet === \"herbivore\")\n emit(doc._id, 1);\n}", - "reduce": "_sum" - } - }, - "language": "query", - "indexes": { - "newSearch": { - "analyzer": "standard", - "index": "function(doc){\n index(\"default\", doc._id);\n}" - } - } - } - }; - - const designDocArray = _.map([designDoc, mangoDoc], (doc) => { - return Documents.Doc.prototype.parse(doc); - }); - - var designDocs = new Documents.AllDocs(designDocArray, { - params: { limit: 10 }, - database: { - safeID: () => { return 'id'; } - } - }); - - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_INDEX, - options: { - newView: false, - viewName: 'test-view', - designDocs: designDocs, - designDocId: designDoc._id - } - }); - }); - - afterEach(() => { - store.reset(); - }); - - it('DESIGN_DOC_CHANGE changes design doc id', () => { - const designDocId = 'another-one'; - FauxtonAPI.dispatch({ - type: ActionTypes.DESIGN_DOC_CHANGE, - options: { - value: designDocId - } - }); - assert.equal(store.getDesignDocId(), designDocId); - }); - - it('only filters mango docs', () => { - const designDocs = store.getAvailableDesignDocs(); - assert.equal(designDocs.length, 1); - assert.equal(designDocs[0], '_design/test-doc'); - }); - }); - - describe('EDIT_INDEX', () => { - let designDoc, designDocs; - - beforeEach(() => { - designDoc = { - _id: '_design/test-doc', - views: { - 'test-view': { - map: 'boom' - } - } - }; - - designDocs = new Documents.AllDocs([designDoc], { - params: { limit: 10 }, - database: { - safeID: () => { return 'id';} - } - }); - - }); - - it('can set reduce for new design doc', () => { - FauxtonAPI.dispatch({ - type: ActionTypes.EDIT_INDEX, - options: { - newView: true, - newDesignDoc: true, - viewName: 'test-view', - designDocs: designDocs, - designDocId: undefined - } - }); - - FauxtonAPI.dispatch({ - type: ActionTypes.SELECT_REDUCE_CHANGE, - reduceSelectedOption: '_sum' - }); - - assert.equal(store.getReduce(), '_sum'); - }); - - }); - -}); diff --git a/app/addons/documents/index-editor/actions.js b/app/addons/documents/index-editor/actions.js index c25fa99..ad261e0 100644 --- a/app/addons/documents/index-editor/actions.js +++ b/app/addons/documents/index-editor/actions.js @@ -10,40 +10,40 @@ // License for the specific language governing permissions and limitations under // the License. -import app from "../../../app"; -import FauxtonAPI from "../../../core/api"; -import Documents from "../resources"; -import ActionTypes from "./actiontypes"; -import SidebarActions from "../sidebar/actions"; - -function selectReduceChanged (reduceOption) { - FauxtonAPI.dispatch({ +import app from '../../../app'; +import FauxtonAPI from '../../../core/api'; +import Documents from '../resources'; +import ActionTypes from './actiontypes'; +import SidebarActions from '../sidebar/actions'; + +const selectReduceChanged = (reduceOption) => (dispatch) => { + dispatch({ type: ActionTypes.SELECT_REDUCE_CHANGE, reduceSelectedOption: reduceOption }); -} +}; -function changeViewName (name) { - FauxtonAPI.dispatch({ +const changeViewName = (name) => (dispatch) => { + dispatch({ type: ActionTypes.VIEW_NAME_CHANGE, name: name }); -} +}; -function editIndex (options) { - FauxtonAPI.dispatch({ +const editIndex = (options) => (dispatch) => { + dispatch({ type: ActionTypes.EDIT_INDEX, options: options }); -} +}; -function clearIndex () { - FauxtonAPI.dispatch({ type: ActionTypes.CLEAR_INDEX }); -} +const dispatchClearIndex = () => { + FauxtonAPI.reduxDispatch({ type: ActionTypes.CLEAR_INDEX }); +}; -function fetchDesignDocsBeforeEdit (options) { +const dispatchFetchDesignDocsBeforeEdit = (options) => { options.designDocs.fetch({reset: true}).then(() => { - this.editIndex(options); + editIndex(options)(FauxtonAPI.reduxDispatch); }, xhr => { let errorMsg = 'Error'; if (xhr.responseJSON && xhr.responseJSON.error === 'not_found') { @@ -57,16 +57,16 @@ function fetchDesignDocsBeforeEdit (options) { clear: true }); }); -} +}; -function shouldRemoveDdocView(viewInfo) { - return !viewInfo.newView && +const shouldRemoveDdocView = (viewInfo) => { + return !viewInfo.isNewView && viewInfo.originalDesignDocName === viewInfo.designDocId && viewInfo.originalViewName !== viewInfo.viewName; -} +}; -function saveView (viewInfo) { - var designDoc = viewInfo.designDoc; +const saveView = (viewInfo) => (dispatch) => { + const designDoc = viewInfo.designDoc; designDoc.setDdocView(viewInfo.viewName, viewInfo.map, viewInfo.reduce); FauxtonAPI.addNotification({ @@ -89,8 +89,8 @@ function saveView (viewInfo) { // if the user just saved an existing view to a different design doc, remove the view // from the old design doc and delete if it's empty - if (!viewInfo.newView && viewInfo.originalDesignDocName !== viewInfo.designDocId) { - var oldDesignDoc = findDesignDoc(viewInfo.designDocs, viewInfo.originalDesignDocName); + if (!viewInfo.isNewView && viewInfo.originalDesignDocName !== viewInfo.designDocId) { + const oldDesignDoc = findDesignDoc(viewInfo.designDocs, viewInfo.originalDesignDocName); safeDeleteIndex(oldDesignDoc, viewInfo.designDocs, 'views', viewInfo.originalViewName, { onSuccess: function () { SidebarActions.dispatchUpdateDesignDocs(viewInfo.designDocs); @@ -102,8 +102,8 @@ function saveView (viewInfo) { 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)); + dispatch({ type: ActionTypes.VIEW_SAVED }); + const fragment = FauxtonAPI.urls('view', 'showView', viewInfo.database.safeID(), designDoc.safeID(), app.utils.safeURLName(viewInfo.viewName)); FauxtonAPI.navigate(fragment, { trigger: true }); }, (xhr) => { FauxtonAPI.addNotification({ @@ -112,16 +112,16 @@ function saveView (viewInfo) { clear: true }); }); -} +}; -function addDesignDoc (designDoc) { - FauxtonAPI.dispatch({ +const addDesignDoc = (designDoc) => (dispatch) => { + dispatch({ type: ActionTypes.VIEW_ADD_DESIGN_DOC, designDoc: designDoc.toJSON() }); -} +}; -function deleteView (options) { +const deleteView = (options) => { function onSuccess () { @@ -143,11 +143,11 @@ function deleteView (options) { } return safeDeleteIndex(options.designDoc, options.designDocs, 'views', options.indexName, { onSuccess: onSuccess }); -} +}; -function cloneView (params) { - var targetDesignDoc = getDesignDoc(params.designDocs, params.targetDesignDocName, params.newDesignDocName, params.database); - var indexes = targetDesignDoc.get('views'); +const cloneView = (params) => { + const targetDesignDoc = getDesignDoc(params.designDocs, params.targetDesignDocName, params.newDesignDocName, params.database); + let indexes = targetDesignDoc.get('views'); if (indexes && _.has(indexes, params.newIndexName)) { FauxtonAPI.addNotification({ msg: 'That index name is already used in this design doc. Please enter a new name.', @@ -159,14 +159,14 @@ function cloneView (params) { if (!indexes) { indexes = {}; } - var sourceDesignDoc = findDesignDoc(params.designDocs, '_design/' + params.sourceDesignDocName); - var sourceDesignDocJSON = sourceDesignDoc.toJSON(); + const sourceDesignDoc = findDesignDoc(params.designDocs, '_design/' + params.sourceDesignDocName); + const sourceDesignDocJSON = sourceDesignDoc.toJSON(); // this sets whatever content is in the source index into the target design doc under the new index name indexes[params.newIndexName] = sourceDesignDocJSON.views[params.sourceIndexName]; targetDesignDoc.set({ views: indexes }); - targetDesignDoc.save().then(function () { + targetDesignDoc.save().then(() => { params.onComplete(); FauxtonAPI.addNotification({ msg: 'The index has been cloned.', @@ -174,63 +174,62 @@ function cloneView (params) { clear: true }); SidebarActions.dispatchUpdateDesignDocs(params.designDocs); - }, - function (xhr) { + }, (xhr) => { params.onComplete(); - var responseText = JSON.parse(xhr.responseText).reason; + const responseText = JSON.parse(xhr.responseText).reason; FauxtonAPI.addNotification({ msg: 'Clone failed: ' + responseText, type: 'error', clear: true }); }); -} +}; -function gotoEditViewPage (databaseName, partitionKey, designDocName, indexName) { +const gotoEditViewPage = (databaseName, partitionKey, designDocName, indexName) => { FauxtonAPI.navigate('#' + FauxtonAPI.urls('view', 'edit', encodeURIComponent(databaseName), (partitionKey ? encodeURIComponent(partitionKey) : ''), encodeURIComponent(designDocName), encodeURIComponent(indexName))); -} +}; -function updateMapCode (code) { - FauxtonAPI.dispatch({ +const updateMapCode = (code) => (dispatch) => { + dispatch({ type: ActionTypes.VIEW_UPDATE_MAP_CODE, code: code }); -} +}; -function updateReduceCode (code) { - FauxtonAPI.dispatch({ +const updateReduceCode = (code) => (dispatch) => { + dispatch({ type: ActionTypes.VIEW_UPDATE_REDUCE_CODE, code: code }); -} +}; -function selectDesignDoc (designDoc) { - FauxtonAPI.dispatch({ +const selectDesignDoc = (designDoc) => (dispatch) => { + dispatch({ type: ActionTypes.DESIGN_DOC_CHANGE, options: { value: designDoc } }); -} +}; -function updateNewDesignDocName (designDocName) { - FauxtonAPI.dispatch({ +const updateNewDesignDocName = (designDocName) => (dispatch) => { + dispatch({ type: ActionTypes.DESIGN_DOC_NEW_NAME_UPDATED, options: { value: designDocName } }); -} +}; // safely deletes an index of any type. It only deletes the actual design doc if there are no // other indexes of any type left in the doc -function safeDeleteIndex (designDoc, designDocs, indexPropName, indexName, options) { - var opts = _.extend({ +const safeDeleteIndex = (designDoc, designDocs, indexPropName, indexName, options) => { + const opts = _.extend({ onSuccess: function () { }, onError: function (xhr) { - var responseText = JSON.parse(xhr.responseText).reason; + const responseText = JSON.parse(xhr.responseText).reason; FauxtonAPI.addNotification({ msg: 'Delete failed: ' + responseText, type: 'error', @@ -239,21 +238,21 @@ function safeDeleteIndex (designDoc, designDocs, indexPropName, indexName, optio } }, options); - var indexes = designDoc.get(indexPropName) || {}; + const indexes = designDoc.get(indexPropName) || {}; delete indexes[indexName]; - var newIndexes = {}; + const newIndexes = {}; newIndexes[indexPropName] = indexes; designDoc.set(newIndexes); // we either save the design doc with the now-removed index, or we remove it altogether if there are no indexes // of any type left in the design doc - var indexTypePropNames = FauxtonAPI.getIndexTypePropNames(); - var hasRemainingIndex = _.some(indexTypePropNames, function (propName) { + const indexTypePropNames = FauxtonAPI.getIndexTypePropNames(); + const hasRemainingIndex = _.some(indexTypePropNames, function (propName) { return designDoc.get(propName) && _.keys(designDoc.get(propName)).length > 0; }); - var promise; - var deleteDesignDoc = false; + let promise; + let deleteDesignDoc = false; if (hasRemainingIndex) { promise = designDoc.save(); } else { @@ -266,21 +265,21 @@ function safeDeleteIndex (designDoc, designDocs, indexPropName, indexName, optio } opts.onSuccess(); }, opts.onError); -} +}; // ---- helpers ---- -function findDesignDoc (designDocs, designDocName) { +const findDesignDoc = (designDocs, designDocName) => { return designDocs.find(function (doc) { return doc.id === designDocName; }).dDocModel(); -} +}; -function getDesignDoc (designDocs, targetDesignDocName, newDesignDocName, database) { +const getDesignDoc = (designDocs, targetDesignDocName, newDesignDocName, database) => { if (targetDesignDocName === 'new-doc') { - var doc = { + const doc = { "_id": "_design/" + newDesignDocName, "views": {}, "language": "javascript" @@ -288,32 +287,32 @@ function getDesignDoc (designDocs, targetDesignDocName, newDesignDocName, databa return new Documents.Doc(doc, { database: database }); } - var foundDoc = designDocs.find(function (ddoc) { + const foundDoc = designDocs.find(function (ddoc) { return ddoc.id === targetDesignDocName; }); return (!foundDoc) ? null : foundDoc.dDocModel(); -} +}; export default { helpers: { - findDesignDoc: findDesignDoc, - getDesignDoc: getDesignDoc + findDesignDoc, + getDesignDoc }, - safeDeleteIndex: safeDeleteIndex, - selectReduceChanged: selectReduceChanged, - changeViewName: changeViewName, - editIndex: editIndex, - clearIndex: clearIndex, - fetchDesignDocsBeforeEdit: fetchDesignDocsBeforeEdit, - shouldRemoveDdocView: shouldRemoveDdocView, - saveView: saveView, - addDesignDoc: addDesignDoc, - deleteView: deleteView, - cloneView: cloneView, - gotoEditViewPage: gotoEditViewPage, - updateMapCode: updateMapCode, - updateReduceCode: updateReduceCode, - selectDesignDoc: selectDesignDoc, - updateNewDesignDocName: updateNewDesignDocName + safeDeleteIndex, + selectReduceChanged, + changeViewName, + editIndex, + dispatchClearIndex, + dispatchFetchDesignDocsBeforeEdit, + shouldRemoveDdocView, + saveView, + addDesignDoc, + deleteView, + cloneView, + gotoEditViewPage, + updateMapCode, + updateReduceCode, + selectDesignDoc, + updateNewDesignDocName }; diff --git a/app/addons/documents/index-editor/components.js b/app/addons/documents/index-editor/components.js index a96fab6..3cf69bd 100644 --- a/app/addons/documents/index-editor/components.js +++ b/app/addons/documents/index-editor/components.js @@ -10,15 +10,17 @@ // License for the specific language governing permissions and limitations under // the License. -import ReactComponents from "../../components/react-components"; +import ReactComponents from '../../components/react-components'; import DesignDocSelector from './components/DesignDocSelector'; import IndexEditor from './components/IndexEditor'; +import IndexEditorContainer from './components/IndexEditorContainer'; import ReduceEditor from './components/ReduceEditor'; const StyledSelect = ReactComponents.StyledSelect; export default { - EditorController: IndexEditor, + IndexEditorContainer, + IndexEditor, ReduceEditor, DesignDocSelector, StyledSelect diff --git a/app/addons/documents/index-editor/components/DesignDocSelector.js b/app/addons/documents/index-editor/components/DesignDocSelector.js index 956978f..57c6bbc 100644 --- a/app/addons/documents/index-editor/components/DesignDocSelector.js +++ b/app/addons/documents/index-editor/components/DesignDocSelector.js @@ -11,11 +11,9 @@ // the License. import PropTypes from 'prop-types'; - -import React, { Component } from "react"; -import ReactDOM from "react-dom"; -import FauxtonAPI from "../../../../core/api"; -import ReactComponents from "../../../components/react-components"; +import React, { Component } from 'react'; +import FauxtonAPI from '../../../../core/api'; +import ReactComponents from '../../../components/react-components'; const { StyledSelect } = ReactComponents; diff --git a/app/addons/documents/index-editor/components/IndexEditor.js b/app/addons/documents/index-editor/components/IndexEditor.js index 884858d..332b749 100644 --- a/app/addons/documents/index-editor/components/IndexEditor.js +++ b/app/addons/documents/index-editor/components/IndexEditor.js @@ -10,61 +10,27 @@ // License for the specific language governing permissions and limitations under // the License. -import React, { Component } from "react"; -import ReactDOM from "react-dom"; -import app from "../../../../app"; -import FauxtonAPI from "../../../../core/api"; -import ReactComponents from "../../../components/react-components"; -import Stores from "../stores"; -import Actions from "../actions"; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import app from '../../../../app'; +import FauxtonAPI from '../../../../core/api'; +import ReactComponents from '../../../components/react-components'; import DesignDocSelector from './DesignDocSelector'; import ReduceEditor from './ReduceEditor'; const getDocUrl = app.helpers.getDocUrl; -const store = Stores.indexEditorStore; const {CodeEditorPanel, ConfirmButton, LoadLines} = ReactComponents; export default class IndexEditor extends Component { constructor(props) { super(props); - this.state = this.getStoreState(); - } - - getStoreState() { - return { - database: store.getDatabase(), - isNewView: store.isNewView(), - viewName: store.getViewName(), - designDocs: store.getDesignDocs(), - designDocList: store.getAvailableDesignDocs(), - originalViewName: store.getOriginalViewName(), - originalDesignDocName: store.getOriginalDesignDocName(), - newDesignDoc: store.isNewDesignDoc(), - designDocId: store.getDesignDocId(), - newDesignDocName: store.getNewDesignDocName(), - saveDesignDoc: store.getSaveDesignDoc(), - map: store.getMap(), - isLoading: store.isLoading() - }; - } - - onChange() { - this.setState(this.getStoreState()); - } - - componentDidMount() { - store.on('change', this.onChange, this); - } - - componentWillUnmount() { - store.off('change', this.onChange); } // the code editor is a standalone component, so if the user goes from one edit view page to another, we need to // force an update of the editor panel - componentDidUpdate(prevProps, prevState) { - if (this.state.map !== prevState.map && this.mapEditor) { + componentDidUpdate(prevProps) { + if (this.props.map !== prevProps.map && this.mapEditor) { this.mapEditor.update(); } } @@ -76,31 +42,31 @@ export default class IndexEditor extends Component { return; } - Actions.saveView({ - database: this.state.database, - newView: this.state.isNewView, - viewName: this.state.viewName, - designDoc: this.state.saveDesignDoc, - designDocId: this.state.designDocId, - newDesignDoc: this.state.newDesignDoc, - originalViewName: this.state.originalViewName, - originalDesignDocName: this.state.originalDesignDocName, + this.props.saveView({ + database: this.props.database, + isNewView: this.props.isNewView, + viewName: this.props.viewName, + designDoc: this.props.saveDesignDoc, + designDocId: this.props.designDocId, + isNewDesignDoc: this.props.isNewDesignDoc, + originalViewName: this.props.originalViewName, + originalDesignDocName: this.props.originalDesignDocName, map: this.mapEditor.getValue(), reduce: this.reduceEditor.getReduceValue(), - designDocs: this.state.designDocs + designDocs: this.props.designDocs }); } viewChange(el) { - Actions.changeViewName(el.target.value); + this.props.changeViewName(el.target.value); } updateMapCode(code) { - Actions.updateMapCode(code); + this.props.updateMapCode(code); } render() { - if (this.state.isLoading) { + if (this.props.isLoading) { return ( <div className="define-view"> <LoadLines /> @@ -108,9 +74,9 @@ export default class IndexEditor extends Component { ); } - const pageHeader = (this.state.isNewView) ? 'New View' : 'Edit View'; - const btnLabel = (this.state.isNewView) ? 'Create Document and then Build Index' : 'Save Document and then Build Index'; - const cancelLink = '#' + FauxtonAPI.urls('view', 'showView', this.state.database.id, this.state.designDocId, this.state.viewName); + const pageHeader = (this.props.isNewView) ? 'New View' : 'Edit View'; + const btnLabel = (this.props.isNewView) ? 'Create Document and then Build Index' : 'Save Document and then Build Index'; + const cancelLink = '#' + FauxtonAPI.urls('view', 'showView', this.props.database.id, this.props.designDocId, this.props.viewName); return ( <div className="define-view" > <form className="form-horizontal view-query-save" onSubmit={this.saveView.bind(this)}> @@ -119,11 +85,11 @@ export default class IndexEditor extends Component { <div className="new-ddoc-section"> <DesignDocSelector ref={(el) => { this.designDocSelector = el; }} - designDocList={this.state.designDocList} - selectedDesignDocName={this.state.designDocId} - newDesignDocName={this.state.newDesignDocName} - onSelectDesignDoc={Actions.selectDesignDoc} - onChangeNewDesignDocName={Actions.updateNewDesignDocName} + designDocList={this.props.designDocList} + selectedDesignDocName={this.props.designDocId} + newDesignDocName={this.props.newDesignDocName} + onSelectDesignDoc={this.props.selectDesignDoc} + onChangeNewDesignDocName={this.props.updateNewDesignDocName} docLink={getDocUrl('DESIGN_DOCS')} /> </div> @@ -142,7 +108,7 @@ export default class IndexEditor extends Component { <input type="text" id="index-name" - value={this.state.viewName} + value={this.props.viewName} onChange={this.viewChange.bind(this)} placeholder="Index name" /> </div> @@ -153,8 +119,8 @@ export default class IndexEditor extends Component { docLink={getDocUrl('MAP_FUNCS')} blur={this.updateMapCode.bind(this)} allowZenMode={false} - defaultCode={this.state.map} /> - <ReduceEditor ref={(el) => { this.reduceEditor = el; }} /> + defaultCode={this.props.map} /> + <ReduceEditor ref={(el) => { this.reduceEditor = el; }} {...this.props} /> <div className="padded-box"> <div className="control-group"> <ConfirmButton id="save-view" text={btnLabel} /> @@ -166,3 +132,19 @@ export default class IndexEditor extends Component { ); } } + +IndexEditor.propTypes = { + isLoading:PropTypes.bool.isRequired, + isNewView: PropTypes.bool.isRequired, + database: PropTypes.object.isRequired, + designDocId: PropTypes.string.isRequired, + viewName: PropTypes.string.isRequired, + isNewDesignDoc: PropTypes.bool.isRequired, + originalViewName: PropTypes.string, + originalDesignDocName: PropTypes.string, + designDocs: PropTypes.object, + saveDesignDoc: PropTypes.object, + updateNewDesignDocName: PropTypes.func.isRequired, + changeViewName: PropTypes.func.isRequired, + updateMapCode: PropTypes.func.isRequired +}; diff --git a/app/addons/documents/index-editor/components/IndexEditorContainer.js b/app/addons/documents/index-editor/components/IndexEditorContainer.js new file mode 100644 index 0000000..445b029 --- /dev/null +++ b/app/addons/documents/index-editor/components/IndexEditorContainer.js @@ -0,0 +1,78 @@ +// 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 { connect } from 'react-redux'; +import IndexEditor from './IndexEditor'; +import Actions from '../actions'; +import { getSaveDesignDoc, getDesignDocIds, reduceSelectedOption, hasCustomReduce } from '../reducers'; + +const mapStateToProps = ({ indexEditor }) => { + return { + database: indexEditor.database, + isNewView: indexEditor.isNewView, + viewName: indexEditor.viewName, + designDocs: indexEditor.designDocs, + designDocList: getDesignDocIds(indexEditor), + originalViewName: indexEditor.originalViewName, + originalDesignDocName: indexEditor.originalDesignDocName, + isNewDesignDoc: indexEditor.isNewDesignDoc, + designDocId: indexEditor.designDocId, + newDesignDocName: indexEditor.newDesignDocName, + saveDesignDoc: getSaveDesignDoc(indexEditor), + map: indexEditor.view.map, + isLoading: indexEditor.isLoading, + reduce: indexEditor.view.reduce, + reduceOptions: indexEditor.reduceOptions, + reduceSelectedOption: reduceSelectedOption(indexEditor), + hasCustomReduce: hasCustomReduce(indexEditor), + hasReduce: !!indexEditor.view.reduce + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + saveView: (viewInfo) => { + dispatch(Actions.saveView(viewInfo)); + }, + + changeViewName: (name) => { + dispatch(Actions.changeViewName(name)); + }, + + updateMapCode: (code) => { + dispatch(Actions.updateMapCode(code)); + }, + + selectDesignDoc: (designDoc) => { + dispatch(Actions.selectDesignDoc(designDoc)); + }, + + updateNewDesignDocName: (designDocName) => { + dispatch(Actions.updateNewDesignDocName(designDocName)); + }, + + updateReduceCode: (code) => { + dispatch(Actions.updateReduceCode(code)); + }, + + selectReduceChanged: (reduceOption) => { + dispatch(Actions.selectReduceChanged(reduceOption)); + } + }; +}; + +const IndexEditorContainer = connect( + mapStateToProps, + mapDispatchToProps +)(IndexEditor); + +export default IndexEditorContainer; diff --git a/app/addons/documents/index-editor/components/ReduceEditor.js b/app/addons/documents/index-editor/components/ReduceEditor.js index 7e0b181..7f1df52 100644 --- a/app/addons/documents/index-editor/components/ReduceEditor.js +++ b/app/addons/documents/index-editor/components/ReduceEditor.js @@ -10,59 +10,33 @@ // License for the specific language governing permissions and limitations under // the License. -import React, { Component } from "react"; -import ReactDOM from "react-dom"; -import app from "../../../../app"; -import ReactComponents from "../../../components/react-components"; -import Stores from "../stores"; -import Actions from "../actions"; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import app from '../../../../app'; +import ReactComponents from '../../../components/react-components'; const getDocUrl = app.helpers.getDocUrl; -const store = Stores.indexEditorStore; const {CodeEditorPanel, StyledSelect} = ReactComponents; export default class ReduceEditor extends Component { constructor(props) { super(props); - this.state = this.getStoreState(); - } - - onChange() { - this.setState(this.getStoreState()); - } - - componentDidMount() { - store.on('change', this.onChange, this); - } - - componentWillUnmount() { - store.off('change', this.onChange); - } - - getStoreState() { - return { - reduce: store.getReduce(), - reduceOptions: store.reduceOptions(), - reduceSelectedOption: store.reduceSelectedOption(), - hasCustomReduce: store.hasCustomReduce(), - hasReduce: store.hasReduce() - }; } getOptionsList() { - return _.map(this.state.reduceOptions, (reduce, i) => { + return this.props.reduceOptions.map((reduce, i) => { return <option key={i} value={reduce}>{reduce}</option>; }); } getReduceValue() { - if (!this.state.hasReduce) { + if (!this.props.hasReduce) { return null; } - if (!this.state.hasCustomReduce) { - return this.state.reduce; + if (!this.props.hasCustomReduce) { + return this.props.reduce; } return this.reduceEditor.getValue(); @@ -73,23 +47,23 @@ export default class ReduceEditor extends Component { } updateReduceCode(code) { - Actions.updateReduceCode(code); + this.props.updateReduceCode(code); } selectChange(event) { - Actions.selectReduceChanged(event.target.value); + this.props.selectReduceChanged(event.target.value); } render() { const reduceOptions = this.getOptionsList(); let customReduceSection; - if (this.state.hasCustomReduce) { + if (this.props.hasCustomReduce) { customReduceSection = <CodeEditorPanel ref={node => this.reduceEditor = node} id='reduce-function' title={'Custom Reduce function'} - defaultCode={this.state.reduce} + defaultCode={this.props.reduce} allowZenMode={false} blur={this.updateReduceCode.bind(this)} />; @@ -114,10 +88,20 @@ export default class ReduceEditor extends Component { selectContent={reduceOptions} selectChange={this.selectChange.bind(this)} selectId="reduce-function-selector" - selectValue={this.state.reduceSelectedOption} /> + selectValue={this.props.reduceSelectedOption} /> </div> {customReduceSection} </div> ); } } + +ReduceEditor.propTypes = { + reduceOptions: PropTypes.array.isRequired, + hasReduce: PropTypes.bool.isRequired, + hasCustomReduce: PropTypes.bool.isRequired, + reduce: PropTypes.string, + reduceSelectedOption: PropTypes.string.isRequired, + updateReduceCode: PropTypes.func.isRequired, + selectReduceChanged: PropTypes.func.isRequired +}; diff --git a/app/addons/documents/index-editor/reducers.js b/app/addons/documents/index-editor/reducers.js new file mode 100644 index 0000000..663818f --- /dev/null +++ b/app/addons/documents/index-editor/reducers.js @@ -0,0 +1,196 @@ +// 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 ActionTypes from './actiontypes'; +import Resources from "../resources"; + +const defaultMap = 'function (doc) {\n emit(doc._id, 1);\n}'; +const defaultReduce = 'function (keys, values, rereduce) {\n if (rereduce) {\n return sum(values);\n } else {\n return values.length;\n }\n}'; +const builtInReducers = ['_sum', '_count', '_stats']; + +const initialState = { + designDocs: new Backbone.Collection(), + isLoading: true, + view: { reduce: '', map: defaultMap }, + database: { id: '0' }, + designDocId: '', + isNewDesignDoc: false, + newDesignDocName: '', + isNewView: false, + viewName: '', + originalViewName: '', + originalDesignDocName: '', + reduceOptions: builtInReducers.concat(['CUSTOM', 'NONE']) +}; + +function editIndex(state, options) { + const newState = { + ...state, + isLoading: false, + newDesignDocName: '', + isNewView: options.isNewView, + viewName: options.viewName || 'viewName', + isNewDesignDoc: options.isNewDesignDoc || false, + designDocs: options.designDocs, + designDocId: options.designDocId, + originalDesignDocName: options.designDocId, + database: options.database + }; + newState.originalViewName = newState.viewName; + newState.view = getView(newState); + return newState; +} + +function getView(state) { + if (state.isNewView || state.isNewDesignDoc) { + return { reduce: '', map: defaultMap }; + } + + const designDoc = state.designDocs.find(ddoc => { + return state.designDocId == ddoc.id; + }).dDocModel(); + return designDoc.get('views')[state.viewName]; +} + +export function reduceSelectedOption(state) { + if (!state.view.reduce) { + return 'NONE'; + } + if (hasCustomReduce(state)) { + return 'CUSTOM'; + } + return state.view.reduce; +} + +export function hasCustomReduce(state) { + if (state.view.reduce) { + return !builtInReducers.includes(state.view.reduce); + } + return false; +} + +export function getSaveDesignDoc(state) { + if (state.designDocId === 'new-doc') { + const doc = { + _id: '_design/' + state.newDesignDocName, + views: {}, + language: 'javascript' + }; + return new Resources.Doc(doc, { database: state.database }); + } + + if (!state.designDocs) { + return null; + } + + const foundDoc = state.designDocs.find(ddoc => { + return ddoc.id === state.designDocId; + }); + + return foundDoc ? foundDoc.dDocModel() : null; +} + +// returns a simple array of design doc IDs. Omits mango docs +export function getDesignDocIds(state) { + if (!state.designDocs) { + return []; + } + return state.designDocs.filter(doc => { + return !doc.isMangoDoc(); + }).map(doc => { + return doc.id; + }); +} + +export default function indexEditor(state = initialState, action) { + const { options } = action; + switch (action.type) { + + case ActionTypes.CLEAR_INDEX: + return { + ...initialState + }; + + case ActionTypes.EDIT_INDEX: + return editIndex(state, options); + + case ActionTypes.EDIT_NEW_INDEX: + return editIndex(state, options); + + case ActionTypes.VIEW_NAME_CHANGE: + return { + ...state, + viewName: action.name + }; + + case ActionTypes.SELECT_REDUCE_CHANGE: + let newReduce = action.reduceSelectedOption; + if (newReduce === 'NONE') { + newReduce = ''; + } + if (newReduce === 'CUSTOM') { + newReduce = defaultReduce; + } + return { + ...state, + view: { + ...state.view, + reduce: newReduce + } + }; + + case ActionTypes.DESIGN_DOC_CHANGE: + return { + ...state, + designDocId: options.value + }; + + case ActionTypes.VIEW_SAVED: + return state; + + case ActionTypes.VIEW_CREATED: + return state; + + case ActionTypes.VIEW_ADD_DESIGN_DOC: + return { + ...state, + designDocId: action.designDoc._id + }; + + case ActionTypes.VIEW_UPDATE_MAP_CODE: + return { + ...state, + view: { + ...state.view, + map: action.code + } + }; + + case ActionTypes.VIEW_UPDATE_REDUCE_CODE: + return { + ...state, + view: { + ...state.view, + reduce: action.code + } + }; + + case ActionTypes.DESIGN_DOC_NEW_NAME_UPDATED: + return { + ...state, + newDesignDocName: options.value + }; + + default: + return state; + } +} diff --git a/app/addons/documents/index-editor/stores.js b/app/addons/documents/index-editor/stores.js deleted file mode 100644 index c5b6205..0000000 --- a/app/addons/documents/index-editor/stores.js +++ /dev/null @@ -1,264 +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 ActionTypes from "./actiontypes"; -import Resources from "../resources"; -var Stores = {}; - -Stores.IndexEditorStore = FauxtonAPI.Store.extend({ - - defaultMap: 'function (doc) {\n emit(doc._id, 1);\n}', - defaultReduce: 'function (keys, values, rereduce) {\n if (rereduce) {\n return sum(values);\n } else {\n return values.length;\n }\n}', - - initialize: function () { - this.reset(); - }, - - reset: function () { - this._designDocs = []; - this._isLoading = true; - this._view = { reduce: '', map: this.defaultMap }; - this._database = { id: '0' }; - }, - - editIndex: function (options) { - this._database = options.database; - this._newView = options.newView; - this._viewName = options.viewName || 'viewName'; - this._newDesignDoc = options.newDesignDoc || false; - this._newDesignDocName = ''; - this._designDocs = options.designDocs; - this._designDocId = options.designDocId; - this._originalViewName = this._viewName; - this._originalDesignDocName = options.designDocId; - this.setView(); - - this._isLoading = false; - }, - - isLoading: function () { - return this._isLoading; - }, - - setView: function () { - if (this._newView || this._newDesignDoc) { - this._view = { reduce: '', map: this.defaultMap }; - } else { - this._view = this.getDesignDoc().get('views')[this._viewName]; - } - }, - - getDatabase: function () { - return this._database; - }, - - getMap: function () { - return this._view.map; - }, - - setMap: function (map) { - this._view.map = map; - }, - - getReduce: function () { - return this._view.reduce; - }, - - setReduce: function (reduce) { - this._view.reduce = reduce; - }, - - getDesignDoc: function () { - return this._designDocs.find((ddoc) => { - return this._designDocId == ddoc.id; - }).dDocModel(); - }, - - getDesignDocs: function () { - return this._designDocs; - }, - - // returns a simple array of design doc IDs. Omits mango docs - getAvailableDesignDocs: function () { - var availableDocs = this.getDesignDocs().filter(function (doc) { - return !doc.isMangoDoc(); - }); - return _.map(availableDocs, function (doc) { - return doc.id; - }); - }, - - getDesignDocId: function () { - return this._designDocId; - }, - - setDesignDocId: function (designDocId) { - this._designDocId = designDocId; - }, - - isNewDesignDoc: function () { - return this._newDesignDoc; - }, - - isNewView: function () { - return this._newView; - }, - - getViewName: function () { - return this._viewName; - }, - - setViewName: function (name) { - this._viewName = name; - }, - - hasCustomReduce: function () { - if (!this.hasReduce()) { - return false; - } - return !_.includes(this.builtInReduces(), this.getReduce()); - }, - - hasReduce: function () { - if (!this.getReduce()) { - return false; - } - return true; - }, - - getOriginalViewName: function () { - return this._originalViewName; - }, - - getOriginalDesignDocName: function () { - return this._originalDesignDocName; - }, - - builtInReduces: function () { - return ['_sum', '_count', '_stats']; - }, - - reduceSelectedOption: function () { - if (!this.hasReduce()) { - return 'NONE'; - } - if (this.hasCustomReduce()) { - return 'CUSTOM'; - } - return this.getReduce(); - }, - - reduceOptions: function () { - return this.builtInReduces().concat(['CUSTOM', 'NONE']); - }, - - updateReduceFromSelect: function (selectedReduce) { - if (selectedReduce === 'NONE') { - this.setReduce(null); - return; - } - if (selectedReduce === 'CUSTOM') { - this.setReduce(this.defaultReduce); - return; - } - this.setReduce(selectedReduce); - }, - - addDesignDoc: function (designDoc) { - this._designDocs.add(designDoc, { merge: true }); - this._designDocId = designDoc._id; - }, - - getNewDesignDocName: function () { - return this._newDesignDocName; - }, - - getSaveDesignDoc: function () { - if (this._designDocId === 'new-doc') { - var doc = { - _id: '_design/' + this._newDesignDocName, - views: {}, - language: 'javascript' - }; - return new Resources.Doc(doc, { database: this._database }); - } - - var foundDoc = this._designDocs.find(function (ddoc) { - return ddoc.id === this._designDocId; - }.bind(this)); - - return (!foundDoc) ? null : foundDoc.dDocModel(); - }, - - dispatch: function (action) { - switch (action.type) { - case ActionTypes.CLEAR_INDEX: - this.reset(); - break; - - case ActionTypes.EDIT_INDEX: - this.editIndex(action.options); - break; - - case ActionTypes.VIEW_NAME_CHANGE: - this.setViewName(action.name); - break; - - case ActionTypes.EDIT_NEW_INDEX: - this.editIndex(action.options); - break; - - case ActionTypes.SELECT_REDUCE_CHANGE: - this.updateReduceFromSelect(action.reduceSelectedOption); - break; - - case ActionTypes.DESIGN_DOC_CHANGE: - this.setDesignDocId(action.options.value); - break; - - case ActionTypes.VIEW_SAVED: - break; - - case ActionTypes.VIEW_CREATED: - break; - - case ActionTypes.VIEW_ADD_DESIGN_DOC: - this.addDesignDoc(action.designDoc); - this.setView(); - break; - - case ActionTypes.VIEW_UPDATE_MAP_CODE: - this.setMap(action.code); - break; - - case ActionTypes.VIEW_UPDATE_REDUCE_CODE: - this.setReduce(action.code); - break; - - case ActionTypes.DESIGN_DOC_NEW_NAME_UPDATED: - this._newDesignDocName = action.options.value; - break; - - default: - return; - } - - this.triggerChange(); - } - -}); - -Stores.indexEditorStore = new Stores.IndexEditorStore(); -Stores.indexEditorStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.indexEditorStore.dispatch.bind(Stores.indexEditorStore)); - -export default Stores; diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 4be1d20..7f91c8e 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -247,7 +247,7 @@ export const ViewsTabsSidebarLayout = ({showEditView, database, docURL, endpoint dbName, dropDownLinks, selectedNavItem, designDocInfo, partitionKey }) => { const content = showEditView ? - <IndexEditorComponents.EditorController /> : + <IndexEditorComponents.IndexEditorContainer /> : <DesignDocInfoContainer designDocInfo={designDocInfo} designDocName={selectedNavItem.designDocName}/>; diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js index 8d357ba..bfe9c42 100644 --- a/app/addons/documents/routes-index-editor.js +++ b/app/addons/documents/routes-index-editor.js @@ -74,10 +74,10 @@ const IndexEditorAndResults = BaseRoute.extend({ showView: function (databaseName, partitionKey, ddoc, viewName) { viewName = viewName.replace(/\?.*$/, ''); - ActionsIndexEditor.clearIndex(); - ActionsIndexEditor.fetchDesignDocsBeforeEdit({ + ActionsIndexEditor.dispatchClearIndex(); + ActionsIndexEditor.dispatchFetchDesignDocsBeforeEdit({ viewName: viewName, - newView: false, + isNewView: false, database: this.database, designDocs: this.designDocs, designDocId: '_design/' + ddoc @@ -127,21 +127,21 @@ const IndexEditorAndResults = BaseRoute.extend({ }, createView: function (database, partitionKey, _designDoc) { - let newDesignDoc = true; + let isNewDesignDoc = true; let designDoc = 'new-doc'; if (_designDoc) { designDoc = '_design/' + _designDoc; - newDesignDoc = false; + isNewDesignDoc = false; } - ActionsIndexEditor.fetchDesignDocsBeforeEdit({ + ActionsIndexEditor.dispatchFetchDesignDocsBeforeEdit({ viewName: 'new-view', - newView: true, + isNewView: true, database: this.database, designDocs: this.designDocs, designDocId: designDoc, - newDesignDoc: newDesignDoc + isNewDesignDoc: isNewDesignDoc }); const selectedNavItem = new SidebarItemSelection(''); @@ -163,9 +163,9 @@ const IndexEditorAndResults = BaseRoute.extend({ }, editView: function (databaseName, partitionKey, ddocName, viewName) { - ActionsIndexEditor.fetchDesignDocsBeforeEdit({ + ActionsIndexEditor.dispatchFetchDesignDocsBeforeEdit({ viewName: viewName, - newView: false, + isNewView: false, database: this.database, designDocs: this.designDocs, designDocId: '_design/' + ddocName