Update documents/changes to use redux (#1134)
* Split components to separate files
* Use redux
* Update tests
diff --git a/app/addons/documents/__tests__/changes-reducers.test.js b/app/addons/documents/__tests__/changes-reducers.test.js
new file mode 100644
index 0000000..9b494fc
--- /dev/null
+++ b/app/addons/documents/__tests__/changes-reducers.test.js
@@ -0,0 +1,145 @@
+
+// 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 utils from '../../../../test/mocha/testUtils';
+import ActionTypes from '../changes/actiontypes';
+import reducer from '../changes/reducers';
+
+FauxtonAPI.router = new FauxtonAPI.Router([]);
+
+const assert = utils.assert;
+
+describe('Changes Reducer', () => {
+
+ const changesList = [
+ { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' }, isNew: false }
+ ];
+
+ it('adds new filter to state', () => {
+ const filter = 'My filter';
+ const action = {
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter
+ };
+ const newState = reducer(undefined, action);
+
+ assert.ok(newState.filters.length === 1);
+ assert.ok(newState.filters[0] === filter);
+ });
+
+ it('removes filter from state', () => {
+ const filter1 = 'My filter 1';
+ const filter2 = 'My filter 2';
+ let newState = reducer(undefined, {
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter: filter1
+ });
+ newState = reducer(newState, {
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter: filter2
+ });
+ newState = reducer(newState, {
+ type: ActionTypes.REMOVE_CHANGES_FILTER_ITEM,
+ filter: filter1
+ });
+
+ assert.ok(newState.filters.length === 1);
+ assert.ok(newState.filters[0] === filter2);
+ });
+
+ it('number of items is capped by maxChangesListed', () => {
+
+ // to keep the test speedy, we override the default max value
+ const maxChanges = 10;
+ const changes = [];
+ for (let i = 0; i < maxChanges + 10; i++) {
+ changes.push({ id: 'doc_' + i, seq: 1, changes: {}});
+ }
+ let state = reducer(undefined, {type: 'DO_NOTHING'});
+ state.maxChangesListed = maxChanges;
+
+ const seqNum = 123;
+ state = reducer(state, {
+ type: ActionTypes.UPDATE_CHANGES,
+ seqNum,
+ changes
+ });
+ assert.equal(state.changes.length, changes.length);
+ assert.equal(state.filteredChanges.length, maxChanges);
+ });
+
+ it('tracks last sequence number', () => {
+ let state = reducer(undefined, {type: 'DO_NOTHING'});
+ assert.equal(state.lastSequenceNum, null);
+
+ const seqNum = 123;
+ state = reducer(state, {
+ type: ActionTypes.UPDATE_CHANGES,
+ seqNum,
+ changes: []
+ });
+
+ // confirm it's been stored
+ assert.equal(state.lastSequenceNum, seqNum);
+ });
+
+ it('"true" filter should apply to change deleted status', () => {
+ let state = reducer(undefined, {
+ type: ActionTypes.UPDATE_CHANGES,
+ seqNum: 123,
+ changes: changesList
+ });
+
+ // add a filter
+ state = reducer(state, {
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter: 'true'
+ });
+
+ // confirm only the two deleted items are part of filtered results
+ assert.equal(state.filteredChanges.length, 2);
+ state.filteredChanges.forEach(el => {
+ assert.equal(el.deleted, true);
+ });
+ });
+
+ // confirms that if there are multiple filters, ALL are applied to return the subset of results that match
+ // all filters
+ it('multiple filters should all be applied to results', () => {
+ let state = reducer(undefined, {
+ type: ActionTypes.UPDATE_CHANGES,
+ seqNum: 123,
+ changes: changesList
+ });
+
+ // add the filters
+ state = reducer(state, {
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter: 'true'
+ });
+ state = reducer(state, {
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter: '1'
+ });
+
+ // confirm only doc_5 matches both filters
+ assert.equal(state.filteredChanges.length, 1);
+ assert.equal(state.filteredChanges[0].id, 'doc_5');
+ });
+
+});
diff --git a/app/addons/documents/__tests__/changes-stores.test.js b/app/addons/documents/__tests__/changes-stores.test.js
deleted file mode 100644
index 7336af0..0000000
--- a/app/addons/documents/__tests__/changes-stores.test.js
+++ /dev/null
@@ -1,90 +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 "../changes/stores";
-import utils from "../../../../test/mocha/testUtils";
-FauxtonAPI.router = new FauxtonAPI.Router([]);
-
-const assert = utils.assert;
-
-describe('ChangesStore', () => {
-
- afterEach(() => {
- Stores.changesStore.reset();
- });
-
- it('addFilter() adds item in store', () => {
- const filter = 'My filter';
- Stores.changesStore.addFilter(filter);
- const filters = Stores.changesStore.getFilters();
- assert.ok(filters.length === 1);
- assert.ok(filters[0] === filter);
- });
-
- it('removeFilter() removes item from store', () => {
- const filter1 = 'My filter 1';
- const filter2 = 'My filter 2';
- Stores.changesStore.addFilter(filter1);
- Stores.changesStore.addFilter(filter2);
- Stores.changesStore.removeFilter(filter1);
-
- const filters = Stores.changesStore.getFilters();
- assert.ok(filters.length === 1);
- assert.ok(filters[0] === filter2);
- });
-
- it('hasFilter() finds item in store', () => {
- const filter = 'My filter';
- Stores.changesStore.addFilter(filter);
- assert.ok(Stores.changesStore.hasFilter(filter) === true);
- });
-
- it('getDatabaseName() returns database name', () => {
- const dbName = 'hoopoes';
- Stores.changesStore.initChanges({ databaseName: dbName });
- assert.equal(Stores.changesStore.getDatabaseName(), dbName);
-
- Stores.changesStore.reset();
- assert.equal(Stores.changesStore.getDatabaseName(), '');
- });
-
- it("getChanges() should return a subset if there are a lot of changes", () => {
-
- // to keep the test speedy, we override the default max value
- const maxChanges = 10;
- const changes = [];
- _.times(maxChanges + 10, (i) => {
- changes.push({ id: 'doc_' + i, seq: 1, changes: {}});
- });
- Stores.changesStore.initChanges({ databaseName: "test" });
- Stores.changesStore.setMaxChanges(maxChanges);
-
- const seqNum = 123;
- Stores.changesStore.updateChanges(seqNum, changes);
-
- const results = Stores.changesStore.getChanges();
- assert.equal(maxChanges, results.length);
- });
-
- it("tracks last sequence number", () => {
- assert.equal(null, Stores.changesStore.getLastSeqNum());
-
- const seqNum = 123;
- Stores.changesStore.updateChanges(seqNum, []);
-
- // confirm it's been stored
- assert.equal(seqNum, Stores.changesStore.getLastSeqNum());
- });
-
-});
diff --git a/app/addons/documents/__tests__/changes.test.js b/app/addons/documents/__tests__/changes.test.js
index 0745458..833c8c1 100644
--- a/app/addons/documents/__tests__/changes.test.js
+++ b/app/addons/documents/__tests__/changes.test.js
@@ -13,9 +13,9 @@
import React from "react";
import ReactDOM from "react-dom";
-import Changes from "../changes/components";
-import Stores from "../changes/stores";
-import Actions from "../changes/actions";
+import ChangeRow from '../changes/components/ChangeRow';
+import ChangesScreen from '../changes/components/ChangesScreen';
+import ChangesTabContent from "../changes/components/ChangesTabContent";
import {mount} from 'enzyme';
import utils from "../../../../test/mocha/testUtils";
import sinon from "sinon";
@@ -25,86 +25,78 @@
describe('ChangesTabContent', () => {
- let el;
+ const defaultProps = {
+ filters: [],
+ addFilter: () => {},
+ removeFilter: () => {}
+ };
- beforeEach(() => {
- el = mount(<Changes.ChangesTabContent />);
- });
-
- afterEach(() => {
- Stores.changesStore.reset();
- });
-
- it('should add filter markup', () => {
- const submitBtn = el.find('[type="submit"]'),
- addItemField = el.find('.js-changes-filter-field');
-
- addItemField.simulate('change', {target: {value: 'I wandered lonely as a filter'}});
- submitBtn.simulate('submit');
-
- addItemField.simulate('change', {target: {value: 'A second filter'}});
- submitBtn.simulate('submit');
+ it('should add filter badges', () => {
+ const el = mount(<ChangesTabContent
+ {...defaultProps}
+ filters={['I wandered lonely as a filter', 'A second filter']}
+ />);
assert.equal(2, el.find('.remove-filter').length);
});
it('should call addFilter action on click', () => {
- const submitBtn = el.find('[type="submit"]'),
- addItemField = el.find('.js-changes-filter-field');
-
- const spy = sinon.spy(Actions, 'addFilter');
-
- addItemField.simulate('change', {target: {value: 'I wandered lonely as a filter'}});
- submitBtn.simulate('submit');
-
- assert.ok(spy.calledOnce);
- });
-
- it('should remove filter markup', () => {
+ const addFilterStub = sinon.stub();
+ const el = mount(<ChangesTabContent
+ {...defaultProps}
+ addFilter={addFilterStub}
+ />);
const submitBtn = el.find('[type="submit"]'),
addItemField = el.find('.js-changes-filter-field');
addItemField.simulate('change', {target: {value: 'I wandered lonely as a filter'}});
submitBtn.simulate('submit');
- addItemField.simulate('change', {target: {value: 'Flibble'}});
- submitBtn.simulate('submit');
-
- // clicks ALL 'remove' elements
- el.find('.remove-filter').first().simulate('click');
- el.find('.remove-filter').simulate('click');
-
- assert.equal(0, el.find('.remove-filter').length);
+ assert.ok(addFilterStub.calledOnce);
});
it('should call removeFilter action on click', () => {
- const submitBtn = el.find('[type="submit"]'),
- addItemField = el.find('.js-changes-filter-field');
-
- const spy = sinon.spy(Actions, 'removeFilter');
-
- addItemField.simulate('change', {target: {value: 'Flibble'}});
- submitBtn.simulate('submit');
+ const removeFilterStub = sinon.stub();
+ const el = mount(<ChangesTabContent
+ {...defaultProps}
+ filters={['I wandered lonely as a filter']}
+ removeFilter={removeFilterStub}
+ />);
el.find('.remove-filter').simulate('click');
- assert.ok(spy.calledOnce);
+ assert.ok(removeFilterStub.calledOnce);
});
it('should not add empty filters', () => {
+ const addFilterStub = sinon.stub();
+ const el = mount(<ChangesTabContent
+ {...defaultProps}
+ addFilter={addFilterStub}
+ />);
const submitBtn = el.find('[type="submit"]'),
addItemField = el.find('.js-changes-filter-field');
addItemField.simulate('change', {target: {value: ''}});
submitBtn.simulate('submit');
- assert.equal(0, el.find('.remove-filter').length);
+ assert.ok(addFilterStub.notCalled);
});
- it('should not add tooltips by default', () => {
+ it('should not add badges by default', () => {
+ const el = mount(<ChangesTabContent
+ {...defaultProps}
+ />);
assert.equal(0, el.find('.remove-filter').length);
});
it('should not add the same filter twice', () => {
+ const filters = [];
+ let callCount = 0;
+ const el = mount(<ChangesTabContent
+ {...defaultProps}
+ addFilter={(f) => {filters.push(f); callCount++;}}
+ filters={filters}
+ />);
const submitBtn = el.find('[type="submit"]'),
addItemField = el.find('.js-changes-filter-field');
@@ -115,143 +107,52 @@
addItemField.simulate('change', {target: {value: filter}});
submitBtn.simulate('submit');
- assert.equal(1, el.find('.remove-filter').length);
+ assert.equal(callCount, 1);
});
});
-describe('ChangesController', () => {
- let headerEl, changesEl;
-
- const results = [
- { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' } },
- { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' } },
- { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' } },
- { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' } },
- { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' } }
+describe('ChangesScreen', () => {
+ const changesList = [
+ { id: 'doc_1', seq: 4, deleted: false, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_2', seq: 1, deleted: false, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_3', seq: 6, deleted: true, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_4', seq: 7, deleted: false, changes: { code: 'here' }, isNew: false },
+ { id: 'doc_5', seq: 1, deleted: true, changes: { code: 'here' }, isNew: false }
];
- const changesResponse = {
- last_seq: 123,
- 'results': results
+ const defaultProps = {
+ changes: [],
+ loaded: true,
+ databaseName: 'my_db',
+ isShowingSubset: false,
+ loadChanges: () => {}
};
- beforeEach(() => {
- Actions.initChanges({ databaseName: 'testDatabase' });
- Actions.updateChanges(changesResponse);
- headerEl = mount(<Changes.ChangesTabContent />);
- changesEl = mount(<Changes.ChangesController />);
- });
-
- afterEach(() => {
- Stores.changesStore.reset();
- });
-
-
it('should list the right number of changes', () => {
- changesEl.update();
- assert.equal(results.length, changesEl.find('.change-box').length);
- });
+ const changesEl = mount(<ChangesScreen
+ {...defaultProps}
+ changes={changesList}
+ />);
-
- it('"false"/"true" filter strings should apply to change deleted status', () => {
- // add a filter
- const addItemField = headerEl.find('.js-changes-filter-field');
- const submitBtn = headerEl.find('[type="submit"]');
- addItemField.value = 'true';
- addItemField.simulate('change', {target: {value: 'true'}});
- submitBtn.simulate('submit');
-
- // confirm only the two deleted items shows up and the IDs maps to the deleted rows
- changesEl.update();
- assert.equal(2, changesEl.find('.change-box').length);
- assert.equal('doc_3', changesEl.find('.js-doc-id').first().text());
- assert.equal('doc_5', changesEl.find('.js-doc-id').at(1).text());
- });
-
-
- it('confirms that a filter affects the actual search results', () => {
- // add a filter
- const addItemField = headerEl.find('.js-changes-filter-field');
- const submitBtn = headerEl.find('[type="submit"]');
- addItemField.simulate('change', {target: {value: '6'}});
- submitBtn.simulate('submit');
-
- // confirm only one item shows up and the ID maps to what we'd expect
- changesEl.update();
- assert.equal(1, changesEl.find('.change-box').length);
- assert.equal('doc_3', changesEl.find('.js-doc-id').first().text());
- });
-
-
- // confirms that if there are multiple filters, ALL are applied to return the subset of results that match
- // all filters
- it('multiple filters should all be applied to results', () => {
- // add the filters
- const addItemField = headerEl.find('.js-changes-filter-field');
- const submitBtn = headerEl.find('[type="submit"]');
-
- // *** should match doc_1, doc_2 and doc_5
- addItemField.simulate('change', {target: {value: '1'}});
- submitBtn.simulate('submit');
-
- // *** should match doc_3 and doc_5
- addItemField.simulate('change', {target: {value: 'true'}});
- submitBtn.simulate('submit');
-
- // confirm only one item shows up and that it's doc_5
- changesEl.update();
- assert.equal(1, changesEl.find('.change-box').length);
- assert.equal('doc_5', changesEl.find('.js-doc-id').first().text());
+ assert.equal(changesList.length, changesEl.find('.change-box').length);
});
it('shows a No Docs Found message if no docs', () => {
- Stores.changesStore.reset();
- Actions.updateChanges({ last_seq: 124, results: [] });
- changesEl.update();
+ const changesEl = mount(<ChangesScreen
+ {...defaultProps}
+ />);
assert.ok(/There\sare\sno\sdocument\schanges/.test(changesEl.html()));
});
-});
-
-
-describe('ChangesController max results', () => {
- let changesEl;
- const maxChanges = 10;
-
-
- beforeEach(() => {
- const changes = [];
- _.times(maxChanges + 10, (i) => {
- changes.push({ id: 'doc_' + i, seq: 1, changes: { code: 'here' } });
- });
-
- const response = {
- last_seq: 1,
- results: changes
- };
-
- Actions.initChanges({ databaseName: 'test' });
-
- // to keep the test speedy, override the default value (1000)
- Stores.changesStore.setMaxChanges(maxChanges);
-
- Actions.updateChanges(response);
- changesEl = mount(<Changes.ChangesController />);
- });
-
- afterEach(() => {
- Stores.changesStore.reset();
- });
-
- it('should truncate the number of results with very large # of changes', () => {
- // check there's no more than maxChanges results
- assert.equal(maxChanges, changesEl.find('.change-box').length);
- });
it('should show a message if the results are truncated', () => {
+ const changesEl = mount(<ChangesScreen
+ {...defaultProps}
+ changes={changesList}
+ isShowingSubset={true}
+ />);
assert.equal(1, changesEl.find('.changes-result-limit').length);
});
-
});
@@ -264,7 +165,7 @@
};
it('clicking the toggle-json button shows the code section', function () {
- const changeRow = mount(<Changes.ChangeRow change={change} databaseName="testDatabase" />);
+ const changeRow = mount(<ChangeRow change={change} databaseName="testDatabase" />);
// confirm it's hidden by default
assert.equal(0, changeRow.find('.prettyprint').length);
@@ -276,13 +177,13 @@
it('deleted docs should not be clickable', () => {
change.deleted = true;
- const changeRow = mount(<Changes.ChangeRow change={change} databaseName="testDatabase" />);
+ const changeRow = mount(<ChangeRow change={change} databaseName="testDatabase" />);
assert.equal(0, changeRow.find('a.js-doc-link').length);
});
it('non-deleted docs should be clickable', () => {
change.deleted = false;
- const changeRow = mount(<Changes.ChangeRow change={change} databaseName="testDatabase" />);
+ const changeRow = mount(<ChangeRow change={change} databaseName="testDatabase" />);
assert.equal(1, changeRow.find('a.js-doc-link').length);
});
});
diff --git a/app/addons/documents/base.js b/app/addons/documents/base.js
index 090a450..c5f7826 100644
--- a/app/addons/documents/base.js
+++ b/app/addons/documents/base.js
@@ -20,6 +20,7 @@
import sidebarReducers from "./sidebar/reducers";
import partitionKeyReducers from "./partition-key/reducers";
import revisionBrowserReducers from './rev-browser/reducers';
+import changesReducers from './changes/reducers';
import "./assets/less/documents.less";
FauxtonAPI.addReducers({
@@ -28,6 +29,7 @@
sidebar: sidebarReducers,
revisionBrowser: revisionBrowserReducers,
partitionKey: partitionKeyReducers,
+ changes: changesReducers,
designDocInfo: designDocInfoReducers
});
diff --git a/app/addons/documents/changes/actions.js b/app/addons/documents/changes/actions.js
index 224bd12..f6f72d4 100644
--- a/app/addons/documents/changes/actions.js
+++ b/app/addons/documents/changes/actions.js
@@ -11,80 +11,74 @@
// License for the specific language governing permissions and limitations under
// the License.
-import app from "../../../app";
-import FauxtonAPI from "../../../core/api";
-import { get } from "../../../core/ajax";
-import ActionTypes from "./actiontypes";
-import Stores from "./stores";
-import Helpers from "../helpers";
+import app from '../../../app';
+import FauxtonAPI from '../../../core/api';
+import { get } from '../../../core/ajax';
+import ActionTypes from './actiontypes';
+import Helpers from '../helpers';
-const changesStore = Stores.changesStore;
const pollingTimeout = 60000;
-var currentRequest;
+let currentRequest;
+const addFilter = (filter) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
+ filter: filter
+ });
+};
+
+const removeFilter = (filter) => (dispatch) => {
+ dispatch({
+ type: ActionTypes.REMOVE_CHANGES_FILTER_ITEM,
+ filter: filter
+ });
+};
+
+const loadChanges = (databaseName) => (dispatch) => {
+ currentRequest = null;
+ getLatestChanges(dispatch, databaseName);
+};
+
+const getLatestChanges = (dispatch, databaseName, lastSeqNum) => {
+ const params = {
+ limit: 100
+ };
+
+ // after the first request for the changes list has been made, switch to longpoll
+ if (currentRequest) {
+ params.since = lastSeqNum;
+ params.timeout = pollingTimeout;
+ params.feed = 'longpoll';
+ }
+
+ const query = app.utils.queryParams(params);
+ const db = app.utils.safeURLName(databaseName);
+ const endpoint = FauxtonAPI.urls('changes', 'server', db, '?' + query);
+ get(endpoint).then((res) => {
+ if (res.error) {
+ throw new Error(res.reason || res.error);
+ }
+ updateChanges(res, dispatch);
+ }).catch((err) => {
+ FauxtonAPI.addNotification({
+ msg: 'Error loading list of changes. Reason: ' + err.message,
+ type: 'error',
+ clear: true
+ });
+ });
+};
+
+const updateChanges = (json, dispatch) => {
+ const latestSeqNum = Helpers.getSeqNum(json.last_seq);
+ dispatch({
+ type: ActionTypes.UPDATE_CHANGES,
+ changes: json.results,
+ seqNum: latestSeqNum
+ });
+};
export default {
- addFilter: function (filter) {
- FauxtonAPI.dispatch({
- type: ActionTypes.ADD_CHANGES_FILTER_ITEM,
- filter: filter
- });
- },
-
- removeFilter: function (filter) {
- FauxtonAPI.dispatch({
- type: ActionTypes.REMOVE_CHANGES_FILTER_ITEM,
- filter: filter
- });
- },
-
- initChanges: function (options) {
- FauxtonAPI.dispatch({
- type: ActionTypes.INIT_CHANGES,
- options: options
- });
- currentRequest = null;
- this.getLatestChanges();
- },
-
- getLatestChanges: function () {
- const params = {
- limit: 100
- };
-
- // after the first request for the changes list has been made, switch to longpoll
- if (currentRequest) {
- params.since = changesStore.getLastSeqNum();
- params.timeout = pollingTimeout;
- params.feed = 'longpoll';
- }
-
- const query = app.utils.queryParams(params);
- const db = app.utils.safeURLName(changesStore.getDatabaseName());
- const endpoint = FauxtonAPI.urls('changes', 'server', db, '?' + query);
- get(endpoint).then((res) => {
- if (res.error) {
- throw new Error(res.reason || res.error);
- }
- this.updateChanges(res);
- }).catch((err) => {
- FauxtonAPI.addNotification({
- msg: 'Error loading list of changes. Reason: ' + err.message,
- type: 'error',
- clear: true
- });
- });
- },
-
- updateChanges: function (json) {
- // only bother updating the list of changes if the seq num has changed
- const latestSeqNum = Helpers.getSeqNum(json.last_seq);
- if (latestSeqNum !== changesStore.getLastSeqNum()) {
- FauxtonAPI.dispatch({
- type: ActionTypes.UPDATE_CHANGES,
- changes: json.results,
- seqNum: latestSeqNum
- });
- }
- }
+ addFilter,
+ removeFilter,
+ loadChanges
};
diff --git a/app/addons/documents/changes/actiontypes.js b/app/addons/documents/changes/actiontypes.js
index bd2e412..6325683 100644
--- a/app/addons/documents/changes/actiontypes.js
+++ b/app/addons/documents/changes/actiontypes.js
@@ -11,7 +11,6 @@
// the License.
export default {
- INIT_CHANGES: 'INIT_CHANGES',
UPDATE_CHANGES: 'UPDATE_CHANGES',
ADD_CHANGES_FILTER_ITEM: 'ADD_CHANGES_FILTER_ITEM',
REMOVE_CHANGES_FILTER_ITEM: 'REMOVE_CHANGES_FILTER_ITEM',
diff --git a/app/addons/documents/changes/components.js b/app/addons/documents/changes/components.js
deleted file mode 100644
index 3c5d1c9..0000000
--- a/app/addons/documents/changes/components.js
+++ /dev/null
@@ -1,411 +0,0 @@
-import FauxtonAPI from "../../../core/api";
-
-// 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 Actions from "./actions";
-import Stores from "./stores";
-import Components from "../../fauxton/components";
-import ReactComponents from "../../components/react-components";
-import {TransitionMotion, spring, presets} from 'react-motion';
-import "../../../../assets/js/plugins/prettify";
-import uuid from 'uuid';
-
-const store = Stores.changesStore;
-const BadgeList = ReactComponents.BadgeList;
-const {Copy} = ReactComponents;
-
-class ChangesController extends React.Component {
- constructor (props) {
- super(props);
- this.state = this.getStoreState();
- }
-
- getStoreState () {
- return {
- changes: store.getChanges(),
- loaded: store.isLoaded(),
- databaseName: store.getDatabaseName(),
- isShowingSubset: store.isShowingSubset()
- };
- }
-
- onChange () {
- this.setState(this.getStoreState());
- }
-
- componentDidMount () {
- store.on('change', this.onChange, this);
- }
-
- componentWillUnmount () {
- store.off('change', this.onChange);
- }
-
- showingSubsetMsg () {
- const { isShowingSubset, changes } = this.state;
- let msg = '';
- if (isShowingSubset) {
- let numChanges = changes.length;
- msg = <p className="changes-result-limit">Limiting results to latest <b>{numChanges}</b> changes.</p>;
- }
- return msg;
- }
-
- getRows () {
- const { changes, loaded, databaseName } = this.state;
- if (!changes.length && loaded) {
- return (
- <p className="no-doc-changes">
- There are no document changes to display.
- </p>
- );
- }
-
- return changes.map((change, i) => {
- return <ChangeRow change={change} key={i} databaseName={databaseName} />;
- });
- }
-
- render () {
- return (
- <div>
- <div className="js-changes-view">
- {this.showingSubsetMsg()}
- {this.getRows()}
- </div>
- </div>
- );
- }
-}
-
-
-class ChangesTabContent extends React.Component {
- constructor (props) {
- super(props);
- this.state = this.getStoreState();
- }
-
- getStoreState () {
- return {
- filters: store.getFilters()
- };
- }
-
- onChange () {
- this.setState(this.getStoreState());
- }
-
- componentDidMount () {
- store.on('change', this.onChange, this);
- }
-
- componentWillUnmount () {
- store.off('change', this.onChange);
- }
-
- addFilter (newFilter) {
- if (_.isEmpty(newFilter)) {
- return;
- }
- Actions.addFilter(newFilter);
- }
-
- hasFilter (filter) {
- return store.hasFilter(filter);
- }
-
- render () {
- return (
- <div className="changes-header">
- <AddFilterForm filter={(label) => Actions.removeFilter(label)} addFilter={this.addFilter}
- hasFilter={this.hasFilter} />
- <BadgeList elements={this.state.filters} removeBadge={(label) => Actions.removeFilter(label)} />
- </div>
- );
- }
-}
-
-
-class AddFilterForm extends React.Component {
- constructor (props) {
- super(props);
- this.state = {
- filter: '',
- error: false
- };
- this.submitForm = this.submitForm.bind(this);
- }
-
- submitForm (e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (this.props.hasFilter(this.state.filter)) {
- this.setState({ error: true });
-
- // Yuck. This removes the class after the effect has completed so it can occur again. The
- // other option is to use jQuery to add the flash. This seemed slightly less crumby
- let component = this;
- setTimeout(function () {
- component.setState({ error: false });
- }, 1000);
- } else {
- this.props.addFilter(this.state.filter);
- this.setState({ filter: '', error: false });
- }
- }
-
- componentDidMount () {
- this.focusFilterField();
- }
-
- componentDidUpdate () {
- this.focusFilterField();
- }
-
- focusFilterField () {
- this.addItem.focus();
- }
-
- inputClassNames () {
- let className = 'js-changes-filter-field';
- if (this.state.error) {
- className += ' errorHighlight';
- }
- return className;
- }
-
- render () {
- return (
- <form className="form-inline js-filter-form" onSubmit={this.submitForm}>
- <fieldset>
- <i className="fonticon-filter" />
- <input
- type="text"
- ref={node => this.addItem = node}
- className={this.inputClassNames()}
- placeholder="Sequence or ID"
- onChange={(e) => this.setState({ filter: e.target.value })}
- value={this.state.filter} />
- <button type="submit" className="btn btn-secondary">Filter</button>
- <div className="help-block"></div>
- </fieldset>
- </form>
- );
- }
-}
-AddFilterForm.propTypes = {
- addFilter: PropTypes.func.isRequired,
- hasFilter: PropTypes.func.isRequired,
- tooltips: PropTypes.string
-};
-AddFilterForm.defaultProps = {
- tooltip: ''
-};
-
-class ChangeRow extends React.Component {
- constructor (props) {
- super(props);
- this.state = {
- codeVisible: false
- };
- }
-
- toggleJSON (e) {
- e.preventDefault();
- this.setState({ codeVisible: !this.state.codeVisible });
- }
-
- getChangesCode () {
- return (this.state.codeVisible) ? <Components.CodeFormat key="changesCodeSection" code={this.getChangeCode()} /> : null;
- }
-
- getChangeCode () {
- return {
- changes: this.props.change.changes,
- doc: this.props.change.doc
- };
- }
-
- showCopiedMessage (target) {
- let msg = 'The document ID has been copied to your clipboard.';
- if (target === 'seq') {
- msg = 'The document seq number has been copied to your clipboard.';
- }
- FauxtonAPI.addNotification({
- msg: msg,
- type: 'info',
- clear: true
- });
- }
-
- render () {
- const { codeVisible } = this.state;
- const { change, databaseName } = this.props;
- const wrapperClass = 'change-wrapper' + (change.isNew ? ' new-change-row' : '');
-
- return (
- <div className={wrapperClass}>
- <div className="change-box" data-id={change.id}>
- <div className="row-fluid">
- <div className="span2">seq</div>
- <div className="span8 change-sequence">{change.seq}</div>
- <div className="span2 text-right">
- <Copy
- uniqueKey={uuid.v4()}
- text={change.seq.toString()}
- onClipboardClick={() => this.showCopiedMessage('seq')} />
- </div>
- </div>
-
- <div className="row-fluid">
- <div className="span2">id</div>
- <div className="span8">
- <ChangeID id={change.id} deleted={change.deleted} databaseName={databaseName} />
- </div>
- <div className="span2 text-right">
- <Copy
- uniqueKey={uuid.v4()}
- text={change.id}
- onClipboardClick={() => this.showCopiedMessage('id')} />
- </div>
- </div>
-
- <div className="row-fluid">
- <div className="span2">deleted</div>
- <div className="span10">{change.deleted ? 'True' : 'False'}</div>
- </div>
-
- <div className="row-fluid">
- <div className="span2">changes</div>
- <div className="span10">
- <button type="button" className='btn btn-small btn-secondary' onClick={this.toggleJSON.bind(this)}>
- {codeVisible ? 'Close JSON' : 'View JSON'}
- </button>
- </div>
- </div>
-
- <ChangesCodeTransition
- codeVisible={this.state.codeVisible}
- code={this.getChangeCode()}
- />
- </div>
- </div>
- );
- }
-}
-
-ChangeRow.propTypes = {
- change: PropTypes.object,
- databaseName: PropTypes.string.isRequired
-};
-
-
-export class ChangesCodeTransition extends React.Component {
- willEnter () {
- return {
- opacity: spring(1, presets.gentle),
- height: spring(160, presets.gentle)
- };
- }
-
- willLeave () {
- return {
- opacity: spring(0, presets.gentle),
- height: spring(0, presets.gentle)
- };
- }
-
- getStyles (prevStyle) {
- if (!prevStyle && this.props.codeVisible) {
- return [{
- key: '1',
- style: this.willEnter()
- }];
- }
-
- if (!prevStyle && !this.props.codeVisible) {
- return [{
- key: '1',
- style: this.willLeave()
- }];
- }
- return prevStyle.map(item => {
- return {
- key: '1',
- style: item.style
- };
- });
- }
-
- getChildren (items) {
- const code = items.map(({style}) => {
- if (this.props.codeVisible === false && style.opacity === 0) {
- return null;
- }
- return (
- <div key='1' style={{opacity: style.opacity, height: style.height + 'px'}}>
- <Components.CodeFormat
- code={this.props.code}
- />
- </div>
- );
- });
-
- return (
- <span>
- {code}
- </span>
- );
- }
-
- render () {
- return (
- <TransitionMotion
- styles={this.getStyles()}
- willLeave={this.willLeave}
- willEnter={this.willEnter}
- >
- {this.getChildren.bind(this)}
- </TransitionMotion>
- );
- }
-}
-
-
-class ChangeID extends React.Component {
- render () {
- const { deleted, id, databaseName } = this.props;
- if (deleted) {
- return (
- <span className="js-doc-id">{id}</span>
- );
- }
- const link = '#' + FauxtonAPI.urls('document', 'app', databaseName, id);
- return (
- <a href={link} className="js-doc-link">{id}</a>
- );
- }
-}
-
-
-export default {
- ChangesController,
- ChangesTabContent,
- ChangeRow,
- ChangeID
-};
diff --git a/app/addons/documents/changes/components/AddFilterForm.js b/app/addons/documents/changes/components/AddFilterForm.js
new file mode 100644
index 0000000..8e0b795
--- /dev/null
+++ b/app/addons/documents/changes/components/AddFilterForm.js
@@ -0,0 +1,93 @@
+// 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';
+
+export default class AddFilterForm extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ filter: '',
+ error: false
+ };
+ this.submitForm = this.submitForm.bind(this);
+ }
+
+ submitForm (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (this.props.hasFilter(this.state.filter)) {
+ this.setState({ error: true });
+
+ // Yuck. This removes the class after the effect has completed so it can occur again. The
+ // other option is to use jQuery to add the flash. This seemed slightly less crumby
+ let component = this;
+ setTimeout(function () {
+ component.setState({ error: false });
+ }, 1000);
+ } else {
+ this.props.addFilter(this.state.filter);
+ this.setState({ filter: '', error: false });
+ }
+ }
+
+ componentDidMount () {
+ this.focusFilterField();
+ }
+
+ componentDidUpdate () {
+ this.focusFilterField();
+ }
+
+ focusFilterField () {
+ this.addItem.focus();
+ }
+
+ inputClassNames () {
+ let className = 'js-changes-filter-field';
+ if (this.state.error) {
+ className += ' errorHighlight';
+ }
+ return className;
+ }
+
+ render () {
+ return (
+ <form className="form-inline js-filter-form" onSubmit={this.submitForm}>
+ <fieldset>
+ <i className="fonticon-filter" />
+ <input
+ type="text"
+ ref={node => this.addItem = node}
+ className={this.inputClassNames()}
+ placeholder="Sequence or ID"
+ onChange={(e) => this.setState({ filter: e.target.value })}
+ value={this.state.filter} />
+ <button type="submit" className="btn btn-secondary">Filter</button>
+ <div className="help-block"></div>
+ </fieldset>
+ </form>
+ );
+ }
+}
+
+AddFilterForm.propTypes = {
+ addFilter: PropTypes.func.isRequired,
+ hasFilter: PropTypes.func.isRequired,
+ tooltips: PropTypes.string
+};
+AddFilterForm.defaultProps = {
+ tooltip: ''
+};
diff --git a/app/addons/documents/changes/components/ChangeID.js b/app/addons/documents/changes/components/ChangeID.js
new file mode 100644
index 0000000..79c8a33
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangeID.js
@@ -0,0 +1,29 @@
+// 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 React from 'react';
+import FauxtonAPI from '../../../../core/api';
+
+export default class ChangeID extends React.Component {
+ render () {
+ const { deleted, id, databaseName } = this.props;
+ if (deleted) {
+ return (
+ <span className="js-doc-id">{id}</span>
+ );
+ }
+ const link = '#' + FauxtonAPI.urls('document', 'app', databaseName, id);
+ return (
+ <a href={link} className="js-doc-link">{id}</a>
+ );
+ }
+}
diff --git a/app/addons/documents/changes/components/ChangeRow.js b/app/addons/documents/changes/components/ChangeRow.js
new file mode 100644
index 0000000..e6d30fe
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangeRow.js
@@ -0,0 +1,120 @@
+// 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 uuid from 'uuid';
+import FauxtonAPI from '../../../../core/api';
+import Components from '../../../fauxton/components';
+import ReactComponents from '../../../components/react-components';
+import ChangesCodeTransition from './ChangesCodeTransition';
+import ChangeID from './ChangeID';
+
+const {Copy} = ReactComponents;
+
+export default class ChangeRow extends React.Component {
+ constructor (props) {
+ super(props);
+ this.state = {
+ codeVisible: false
+ };
+ }
+
+ toggleJSON (e) {
+ e.preventDefault();
+ this.setState({ codeVisible: !this.state.codeVisible });
+ }
+
+ getChangesCode () {
+ return (this.state.codeVisible) ? <Components.CodeFormat key="changesCodeSection" code={this.getChangeCode()} /> : null;
+ }
+
+ getChangeCode () {
+ return {
+ changes: this.props.change.changes,
+ doc: this.props.change.doc
+ };
+ }
+
+ showCopiedMessage (target) {
+ let msg = 'The document ID has been copied to your clipboard.';
+ if (target === 'seq') {
+ msg = 'The document seq number has been copied to your clipboard.';
+ }
+ FauxtonAPI.addNotification({
+ msg: msg,
+ type: 'info',
+ clear: true
+ });
+ }
+
+ render () {
+ const { codeVisible } = this.state;
+ const { change, databaseName } = this.props;
+ const wrapperClass = 'change-wrapper' + (change.isNew ? ' new-change-row' : '');
+
+ return (
+ <div className={wrapperClass}>
+ <div className="change-box" data-id={change.id}>
+ <div className="row-fluid">
+ <div className="span2">seq</div>
+ <div className="span8 change-sequence">{change.seq}</div>
+ <div className="span2 text-right">
+ <Copy
+ uniqueKey={uuid.v4()}
+ text={change.seq.toString()}
+ onClipboardClick={() => this.showCopiedMessage('seq')} />
+ </div>
+ </div>
+
+ <div className="row-fluid">
+ <div className="span2">id</div>
+ <div className="span8">
+ <ChangeID id={change.id} deleted={change.deleted} databaseName={databaseName} />
+ </div>
+ <div className="span2 text-right">
+ <Copy
+ uniqueKey={uuid.v4()}
+ text={change.id}
+ onClipboardClick={() => this.showCopiedMessage('id')} />
+ </div>
+ </div>
+
+ <div className="row-fluid">
+ <div className="span2">deleted</div>
+ <div className="span10">{change.deleted ? 'True' : 'False'}</div>
+ </div>
+
+ <div className="row-fluid">
+ <div className="span2">changes</div>
+ <div className="span10">
+ <button type="button" className='btn btn-small btn-secondary' onClick={this.toggleJSON.bind(this)}>
+ {codeVisible ? 'Close JSON' : 'View JSON'}
+ </button>
+ </div>
+ </div>
+
+ <ChangesCodeTransition
+ codeVisible={this.state.codeVisible}
+ code={this.getChangeCode()}
+ />
+ </div>
+ </div>
+ );
+ }
+}
+
+ChangeRow.propTypes = {
+ change: PropTypes.object,
+ databaseName: PropTypes.string.isRequired
+};
diff --git a/app/addons/documents/changes/components/ChangesCodeTransition.js b/app/addons/documents/changes/components/ChangesCodeTransition.js
new file mode 100644
index 0000000..09ba58d
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangesCodeTransition.js
@@ -0,0 +1,94 @@
+// 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 Components from '../../../fauxton/components';
+import {TransitionMotion, spring, presets} from 'react-motion';
+import '../../../../../assets/js/plugins/prettify';
+
+export default class ChangesCodeTransition extends React.Component {
+ willEnter () {
+ return {
+ opacity: spring(1, presets.gentle),
+ height: spring(160, presets.gentle)
+ };
+ }
+
+ willLeave () {
+ return {
+ opacity: spring(0, presets.gentle),
+ height: spring(0, presets.gentle)
+ };
+ }
+
+ getStyles (prevStyle) {
+ if (!prevStyle && this.props.codeVisible) {
+ return [{
+ key: '1',
+ style: this.willEnter()
+ }];
+ }
+
+ if (!prevStyle && !this.props.codeVisible) {
+ return [{
+ key: '1',
+ style: this.willLeave()
+ }];
+ }
+ return prevStyle.map(item => {
+ return {
+ key: '1',
+ style: item.style
+ };
+ });
+ }
+
+ getChildren (items) {
+ const code = items.map(({style}) => {
+ if (this.props.codeVisible === false && style.opacity === 0) {
+ return null;
+ }
+ return (
+ <div key='1' style={{opacity: style.opacity, height: style.height + 'px'}}>
+ <Components.CodeFormat
+ code={this.props.code}
+ />
+ </div>
+ );
+ });
+
+ return (
+ <span>
+ {code}
+ </span>
+ );
+ }
+
+ render () {
+ return (
+ <TransitionMotion
+ styles={this.getStyles()}
+ willLeave={this.willLeave}
+ willEnter={this.willEnter}
+ >
+ {this.getChildren.bind(this)}
+ </TransitionMotion>
+ );
+ }
+}
+
+ChangesCodeTransition.propTypes = {
+ code: PropTypes.object.isRequired,
+ codeVisible: PropTypes.bool.isRequired
+};
diff --git a/app/addons/documents/changes/components/ChangesContainer.js b/app/addons/documents/changes/components/ChangesContainer.js
new file mode 100644
index 0000000..3fa7ce3
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangesContainer.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import ChangesScreen from './ChangesScreen';
+import Actions from '../actions';
+
+const mapStateToProps = ({ changes }, ownProps) => {
+ return {
+ changes: changes.filteredChanges,
+ loaded: changes.isLoaded,
+ databaseName: ownProps.databaseName,
+ isShowingSubset: changes.showingSubset
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ loadChanges: (databaseName) => {
+ dispatch(Actions.loadChanges(databaseName));
+ }
+ };
+};
+
+const ChangesContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ChangesScreen);
+
+export default ChangesContainer;
diff --git a/app/addons/documents/changes/components/ChangesScreen.js b/app/addons/documents/changes/components/ChangesScreen.js
new file mode 100644
index 0000000..8a78d37
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangesScreen.js
@@ -0,0 +1,67 @@
+// 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 ChangeRow from './ChangeRow';
+
+export default class ChangesScreen extends React.Component {
+ constructor (props) {
+ super(props);
+ this.props.loadChanges(this.props.databaseName);
+ }
+
+ showingSubsetMsg () {
+ const { isShowingSubset, changes } = this.props;
+ let msg = '';
+ if (isShowingSubset) {
+ let numChanges = changes.length;
+ msg = <p className="changes-result-limit">Limiting results to latest <b>{numChanges}</b> changes.</p>;
+ }
+ return msg;
+ }
+
+ getRows () {
+ const { changes, loaded, databaseName } = this.props;
+ if (!changes.length && loaded) {
+ return (
+ <p className="no-doc-changes">
+ There are no document changes to display.
+ </p>
+ );
+ }
+
+ return changes.map((change, i) => {
+ return <ChangeRow change={change} key={i} databaseName={databaseName} />;
+ });
+ }
+
+ render () {
+ return (
+ <div>
+ <div className="js-changes-view">
+ {this.showingSubsetMsg()}
+ {this.getRows()}
+ </div>
+ </div>
+ );
+ }
+}
+
+ChangesScreen.propTypes = {
+ changes: PropTypes.array.isRequired,
+ loaded: PropTypes.bool.isRequired,
+ databaseName: PropTypes.string.isRequired,
+ isShowingSubset: PropTypes.bool.isRequired,
+ loadChanges: PropTypes.func.isRequired
+};
diff --git a/app/addons/documents/changes/components/ChangesTabContent.js b/app/addons/documents/changes/components/ChangesTabContent.js
new file mode 100644
index 0000000..205fe44
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangesTabContent.js
@@ -0,0 +1,52 @@
+// 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 ReactComponents from '../../../components/react-components';
+import AddFilterForm from './AddFilterForm';
+
+export default class ChangesTabContent extends React.Component {
+ constructor (props) {
+ super(props);
+ this.addFilter = this.addFilter.bind(this);
+ this.hasFilter = this.hasFilter.bind(this);
+ }
+
+ addFilter (newFilter) {
+ if (_.isEmpty(newFilter)) {
+ return;
+ }
+ this.props.addFilter(newFilter);
+ }
+
+ hasFilter (filter) {
+ return this.props.filters.includes(filter);
+ }
+
+ render () {
+ return (
+ <div className="changes-header">
+ <AddFilterForm filter={(label) => this.props.removeFilter(label)} addFilter={this.addFilter}
+ hasFilter={this.hasFilter} />
+ <ReactComponents.BadgeList elements={this.props.filters} removeBadge={(label) => this.props.removeFilter(label)} />
+ </div>
+ );
+ }
+}
+
+ChangesTabContent.propTypes = {
+ filters: PropTypes.array.isRequired,
+ addFilter: PropTypes.func.isRequired,
+ removeFilter: PropTypes.func.isRequired
+};
diff --git a/app/addons/documents/changes/components/ChangesTabContentContainer.js b/app/addons/documents/changes/components/ChangesTabContentContainer.js
new file mode 100644
index 0000000..ed222b4
--- /dev/null
+++ b/app/addons/documents/changes/components/ChangesTabContentContainer.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import ChangesTabContent from './ChangesTabContent';
+import Actions from '../actions';
+
+const mapStateToProps = ({ changes }) => {
+ return {
+ filters: changes.filters
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ addFilter: (filter) => {
+ dispatch(Actions.addFilter(filter));
+ },
+
+ removeFilter: (filter) => {
+ dispatch(Actions.removeFilter(filter));
+ }
+ };
+};
+
+const ChangesTabContentContainer = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ChangesTabContent);
+
+export default ChangesTabContentContainer;
diff --git a/app/addons/documents/changes/reducers.js b/app/addons/documents/changes/reducers.js
new file mode 100644
index 0000000..8bcf9f6
--- /dev/null
+++ b/app/addons/documents/changes/reducers.js
@@ -0,0 +1,126 @@
+
+// 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 Helpers from '../helpers';
+
+const initialState = {
+ isLoaded: false,
+ filters: [],
+ changes: [],
+ filteredChanges: [],
+ maxChangesListed: 100,
+ showingSubset: false,
+ lastSequenceNum: null
+};
+
+function updateChanges(state, seqNum, changes) {
+ const newState = {
+ ...state,
+ // make a note of the most recent sequence number. This is used for a point of reference for polling for new changes
+ lastSequenceNum: seqNum,
+ isLoaded: true
+ };
+
+ // mark any additional changes that come after first page load as "new" so we can add a nice highlight effect
+ // when the new row is rendered
+ const firstBatch = newState.changes.length === 0;
+ newState.changes.forEach((change) => {
+ change.isNew = false;
+ });
+
+ const newChanges = changes.map((change) => {
+ const seq = Helpers.getSeqNum(change.seq);
+ return {
+ id: change.id,
+ seq: seq,
+ deleted: _.has(change, 'deleted') ? change.deleted : false,
+ changes: change.changes,
+ doc: change.doc, // only populated with ?include_docs=true
+ isNew: !firstBatch
+ };
+ });
+
+ // add the new changes to the start of the list
+ newState.changes = newChanges.concat(newState.changes);
+ updateFilteredChanges(newState);
+ return newState;
+}
+
+function addFilter(state, filter) {
+ const newFilters = state.filters.slice();
+ newFilters.push(filter);
+
+ const newState = {
+ ...state,
+ filters: newFilters
+ };
+ updateFilteredChanges(newState);
+ return newState;
+}
+
+function removeFilter(state, filter) {
+ const newFilters = state.filters.slice();
+ const idx = newFilters.indexOf(filter);
+ if (idx >= 0) {
+ newFilters.splice(idx, 1);
+ }
+
+ const newState = {
+ ...state,
+ filters: newFilters
+ };
+ updateFilteredChanges(newState);
+ return newState;
+}
+
+function updateFilteredChanges(state) {
+ state.showingSubset = false;
+ let numMatches = 0;
+ state.filteredChanges = state.changes.filter((change) => {
+ if (numMatches >= state.maxChangesListed) {
+ state.showingSubset = true;
+ return false;
+ }
+ let changeStr = JSON.stringify(change);
+ let match = state.filters.every((filter) => {
+ return new RegExp(filter, 'i').test(changeStr);
+ });
+
+ if (match) {
+ numMatches++;
+ }
+ return match;
+ });
+}
+
+export default function changes (state = initialState, action) {
+ switch (action.type) {
+
+ case ActionTypes.UPDATE_CHANGES:
+ // only bother updating the list of changes if the seq num has changed
+ if (state.lastSequenceNum !== action.seqNum) {
+ return updateChanges(state, action.seqNum, action.changes);
+ }
+ return state;
+
+ case ActionTypes.ADD_CHANGES_FILTER_ITEM:
+ return addFilter(state, action.filter);
+
+ case ActionTypes.REMOVE_CHANGES_FILTER_ITEM:
+ return removeFilter(state, action.filter);
+
+ default:
+ return state;
+ }
+}
diff --git a/app/addons/documents/changes/stores.js b/app/addons/documents/changes/stores.js
deleted file mode 100644
index 7c204ac..0000000
--- a/app/addons/documents/changes/stores.js
+++ /dev/null
@@ -1,157 +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 Helpers from "../helpers";
-
-
-var ChangesStore = FauxtonAPI.Store.extend({
- initialize: function () {
- this.reset();
- },
-
- reset: function () {
- this._isLoaded = false;
- this._filters = [];
- this._changes = [];
- this._databaseName = '';
- this._maxChangesListed = 100;
- this._showingSubset = false;
- this._lastSequenceNum = null;
- },
-
- initChanges: function (options) {
- this.reset();
- this._databaseName = options.databaseName;
- },
-
- isLoaded: function () {
- return this._isLoaded;
- },
-
- updateChanges: function (seqNum, changes) {
-
- // make a note of the most recent sequence number. This is used for a point of reference for polling for new changes
- this._lastSequenceNum = seqNum;
-
- // mark any additional changes that come after first page load as "new" so we can add a nice highlight effect
- // when the new row is rendered
- var firstBatch = this._changes.length === 0;
- _.each(this._changes, (change) => {
- change.isNew = false;
- });
-
- var newChanges = _.map(changes, (change) => {
- var seq = Helpers.getSeqNum(change.seq);
- return {
- id: change.id,
- seq: seq,
- deleted: _.has(change, 'deleted') ? change.deleted : false,
- changes: change.changes,
- doc: change.doc, // only populated with ?include_docs=true
- isNew: !firstBatch
- };
- });
-
- // add the new changes to the start of the list
- this._changes = newChanges.concat(this._changes);
- this._isLoaded = true;
- },
-
- getChanges: function () {
- this._showingSubset = false;
- var numMatches = 0;
-
- return _.filter(this._changes, (change) => {
- if (numMatches >= this._maxChangesListed) {
- this._showingSubset = true;
- return false;
- }
- var changeStr = JSON.stringify(change);
- var match = _.every(this._filters, (filter) => {
- return new RegExp(filter, 'i').test(changeStr);
- });
-
- if (match) {
- numMatches++;
- }
- return match;
- });
- },
-
- addFilter: function (filter) {
- this._filters.push(filter);
- },
-
- removeFilter: function (filter) {
- this._filters = _.without(this._filters, filter);
- },
-
- getFilters: function () {
- return this._filters;
- },
-
- hasFilter: function (filter) {
- return _.includes(this._filters, filter);
- },
-
- getDatabaseName: function () {
- return this._databaseName;
- },
-
- isShowingSubset: function () {
- return this._showingSubset;
- },
-
- // added to speed up the tests
- setMaxChanges: function (num) {
- this._maxChangesListed = num;
- },
-
- getLastSeqNum: function () {
- return this._lastSequenceNum;
- },
-
- dispatch: function (action) {
- switch (action.type) {
- case ActionTypes.INIT_CHANGES:
- this.initChanges(action.options);
- break;
-
- case ActionTypes.UPDATE_CHANGES:
- this.updateChanges(action.seqNum, action.changes);
- break;
-
- case ActionTypes.ADD_CHANGES_FILTER_ITEM:
- this.addFilter(action.filter);
- break;
-
- case ActionTypes.REMOVE_CHANGES_FILTER_ITEM:
- this.removeFilter(action.filter);
- break;
-
- default:
- return;
- }
-
- this.triggerChange();
- }
-});
-
-
-var Stores = {};
-Stores.changesStore = new ChangesStore();
-Stores.changesStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.changesStore.dispatch.bind(Stores.changesStore));
-
-export default Stores;
diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js
index bb11d66..3b921aa 100644
--- a/app/addons/documents/layouts.js
+++ b/app/addons/documents/layouts.js
@@ -16,7 +16,8 @@
import { NotificationCenterButton } from '../fauxton/notifications/notifications';
import SidebarControllerContainer from "./sidebar/SidebarControllerContainer";
import HeaderDocsLeft from './components/header-docs-left';
-import Changes from './changes/components';
+import ChangesContainer from './changes/components/ChangesContainer';
+import ChangesTabContentContainer from './changes/components/ChangesTabContentContainer';
import IndexEditorComponents from "./index-editor/components";
import DesignDocInfoContainer from './designdocinfo/components/DesignDocInfoContainer';
import RightAllDocsHeader from './components/header-docs-right';
@@ -105,7 +106,7 @@
showPartitionKeySelector: PropTypes.bool.isRequired,
partitionKey: PropTypes.string,
onPartitionKeySelected: PropTypes.func,
- onGlobalModeSelected: PropTypes.bool,
+ onGlobalModeSelected: PropTypes.func,
globalMode: PropTypes.bool
};
@@ -228,8 +229,8 @@
hideQueryOptions={true}
/>
<TabsSidebarContent
- upperContent={<Changes.ChangesTabContent />}
- lowerContent={<Changes.ChangesController />}
+ upperContent={<ChangesTabContentContainer />}
+ lowerContent={<ChangesContainer databaseName={dbName}/>}
hideFooter={true}
selectedNavItem={selectedNavItem}
/>
diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js
index e61fab7..9def382 100644
--- a/app/addons/documents/routes-documents.js
+++ b/app/addons/documents/routes-documents.js
@@ -13,7 +13,6 @@
import React from 'react';
import FauxtonAPI from '../../core/api';
import BaseRoute from './shared-routes';
-import ChangesActions from './changes/actions';
import Databases from '../databases/base';
import Resources from './resources';
import {SidebarItemSelection} from './sidebar/helpers';
@@ -131,9 +130,6 @@
},
changes: function () {
- ChangesActions.initChanges({
- databaseName: this.database.id
- });
const selectedNavItem = new SidebarItemSelection('changes');
return <ChangesSidebarLayout