Commit 69357aab authored by Kyle Keating's avatar Kyle Keating

Remove base namespace and getRootState wrapper

- Clean up the code and remove logic that groups non-namespaced modules into the base store.
- Deprecate the getRootState wrapper.
- Clean up createReducer (It no longer requires an array since reducers are namespaced.)
parent 599e8d75
......@@ -40,7 +40,7 @@ function AnnotationViewerContentController(
const id = $routeParams.id;
this.rootThread = () => rootThread.thread(store.getRootState());
this.rootThread = () => rootThread.thread(store.getState());
this.setCollapsed = function(id, collapsed) {
store.setCollapsed(id, collapsed);
......
......@@ -26,7 +26,7 @@ const countVisibleAnns = annThread => {
* annotations, and, in some cases, a mechanism for clearing the filter(s).
* */
function SearchStatusBar({ rootThread }) {
const thread = useStore(store => rootThread.thread(store.getRootState()));
const thread = useStore(store => rootThread.thread(store.getState()));
const actions = useStore(store => ({
clearSelection: store.clearSelection,
......@@ -45,13 +45,13 @@ function SearchStatusBar({ rootThread }) {
selectionMap,
selectedTab,
} = useStore(store => ({
directLinkedGroupFetchFailed: store.getRootState().directLinked
directLinkedGroupFetchFailed: store.getState().directLinked
.directLinkedGroupFetchFailed,
filterQuery: store.getRootState().selection.filterQuery,
filterQuery: store.getState().selection.filterQuery,
focusModeFocused: store.focusModeFocused(),
focusModeUserPrettyName: store.focusModeUserPrettyName(),
selectionMap: store.getRootState().selection.selectedAnnotationMap,
selectedTab: store.getRootState().selection.selectedTab,
selectionMap: store.getState().selection.selectedAnnotationMap,
selectedTab: store.getState().selection.selectedTab,
}));
// The search status bar UI represents multiple "modes" of filtering
......
......@@ -70,9 +70,7 @@ Tab.propTypes = {
*/
function SelectionTabs({ isLoading, settings, session }) {
const selectedTab = useStore(
store => store.getRootState().selection.selectedTab
);
const selectedTab = useStore(store => store.getState().selection.selectedTab);
const noteCount = useStore(store => store.noteCount());
const annotationCount = useStore(store => store.annotationCount());
const orphanCount = useStore(store => store.orphanCount());
......
......@@ -17,7 +17,7 @@ function SidebarContentController(
) {
const self = this;
this.rootThread = () => rootThread.thread(store.getRootState());
this.rootThread = () => rootThread.thread(store.getState());
function focusAnnotation(annotation) {
let highlights = [];
......@@ -41,11 +41,11 @@ function SidebarContentController(
* not the order in which they appear in the document.
*/
function firstSelectedAnnotation() {
if (store.getRootState().selection.selectedAnnotationMap) {
if (store.getState().selection.selectedAnnotationMap) {
const id = Object.keys(
store.getRootState().selection.selectedAnnotationMap
store.getState().selection.selectedAnnotationMap
)[0];
return store.getRootState().annotations.annotations.find(function(annot) {
return store.getState().annotations.annotations.find(function(annot) {
return annot.id === id;
});
} else {
......@@ -135,7 +135,7 @@ function SidebarContentController(
if (
this.selectedAnnotationUnavailable() ||
this.selectedGroupUnavailable() ||
store.getRootState().selection.filterQuery
store.getState().selection.filterQuery
) {
return false;
} else if (store.focusModeFocused()) {
......@@ -153,7 +153,7 @@ function SidebarContentController(
this.scrollTo = scrollToAnnotation;
this.selectedGroupUnavailable = function() {
return store.getRootState().directLinked.directLinkedGroupFetchFailed;
return store.getState().directLinked.directLinkedGroupFetchFailed;
};
this.selectedAnnotationUnavailable = function() {
......@@ -171,7 +171,7 @@ function SidebarContentController(
// If user has not landed on a direct linked annotation
// don't show the CTA.
if (!store.getRootState().directLinked.directLinkedAnnotationId) {
if (!store.getState().directLinked.directLinkedAnnotationId) {
return false;
}
......
......@@ -15,11 +15,11 @@ function SortMenu() {
setSortKey: store.setSortKey,
}));
// The currently-applied sort order
const sortKey = useStore(store => store.getRootState().selection.sortKey);
const sortKey = useStore(store => store.getState().selection.sortKey);
// All available sorting options. These change depending on current
// "tab" or context.
const sortKeysAvailable = useStore(
store => store.getRootState().selection.sortKeysAvailable
store => store.getState().selection.sortKeysAvailable
);
const menuItems = sortKeysAvailable.map(sortOption => {
......
......@@ -60,7 +60,7 @@ function StreamContentController(
fetch(20);
this.setCollapsed = store.setCollapsed;
this.rootThread = () => rootThread.thread(store.getRootState());
this.rootThread = () => rootThread.thread(store.getState());
// Sort the stream so that the newest annotations are at the top
store.setSortKey('Newest');
......
......@@ -20,8 +20,7 @@ describe('SearchStatusBar', () => {
thread: sinon.stub().returns({ children: [] }),
};
fakeStore = {
getState: sinon.stub(),
getRootState: sinon.stub().returns({
getState: sinon.stub().returns({
selection: {},
directLinked: {},
}),
......@@ -42,7 +41,7 @@ describe('SearchStatusBar', () => {
context('user search query is applied', () => {
beforeEach(() => {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
......@@ -179,7 +178,7 @@ describe('SearchStatusBar', () => {
],
});
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
......@@ -239,7 +238,7 @@ describe('SearchStatusBar', () => {
},
].forEach(test => {
it(test.description, () => {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: {
filterQuery: null,
selectedAnnotationMap: { annId: true },
......@@ -272,7 +271,7 @@ describe('SearchStatusBar', () => {
].forEach(test => {
it(`displays correct text for tab '${test.tab}', without count`, () => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: {
filterQuery: null,
selectedTab: test.tab,
......@@ -302,7 +301,7 @@ describe('SearchStatusBar', () => {
// Applied-query mode wins out here; no selection UI rendered
it('does not show selected-mode elements', () => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
......
......@@ -63,7 +63,7 @@ describe('SelectionTabs', function() {
noteCount: sinon.stub().returns(456),
orphanCount: sinon.stub().returns(0),
isWaitingToAnchorAnnotations: sinon.stub().returns(false),
getRootState: sinon.stub().returns({
getState: sinon.stub().returns({
selection: {
selectedTab: uiConstants.TAB_ANNOTATIONS,
},
......@@ -103,7 +103,7 @@ describe('SelectionTabs', function() {
});
it('should display notes tab as selected', function() {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
const wrapper = createDeepComponent({});
......@@ -112,7 +112,7 @@ describe('SelectionTabs', function() {
});
it('should display orphans tab as selected if there is 1 or more orphans', function() {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_ORPHANS },
});
fakeStore.orphanCount.returns(1);
......@@ -122,7 +122,7 @@ describe('SelectionTabs', function() {
});
it('should not display orphans tab if there are 0 orphans', function() {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_ORPHANS },
});
const wrapper = createDeepComponent({});
......@@ -147,7 +147,7 @@ describe('SelectionTabs', function() {
});
it('should not display the new-note-btn when the notes tab is active and the new-note-btn is disabled', function() {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
const wrapper = createComponent({});
......@@ -156,7 +156,7 @@ describe('SelectionTabs', function() {
it('should display the new-note-btn when the notes tab is active and the new-note-btn is enabled', function() {
fakeSettings.enableExperimentalNewNoteButton = true;
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
const wrapper = createComponent({});
......@@ -172,7 +172,7 @@ describe('SelectionTabs', function() {
});
it('should not display a message when its loading notes count is 0', function() {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
fakeStore.noteCount.returns(0);
......@@ -192,7 +192,7 @@ describe('SelectionTabs', function() {
});
it('should display the longer version of the no notes message when there are no notes', function() {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
fakeStore.noteCount.returns(0);
......@@ -205,7 +205,7 @@ describe('SelectionTabs', function() {
it('should display the prompt to create a note when there are no notes and enableExperimentalNewNoteButton is true', function() {
fakeSettings.enableExperimentalNewNoteButton = true;
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
fakeStore.noteCount.returns(0);
......@@ -242,7 +242,7 @@ describe('SelectionTabs', function() {
context('when the sidebar tutorial is displayed', function() {
it('should display the shorter version of the no notes message when there are no notes', function() {
fakeSession.state.preferences.show_sidebar_tutorial = true;
fakeStore.getRootState.returns({
fakeStore.getState.returns({
selection: { selectedTab: uiConstants.TAB_NOTES },
});
fakeStore.noteCount.returns(0);
......
......@@ -176,7 +176,7 @@ describe('sidebar.components.sidebar-content', function() {
}
it('generates the thread list', () => {
const thread = fakeRootThread.thread(store.getRootState());
const thread = fakeRootThread.thread(store.getState());
assert.equal(ctrl.rootThread(), thread);
});
......
......@@ -23,7 +23,7 @@ describe('SortMenu', () => {
};
fakeStore = {
setSortKey: sinon.stub(),
getRootState: sinon.stub().returns(fakeState),
getState: sinon.stub().returns(fakeState),
};
SortMenu.$imports.$mock({
......
......@@ -5,7 +5,7 @@ const angular = require('angular');
const events = require('../events');
function getExistingAnnotation(store, id) {
return store.getRootState().annotations.annotations.find(function(annot) {
return store.getState().annotations.annotations.find(function(annot) {
return annot.id === id;
});
}
......
......@@ -56,7 +56,7 @@ function FrameSync($rootScope, $window, Discovery, store, bridge) {
let prevPublicAnns = 0;
store.subscribe(function() {
const state = store.getRootState();
const state = store.getState();
if (
state.annotations.annotations === prevAnnotations &&
state.frames === prevFrames
......
......@@ -179,7 +179,7 @@ function groups(
// If there is a direct-linked annotation, fetch the annotation in case
// the associated group has not already been fetched and we need to make
// an additional request for it.
const directLinkedAnnId = store.getRootState().directLinked
const directLinkedAnnId = store.getState().directLinked
.directLinkedAnnotationId;
let directLinkedAnnApi = null;
if (directLinkedAnnId) {
......@@ -194,7 +194,7 @@ function groups(
// If there is a direct-linked group, add an API request to get that
// particular group since it may not be in the set of groups that are
// fetched by other requests.
const directLinkedGroupId = store.getRootState().directLinked
const directLinkedGroupId = store.getState().directLinked
.directLinkedGroupId;
let directLinkedGroupApi = null;
if (directLinkedGroupId) {
......
......@@ -127,7 +127,7 @@ function RootThread($rootScope, store, searchFilter, viewFilter) {
// logic in this event handler can be moved to the annotations reducer.
$rootScope.$on(events.GROUP_FOCUSED, function(event, focusedGroupId) {
const updatedAnnots = store
.getRootState()
.getState()
.annotations.annotations.filter(function(ann) {
return metadata.isNew(ann) && !metadata.isReply(ann);
})
......
......@@ -42,7 +42,7 @@ function serviceUrl(store, apiRoutes) {
});
return function(linkName, params) {
const links = store.getRootState().links;
const links = store.getState().links;
if (links === null) {
return '';
......
......@@ -106,7 +106,7 @@ function session(
* @return {Profile} The updated profile data
*/
function update(model) {
const prevSession = store.getRootState().session;
const prevSession = store.getState().session;
const userChanged = model.userid !== prevSession.userid;
// Update the session model used by the application
......@@ -184,7 +184,7 @@ function session(
// this service. In future, other services which access the session state
// will do so directly from store or via selector functions
get state() {
return store.getRootState().session;
return store.getState().session;
},
update,
......
......@@ -98,7 +98,7 @@ function Streamer(
} else if (message.type === 'session-change') {
handleSessionChangeNotification(message);
} else if (message.type === 'whoyouare') {
const userid = store.getRootState().session.userid;
const userid = store.getState().session.userid;
if (message.userid !== userid) {
console.warn(
'WebSocket user ID "%s" does not match logged-in ID "%s"',
......
......@@ -72,16 +72,16 @@ describe('groups', function() {
getGroup: sinon.stub(),
loadGroups: sinon.stub(),
allGroups() {
return this.getRootState().groups.groups;
return this.getState().groups.groups;
},
focusedGroup() {
return this.getRootState().groups.focusedGroup;
return this.getState().groups.focusedGroup;
},
mainFrame() {
return this.getRootState().frames[0];
return this.getState().frames[0];
},
focusedGroupId() {
const group = this.getRootState().groups.focusedGroup;
const group = this.getState().groups.focusedGroup;
return group ? group.id : null;
},
setDirectLinkedGroupFetchFailed: sinon.stub(),
......
......@@ -53,7 +53,7 @@ describe('rootThread', function() {
sortKeysAvailable: ['Location'],
},
},
getRootState: function() {
getState: function() {
return this.state;
},
......
......@@ -12,9 +12,6 @@ function fakeStore() {
getState: function() {
return { links: links };
},
getRootState: function() {
return { links: links };
},
};
}
......
......@@ -35,7 +35,7 @@ describe('sidebar.session', function() {
events: require('../analytics').events,
};
const fakeStore = {
getRootState: function() {
getState: function() {
return { session: state };
},
updateSession: function(session) {
......
......@@ -119,7 +119,7 @@ describe('Streamer', function() {
fakeStore = {
annotationExists: sinon.stub().returns(false),
clearPendingUpdates: sinon.stub(),
getRootState: sinon.stub().returns({
getState: sinon.stub().returns({
session: {
userid: 'jim@hypothes.is',
},
......@@ -408,7 +408,7 @@ describe('Streamer', function() {
},
].forEach(testCase => {
it('does nothing if the userid matches the logged-in userid', () => {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
session: {
userid: testCase.userid,
},
......@@ -435,7 +435,7 @@ describe('Streamer', function() {
},
].forEach(testCase => {
it('logs a warning if the userid does not match the logged-in userid', () => {
fakeStore.getRootState.returns({
fakeStore.getState.returns({
session: {
userid: testCase.userid,
},
......
......@@ -26,63 +26,33 @@ const { createReducer, bindSelectors } = require('./util');
* @param [any[]] middleware - List of additional Redux middlewares to use.
*/
function createStore(modules, initArgs = [], middleware = []) {
// Create the initial state and state update function. The "base"
// namespace is reserved for non-namespaced modules which will eventually
// be converted over.
// Create the initial state and state update function.
// Namespaced objects for initial states.
const initialState = {
base: null,
};
const initialState = {};
// Namespaced reducers from each module.
const allReducers = {
base: null,
};
const allReducers = {};
// Namespaced selectors from each module.
const allSelectors = {
base: {
selectors: {},
// Tells the bindSelector method to use local state for these selectors
// rather than the top level root state.
useLocalState: true,
},
};
// Temporary list of non-namespaced modules used for createReducer.
const baseModules = [];
const allSelectors = {};
// Iterate over each module and prep each module's:
// 1. state
// 2. reducers
// 3. selectors
//
// Modules that have no namespace get dumped into the "base" namespace.
//
modules.forEach(module => {
if (module.namespace) {
initialState[module.namespace] = module.init(...initArgs);
allReducers[module.namespace] = createReducer(module.update);
allSelectors[module.namespace] = {
selectors: module.selectors,
};
} else {
// No namespace
allSelectors.base.selectors = {
// Aggregate the selectors into a single "base" map
...allSelectors.base.selectors,
...module.selectors,
};
initialState.base = {
...initialState.base,
...module.init(...initArgs),
};
baseModules.push(module);
console.warn('Store module does not specify a namespace', module);
}
});
// Create the base reducer for modules that are not opting in for namespacing
allReducers.base = createReducer(...baseModules.map(m => m.update));
const defaultMiddleware = [
// The `thunk` middleware handles actions which are functions.
// This is used to implement actions which have side effects or are
......@@ -98,21 +68,10 @@ function createStore(modules, initArgs = [], middleware = []) {
enhancer
);
// Temporary wrapper while we use the "base" namespace. This allows getState
// to work as it did before. Under the covers the state is actually
// nested inside "base" namespace.
const getState = store.getState;
store.getState = () => getState().base;
// Because getState is overridden, we still need a fallback for the root state
// for the namespaced modules. They will temporarily use getRootState
// until all modules are namespaced and then this will be deprecated.
store.getRootState = () => getState();
// Add actions and selectors as methods to the store.
const actions = Object.assign({}, ...modules.map(m => m.actions));
const boundActions = redux.bindActionCreators(actions, store.dispatch);
const boundSelectors = bindSelectors(allSelectors, store.getRootState);
const boundSelectors = bindSelectors(allSelectors, store.getState);
Object.assign(store, boundActions, boundSelectors);
......
......@@ -57,7 +57,7 @@ describe('sidebar/store/modules/activity', () => {
});
it('defaults `activeAnnotationFetches` counter to zero', () => {
assert.equal(store.getRootState().activity.activeAnnotationFetches, 0);
assert.equal(store.getState().activity.activeAnnotationFetches, 0);
});
describe('annotationFetchFinished', () => {
......@@ -70,7 +70,7 @@ describe('sidebar/store/modules/activity', () => {
it('increments `activeAnnotationFetches` counter when a new annotation fetch is started', () => {
store.annotationFetchStarted();
assert.equal(store.getRootState().activity.activeAnnotationFetches, 1);
assert.equal(store.getState().activity.activeAnnotationFetches, 1);
});
});
......@@ -86,7 +86,7 @@ describe('sidebar/store/modules/activity', () => {
store.annotationFetchFinished();
assert.equal(store.getRootState().activity.activeAnnotationFetches, 0);
assert.equal(store.getState().activity.activeAnnotationFetches, 0);
});
});
......
......@@ -138,10 +138,7 @@ describe('sidebar/store/modules/annotations', function() {
store.dispatch(actions.addAnnotations([ann]));
store.dispatch(actions.hideAnnotation(ann.id));
const storeAnn = selectors.findAnnotationByID(
store.getRootState(),
ann.id
);
const storeAnn = selectors.findAnnotationByID(store.getState(), ann.id);
assert.equal(storeAnn.hidden, true);
});
});
......@@ -154,10 +151,7 @@ describe('sidebar/store/modules/annotations', function() {
store.dispatch(actions.addAnnotations([ann]));
store.dispatch(actions.unhideAnnotation(ann.id));
const storeAnn = selectors.findAnnotationByID(
store.getRootState(),
ann.id
);
const storeAnn = selectors.findAnnotationByID(store.getState(), ann.id);
assert.equal(storeAnn.hidden, false);
});
});
......@@ -168,7 +162,7 @@ describe('sidebar/store/modules/annotations', function() {
const ann = fixtures.defaultAnnotation();
store.dispatch(actions.addAnnotations([ann]));
store.dispatch(actions.removeAnnotations([ann]));
assert.equal(store.getRootState().annotations.annotations.length, 0);
assert.equal(store.getState().annotations.annotations.length, 0);
});
});
......@@ -226,10 +220,7 @@ describe('sidebar/store/modules/annotations', function() {
store.dispatch(actions.addAnnotations([ann]));
store.dispatch(actions.updateFlagStatus(ann.id, testCase.nowFlagged));
const storeAnn = selectors.findAnnotationByID(
store.getRootState(),
ann.id
);
const storeAnn = selectors.findAnnotationByID(store.getState(), ann.id);
assert.equal(storeAnn.flagged, testCase.nowFlagged);
assert.deepEqual(storeAnn.moderation, testCase.newModeration);
});
......@@ -242,7 +233,7 @@ describe('sidebar/store/modules/annotations', function() {
const ann = fixtures.oldAnnotation();
store.dispatch(actions.createAnnotation(ann));
assert.equal(
selectors.findAnnotationByID(store.getRootState(), ann.id).id,
selectors.findAnnotationByID(store.getState(), ann.id).id,
ann.id
);
});
......@@ -251,7 +242,7 @@ describe('sidebar/store/modules/annotations', function() {
const store = createStore();
store.dispatch(actions.createAnnotation(fixtures.oldAnnotation()));
assert.equal(
store.getRootState().selection.selectedTab,
store.getState().selection.selectedTab,
uiConstants.TAB_ANNOTATIONS
);
});
......@@ -260,7 +251,7 @@ describe('sidebar/store/modules/annotations', function() {
const store = createStore();
store.dispatch(actions.createAnnotation(fixtures.oldPageNote()));
assert.equal(
store.getRootState().selection.selectedTab,
store.getState().selection.selectedTab,
uiConstants.TAB_NOTES
);
});
......@@ -291,7 +282,7 @@ describe('sidebar/store/modules/annotations', function() {
tags: [],
})
);
assert.isTrue(store.getRootState().selection.expanded.annotation_id);
assert.isTrue(store.getState().selection.expanded.annotation_id);
});
});
});
......@@ -8,7 +8,7 @@ describe('sidebar/store/modules/direct-linked', () => {
let fakeSettings = {};
const getDirectLinkedState = () => {
return store.getRootState().directLinked;
return store.getState().directLinked;
};
beforeEach(() => {
......
......@@ -91,7 +91,7 @@ describe('sidebar/store/modules/groups', () => {
store.focusGroup(publicGroup.id);
assert.equal(store.getRootState().groups.focusedGroupId, publicGroup.id);
assert.equal(store.getState().groups.focusedGroupId, publicGroup.id);
assert.notCalled(console.error);
});
......@@ -100,7 +100,7 @@ describe('sidebar/store/modules/groups', () => {
store.focusGroup(privateGroup.id);
assert.equal(store.getRootState().groups.focusedGroupId, publicGroup.id);
assert.equal(store.getState().groups.focusedGroupId, publicGroup.id);
assert.called(console.error);
});
});
......@@ -108,7 +108,7 @@ describe('sidebar/store/modules/groups', () => {
describe('loadGroups', () => {
it('updates the set of groups', () => {
store.loadGroups([publicGroup]);
assert.deepEqual(store.getRootState().groups.groups, [publicGroup]);
assert.deepEqual(store.getState().groups.groups, [publicGroup]);
});
it('resets the focused group if not in new set of groups', () => {
......@@ -116,7 +116,7 @@ describe('sidebar/store/modules/groups', () => {
store.focusGroup(publicGroup.id);
store.loadGroups([]);
assert.equal(store.getRootState().groups.focusedGroupId, null);
assert.equal(store.getState().groups.focusedGroupId, null);
});
it('leaves focused group unchanged if in new set of groups', () => {
......@@ -124,7 +124,7 @@ describe('sidebar/store/modules/groups', () => {
store.focusGroup(publicGroup.id);
store.loadGroups([publicGroup, privateGroup]);
assert.equal(store.getRootState().groups.focusedGroupId, publicGroup.id);
assert.equal(store.getState().groups.focusedGroupId, publicGroup.id);
});
});
......@@ -134,7 +134,7 @@ describe('sidebar/store/modules/groups', () => {
store.clearGroups();
assert.equal(store.getRootState().groups.groups.length, 0);
assert.equal(store.getState().groups.groups.length, 0);
});
it('clears the focused group id', () => {
......@@ -143,7 +143,7 @@ describe('sidebar/store/modules/groups', () => {
store.clearGroups();
assert.equal(store.getRootState().groups.focusedGroupId, null);
assert.equal(store.getState().groups.focusedGroupId, null);
});
});
......
......@@ -10,7 +10,7 @@ describe('sidebar/store/modules/selection', () => {
let fakeSettings = [{}, {}];
const getSelectionState = () => {
return store.getRootState().selection;
return store.getState().selection;
};
beforeEach(() => {
......
......@@ -16,7 +16,7 @@ describe('sidebar/store/modules/session', function() {
it('updates the session state', function() {
const newSession = Object.assign(init(), { userid: 'john' });
store.updateSession({ userid: 'john' });
assert.deepEqual(store.getRootState().session, newSession);
assert.deepEqual(store.getState().session, newSession);
});
});
......
......@@ -22,12 +22,12 @@ describe('store/modules/viewer', function() {
it('sets a flag indicating that highlights are visible', function() {
store.setShowHighlights(true);
assert.isTrue(store.getRootState().viewer.visibleHighlights);
assert.isTrue(store.getState().viewer.visibleHighlights);
});
it('sets a flag indicating that highlights are not visible', function() {
store.setShowHighlights(false);
assert.isFalse(store.getRootState().viewer.visibleHighlights);
assert.isFalse(store.getState().viewer.visibleHighlights);
});
});
});
......@@ -2,55 +2,62 @@
const createStore = require('../create-store');
const BASE = 0;
const A = 0;
const modules = [
{
// base module
// namespaced module A
init(value = 0) {
return { count: value };
},
namespace: 'a',
update: {
INCREMENT_COUNTER: (state, action) => {
INCREMENT_COUNTER_A: (state, action) => {
return { count: state.count + action.amount };
},
RESET: () => {
return { count: 0 };
},
},
actions: {
increment(amount) {
return { type: 'INCREMENT_COUNTER', amount };
incrementA(amount) {
return { type: 'INCREMENT_COUNTER_A', amount };
},
},
selectors: {
getCount(state) {
return state.count;
getCountA(state) {
return state.a.count;
},
},
},
{
// namespaced module
// namespaced module B
init(value = 0) {
return { count: value };
},
namespace: 'foo',
namespace: 'b',
update: {
['INCREMENT_COUNTER_2'](state, action) {
INCREMENT_COUNTER_B: (state, action) => {
return { count: state.count + action.amount };
},
RESET: () => {
return { count: 0 };
},
},
actions: {
increment2(amount) {
return { type: 'INCREMENT_COUNTER_2', amount };
incrementB(amount) {
return { type: 'INCREMENT_COUNTER_B', amount };
},
},
selectors: {
getCount2(state) {
return state.foo.count;
getCountB(state) {
return state.b.count;
},
},
},
......@@ -60,50 +67,55 @@ function counterStore(initArgs = [], middleware = []) {
return createStore(modules, initArgs, middleware);
}
describe('sidebar.store.create-store', () => {
describe('sidebar/store/create-store', () => {
it('returns a working Redux store', () => {
const store = counterStore();
store.dispatch(modules[BASE].actions.increment(5));
assert.equal(store.getState().count, 5);
assert.equal(store.getState().a.count, 0);
});
it('dispatches bound actions', () => {
const store = counterStore();
store.incrementA(5);
assert.equal(store.getState().a.count, 5);
});
it('notifies subscribers when state changes', () => {
const store = counterStore();
const subscriber = sinon.spy(() => assert.equal(store.getCount(), 1));
const subscriber = sinon.spy(() => assert.equal(store.getCountA(), 1));
store.subscribe(subscriber);
store.increment(1);
store.incrementA(1);
assert.calledWith(subscriber);
});
it('passes initial state args to `init` function', () => {
const store = counterStore([21]);
assert.equal(store.getState().count, 21);
assert.equal(store.getState().a.count, 21);
});
it('adds actions as methods to the store', () => {
const store = counterStore();
store.increment(5);
assert.equal(store.getState().count, 5);
store.incrementA(5);
assert.equal(store.getState().a.count, 5);
});
it('adds selectors as methods to the store', () => {
const store = counterStore();
store.dispatch(modules[BASE].actions.increment(5));
assert.equal(store.getCount(), 5);
store.dispatch(modules[A].actions.incrementA(5));
assert.equal(store.getCountA(), 5);
});
it('applies `thunk` middleware by default', () => {
const store = counterStore();
const doubleAction = (dispatch, getState) => {
dispatch(modules[BASE].actions.increment(getState().base.count));
dispatch(modules[A].actions.incrementA(getState().a.count));
};
store.increment(5);
store.incrementA(5);
store.dispatch(doubleAction);
assert.equal(store.getCount(), 10);
assert.equal(store.getCountA(), 10);
});
it('applies additional middleware', () => {
......@@ -118,29 +130,35 @@ describe('sidebar.store.create-store', () => {
};
const store = counterStore([], [middleware]);
store.increment(5);
store.incrementA(5);
assert.deepEqual(actions, [{ type: 'INCREMENT_COUNTER', amount: 5 }]);
assert.deepEqual(actions, [{ type: 'INCREMENT_COUNTER_A', amount: 5 }]);
});
it('namespaced actions and selectors operate on their respective state', () => {
it('actions and selectors operate on their respective namespaced state', () => {
const store = counterStore();
store.increment2(6);
store.increment(5);
assert.equal(store.getCount2(), 6);
store.incrementB(6);
store.incrementA(5);
assert.equal(store.getCountB(), 6);
assert.equal(store.getCountA(), 5);
});
it('getState returns the base state', () => {
it('getState returns the top level root state', () => {
const store = counterStore();
store.increment(5);
assert.equal(store.getState().count, 5);
store.incrementA(5);
store.incrementB(6);
assert.equal(store.getState().a.count, 5);
assert.equal(store.getState().b.count, 6);
});
it('getRootState returns the top level root state', () => {
it('action can be handled across multiple reducers', () => {
const store = counterStore();
store.increment(5);
store.increment2(6);
assert.equal(store.getRootState().base.count, 5);
assert.equal(store.getRootState().foo.count, 6);
store.incrementA(1);
store.incrementB(1);
store.dispatch({
type: 'RESET',
});
assert.equal(store.getState().a.count, 0);
assert.equal(store.getState().b.count, 0);
});
});
This diff is collapsed.
......@@ -23,11 +23,9 @@ const fixtures = {
},
},
namespace2: {
useLocalState: true,
selectors: {
countAnnotations2: function(state) {
// useLocalState does not need namespaced path.
return state.annotations.length;
return state.namespace2.annotations.length;
},
},
},
......@@ -107,29 +105,6 @@ describe('reducer utils', function() {
});
});
it('applies update functions from each input object', () => {
const firstCounterActions = {
INCREMENT_COUNTER(state) {
return { firstCounter: state.firstCounter + 1 };
},
};
const secondCounterActions = {
INCREMENT_COUNTER(state) {
return { secondCounter: state.secondCounter + 1 };
},
};
const reducer = util.createReducer(
firstCounterActions,
secondCounterActions
);
const state = { firstCounter: 5, secondCounter: 10 };
const action = { type: 'INCREMENT_COUNTER' };
const newState = reducer(state, action);
assert.deepEqual(newState, { firstCounter: 6, secondCounter: 11 });
});
it('supports reducer functions that return an array', function() {
const action = {
type: 'FIRST_ITEM',
......@@ -154,13 +129,11 @@ describe('reducer utils', function() {
annotations: [{ id: 1 }],
},
namespace2: {
annotations: [{ id: 1 }],
annotations: [{ id: 2 }],
},
});
const bound = util.bindSelectors(fixtures.selectors, getState);
// Test non-namespaced selector (useLocalState=true)
assert.equal(bound.countAnnotations1(), 1);
// Test the namespaced selector
assert.equal(bound.countAnnotations2(), 1);
});
});
......
......@@ -14,34 +14,24 @@ function actionTypes(updateFns) {
* Given objects which map action names to update functions, this returns a
* reducer function that can be passed to the redux `createStore` function.
*
* @param {Object[]} actionToUpdateFn - Objects mapping action names to update
* @param {Object} actionToUpdateFn - Object mapping action names to update
* functions.
*/
function createReducer(...actionToUpdateFn) {
// Combine the (action name => update function) maps together into a single
// (action name => update functions) map.
//
// After namespace migration, remove the requirement for actionToUpdateFns
// to use arrays. Why? createReducer will be called once for each module rather
// than once for all modules.
const actionToUpdateFns = {};
actionToUpdateFn.forEach(map => {
Object.keys(map).forEach(k => {
actionToUpdateFns[k] = (actionToUpdateFns[k] || []).concat(map[k]);
});
});
function createReducer(actionToUpdateFn) {
return (state = {}, action) => {
const fns = actionToUpdateFns[action.type];
if (!fns) {
const fn = actionToUpdateFn[action.type];
if (!fn) {
return state;
}
// Some modules return an array rather than an object. They need to be
// handled differently so we don't cast them to an object.
if (Array.isArray(state)) {
return [...fns[0](state, action)];
return [...fn(state, action)];
}
return Object.assign({}, state, ...fns.map(f => f(state, action)));
return {
...state,
...fn(state, action),
};
};
}
......@@ -49,24 +39,16 @@ function createReducer(...actionToUpdateFn) {
* Takes a mapping of namespaced modules and the store's `getState()` function
* and returns an aggregated flat object with all the selectors at the root
* level. The keys to this object are functions that call the original
* selectors with the `state` argument set to the current value of `getState()`
* for namespaced modules or `getState().base` for non-namespaced modules.
* selectors with the `state` argument set to the current value of `getState()`.
*/
function bindSelectors(namespaces, getState) {
const totalSelectors = {};
Object.keys(namespaces).forEach(namespace => {
const selectors = namespaces[namespace].selectors;
const useLocalState = namespaces[namespace].useLocalState;
Object.keys(selectors).forEach(selector => {
totalSelectors[selector] = function() {
const args = [].slice.apply(arguments);
if (useLocalState) {
// Temporary scaffold until all selectors use namespaces.
args.unshift(getState()[namespace]);
} else {
// Namespace modules get root scope
args.unshift(getState());
}
args.unshift(getState());
return selectors[selector].apply(null, args);
};
});
......
......@@ -24,10 +24,6 @@ function fakeStore(initialState, methods) {
const store = redux.createStore(update, initialState);
store.getRootState = () => {
return store.getState();
};
store.setState = function(state) {
store.dispatch({ type: 'SET_STATE', state: state });
};
......
......@@ -71,20 +71,20 @@ describe('annotation threading', function() {
it('should display newly loaded annotations', function() {
store.addAnnotations(fixtures.annotations);
assert.equal(rootThread.thread(store.getRootState()).children.length, 2);
assert.equal(rootThread.thread(store.getState()).children.length, 2);
});
it('should not display unloaded annotations', function() {
store.addAnnotations(fixtures.annotations);
store.removeAnnotations(fixtures.annotations);
assert.equal(rootThread.thread(store.getRootState()).children.length, 0);
assert.equal(rootThread.thread(store.getState()).children.length, 0);
});
it('should filter annotations when a search is set', function() {
store.addAnnotations(fixtures.annotations);
store.setFilterQuery('second');
assert.equal(rootThread.thread(store.getRootState()).children.length, 1);
assert.equal(rootThread.thread(store.getRootState()).children[0].id, '2');
assert.equal(rootThread.thread(store.getState()).children.length, 1);
assert.equal(rootThread.thread(store.getState()).children[0].id, '2');
});
unroll(
......@@ -93,7 +93,7 @@ describe('annotation threading', function() {
store.addAnnotations(fixtures.annotations);
store.setSortKey(testCase.sortKey);
const actualOrder = rootThread
.thread(store.getRootState())
.thread(store.getState())
.children.map(function(thread) {
return thread.annotation.id;
});
......
......@@ -7,7 +7,9 @@ describe('state-util', function() {
let store;
beforeEach(function() {
store = fakeStore({ val: 0 });
store = fakeStore({
fake: { val: 0 },
});
});
describe('awaitStateChange()', function() {
......@@ -20,9 +22,7 @@ describe('state-util', function() {
it('should return promise that resolves to a non-null value', function() {
const expected = 5;
store.setState({ val: 5 });
return stateUtil
.awaitStateChange(store, getValWhenGreaterThanTwo)
.then(function(actual) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment