Commit ce7f1330 authored by Robert Knight's avatar Robert Knight

Refactor reducers into a map of action type -> update function

Split the large switch statements in each reducer function
into objects which map action types to state update functions.

These functions are then composed together with a `createReducer`
helper defined in `reducers/util`
parent f913a4b0
...@@ -10,18 +10,7 @@ var metadata = require('../annotation-metadata'); ...@@ -10,18 +10,7 @@ var metadata = require('../annotation-metadata');
var uiConstants = require('../ui-constants'); var uiConstants = require('../ui-constants');
var selection = require('./selection'); var selection = require('./selection');
var util = require('./util');
var actions = {
ADD_ANNOTATIONS: 'ADD_ANNOTATIONS',
REMOVE_ANNOTATIONS: 'REMOVE_ANNOTATIONS',
CLEAR_ANNOTATIONS: 'CLEAR_ANNOTATIONS',
/**
* Update an annotation's status flags after attempted anchoring in the
* document completes.
*/
UPDATE_ANCHOR_STATUS: 'UPDATE_ANCHOR_STATUS',
};
/** /**
* Return a copy of `current` with all matching annotations in `annotations` * Return a copy of `current` with all matching annotations in `annotations`
...@@ -87,88 +76,85 @@ function init() { ...@@ -87,88 +76,85 @@ function init() {
}; };
} }
function update(state, action) { var update = {
switch (action.type) { ADD_ANNOTATIONS: function (state, action) {
case actions.ADD_ANNOTATIONS: var updatedIDs = {};
{ var updatedTags = {};
var updatedIDs = {};
var updatedTags = {};
var added = []; var added = [];
var unchanged = []; var unchanged = [];
var updated = []; var updated = [];
action.annotations.forEach(function (annot) {
var existing;
if (annot.id) {
existing = findByID(state.annotations, annot.id);
}
if (!existing && annot.$$tag) {
existing = findByTag(state.annotations, annot.$$tag);
}
action.annotations.forEach(function (annot) { if (existing) {
var existing; // Merge the updated annotation with the private fields from the local
// annotation
updated.push(Object.assign({}, existing, annot));
if (annot.id) { if (annot.id) {
existing = findByID(state.annotations, annot.id); updatedIDs[annot.id] = true;
} }
if (!existing && annot.$$tag) { if (existing.$$tag) {
existing = findByTag(state.annotations, annot.$$tag); updatedTags[existing.$$tag] = true;
} }
} else {
added.push(initializeAnnot(annot));
}
});
if (existing) { state.annotations.forEach(function (annot) {
// Merge the updated annotation with the private fields from the local if (!updatedIDs[annot.id] && !updatedTags[annot.$$tag]) {
// annotation unchanged.push(annot);
updated.push(Object.assign({}, existing, annot)); }
if (annot.id) { });
updatedIDs[annot.id] = true;
}
if (existing.$$tag) {
updatedTags[existing.$$tag] = true;
}
} else {
added.push(initializeAnnot(annot));
}
});
state.annotations.forEach(function (annot) { return {annotations: added.concat(updated).concat(unchanged)};
if (!updatedIDs[annot.id] && !updatedTags[annot.$$tag]) { },
unchanged.push(annot);
}
});
return Object.assign({}, state, { REMOVE_ANNOTATIONS: function (state, action) {
annotations: added.concat(updated).concat(unchanged), var annots = excludeAnnotations(state.annotations, action.annotations);
}); var selectedTab = state.selectedTab;
if (selectedTab === uiConstants.TAB_ORPHANS &&
arrayUtil.countIf(annots, metadata.isOrphan) === 0) {
selectedTab = uiConstants.TAB_ANNOTATIONS;
} }
case actions.REMOVE_ANNOTATIONS:
{ var tabUpdateFn = selection.update.SELECT_TAB;
var annots = excludeAnnotations(state.annotations, action.annotations); return Object.assign(
var selectedTab = state.selectedTab; {annotations: annots},
if (selectedTab === uiConstants.TAB_ORPHANS && tabUpdateFn(state, selection.selectTab(selectedTab))
arrayUtil.countIf(annots, metadata.isOrphan) === 0) { );
selectedTab = uiConstants.TAB_ANNOTATIONS; },
CLEAR_ANNOTATIONS: function () {
return {annotations: []};
},
UPDATE_ANCHOR_STATUS: function (state, action) {
var annotations = state.annotations.map(function (annot) {
var match = (annot.id && annot.id === action.id) ||
(annot.$$tag && annot.$$tag === action.tag);
if (match) {
return Object.assign({}, annot, {
$orphan: action.isOrphan,
$$tag: action.tag,
});
} else {
return annot;
} }
return Object.assign( });
{}, return {annotations: annotations};
state, },
{annotations: annots}, };
selection.selectTabHelper(state, selectedTab)
); var actions = util.actionTypes(update);
}
case actions.CLEAR_ANNOTATIONS:
return Object.assign({}, state, {annotations: []});
case actions.UPDATE_ANCHOR_STATUS:
{
var annotations = state.annotations.map(function (annot) {
var match = (annot.id && annot.id === action.id) ||
(annot.$$tag && annot.$$tag === action.tag);
if (match) {
return Object.assign({}, annot, {
$orphan: action.isOrphan,
$$tag: action.tag,
});
} else {
return annot;
}
});
return Object.assign({}, state, {annotations: annotations});
}
default:
return state;
}
}
/** Add annotations to the currently displayed set. */ /** Add annotations to the currently displayed set. */
function addAnnotations(annotations, now) { function addAnnotations(annotations, now) {
......
...@@ -31,11 +31,11 @@ function init(settings) { ...@@ -31,11 +31,11 @@ function init(settings) {
); );
} }
var update = util.composeReducers([ var update = util.createReducer(Object.assign(
annotations.update, annotations.update,
selection.update, selection.update,
viewer.update, viewer.update
]); ));
module.exports = { module.exports = {
init: init, init: init,
......
...@@ -14,6 +14,8 @@ var immutable = require('seamless-immutable'); ...@@ -14,6 +14,8 @@ var immutable = require('seamless-immutable');
var uiConstants = require('../ui-constants'); var uiConstants = require('../ui-constants');
var util = require('./util');
/** /**
* Default starting tab. * Default starting tab.
*/ */
...@@ -58,43 +60,6 @@ function toSet(list) { ...@@ -58,43 +60,6 @@ function toSet(list) {
}, {}); }, {});
} }
/**
* Return state updates necessary to select a different tab.
*
* This function accepts the name of a tab and returns an object which must be
* merged into the current state to achieve the desired tab change.
*/
function selectTabHelper(state, newTab) {
// Do nothing if the "new tab" is not a valid tab.
if ([uiConstants.TAB_ANNOTATIONS,
uiConstants.TAB_NOTES,
uiConstants.TAB_ORPHANS].indexOf(newTab) === -1) {
return {};
}
// Shortcut if the tab is already correct, to avoid resetting the sortKey
// unnecessarily.
if (state.selectedTab === newTab) {
return {};
}
return {
selectedTab: newTab,
sortKey: TAB_SORTKEY_DEFAULT[newTab],
sortKeysAvailable: TAB_SORTKEYS_AVAILABLE[newTab],
};
}
var actions = {
CLEAR_SELECTION: 'CLEAR_SELECTION',
SELECT_ANNOTATIONS: 'SELECT_ANNOTATIONS',
FOCUS_ANNOTATIONS: 'FOCUS_ANNOTATIONS',
HIGHLIGHT_ANNOTATIONS: 'HIGHLIGHT_ANNOTATIONS',
SET_FORCE_VISIBLE: 'SET_FORCE_VISIBLE',
SET_EXPANDED: 'SET_EXPANDED',
SET_FILTER_QUERY: 'SET_FILTER_QUERY',
SET_SORT_KEY: 'SET_SORT_KEY',
SELECT_TAB: 'SELECT_TAB',
};
function init(settings) { function init(settings) {
return { return {
// Contains a map of annotation tag:true pairs. // Contains a map of annotation tag:true pairs.
...@@ -127,37 +92,64 @@ function init(settings) { ...@@ -127,37 +92,64 @@ function init(settings) {
}; };
} }
function update(state, action) { var update = {
switch (action.type) { CLEAR_SELECTION: function () {
case actions.CLEAR_SELECTION: return {filterQuery: null, selectedAnnotationMap: null};
return Object.assign({}, state, { },
filterQuery: null,
selectedAnnotationMap: null, SELECT_ANNOTATIONS: function (state, action) {
}); return {selectedAnnotationMap: action.selection};
case actions.SELECT_ANNOTATIONS: },
return Object.assign({}, state, {selectedAnnotationMap: action.selection});
case actions.FOCUS_ANNOTATIONS: FOCUS_ANNOTATIONS: function (state, action) {
return Object.assign({}, state, {focusedAnnotationMap: action.focused}); return {focusedAnnotationMap: action.focused};
case actions.SET_FORCE_VISIBLE: },
return Object.assign({}, state, {forceVisible: action.forceVisible});
case actions.SET_EXPANDED: SET_FORCE_VISIBLE: function (state, action) {
return Object.assign({}, state, {expanded: action.expanded}); return {forceVisible: action.forceVisible};
case actions.HIGHLIGHT_ANNOTATIONS: },
return Object.assign({}, state, {highlighted: action.highlighted});
case actions.SELECT_TAB: SET_EXPANDED: function (state, action) {
return Object.assign({}, state, selectTabHelper(state, action.tab)); return {expanded: action.expanded};
case actions.SET_FILTER_QUERY: },
return Object.assign({}, state, {
HIGHLIGHT_ANNOTATIONS: function (state, action) {
return {highlighted: action.highlighted};
},
SELECT_TAB: function (state, action) {
// Do nothing if the "new tab" is not a valid tab.
if ([uiConstants.TAB_ANNOTATIONS,
uiConstants.TAB_NOTES,
uiConstants.TAB_ORPHANS].indexOf(action.tab) === -1) {
return {};
}
// Shortcut if the tab is already correct, to avoid resetting the sortKey
// unnecessarily.
if (state.selectedTab === action.tab) {
return {};
}
return {
selectedTab: action.tab,
sortKey: TAB_SORTKEY_DEFAULT[action.tab],
sortKeysAvailable: TAB_SORTKEYS_AVAILABLE[action.tab],
};
},
SET_FILTER_QUERY: function (state, action) {
return {
filterQuery: action.query, filterQuery: action.query,
forceVisible: {}, forceVisible: {},
expanded: {}, expanded: {},
}); };
case actions.SET_SORT_KEY: },
return Object.assign({}, state, {sortKey: action.key});
default: SET_SORT_KEY: function (state, action) {
return state; return {sortKey: action.key};
} },
} };
var actions = util.actionTypes(update);
function select(annotations) { function select(annotations) {
return { return {
...@@ -325,7 +317,4 @@ module.exports = { ...@@ -325,7 +317,4 @@ module.exports = {
// Selectors // Selectors
hasSelectedAnnotations: hasSelectedAnnotations, hasSelectedAnnotations: hasSelectedAnnotations,
isAnnotationSelected: isAnnotationSelected, isAnnotationSelected: isAnnotationSelected,
// Helpers
selectTabHelper: selectTabHelper,
}; };
'use strict'; 'use strict';
/** /**
* Compose a list of `(state, action) => new state` reducer functions * Return an object where each key in `updateFns` is mapped to the key itself.
* into a single reducer.
*/ */
function composeReducers(reducers) { function actionTypes(updateFns) {
return Object.keys(updateFns).reduce(function (types, key) {
types[key] = key;
return types;
}, {});
}
/**
* Given an object which maps action names to update functions, this returns
* a reducer function that can be passed to the redux `createStore` function.
*/
function createReducer(updateFns) {
return function (state, action) { return function (state, action) {
return reducers.reduce(function (state, reducer) { var fn = updateFns[action.type];
return reducer(state, action); if (!fn) {
}, state); return state;
}
return Object.assign({}, state, fn(state, action));
}; };
} }
module.exports = { module.exports = {
composeReducers: composeReducers, actionTypes: actionTypes,
createReducer: createReducer,
}; };
'use strict'; 'use strict';
var util = require('./util');
/** /**
* This module defines actions and state related to the display mode of the * This module defines actions and state related to the display mode of the
* sidebar. * sidebar.
*/ */
var actions = {
/** Sets whether annotated text is highlighted in the page. */
SET_HIGHLIGHTS_VISIBLE: 'SET_HIGHLIGHTS_VISIBLE',
/**
* Set whether the app is the sidebar or not.
*
* When not in the sidebar, we do not expect annotations to anchor and always
* display all annotations, rather than only those in the current tab.
*/
SET_SIDEBAR: 'SET_SIDEBAR',
};
function init() { function init() {
return { return {
// Flag that indicates whether the app is the sidebar and connected to // Flag that indicates whether the app is the sidebar and connected to
...@@ -28,16 +17,16 @@ function init() { ...@@ -28,16 +17,16 @@ function init() {
}; };
} }
function update(state, action) { var update = {
switch (action.type) { SET_SIDEBAR: function (state, action) {
case actions.SET_SIDEBAR: return {isSidebar: action.isSidebar};
return Object.assign({}, state, {isSidebar: action.isSidebar}); },
case actions.SET_HIGHLIGHTS_VISIBLE: SET_HIGHLIGHTS_VISIBLE: function (state, action) {
return Object.assign({}, state, {visibleHighlights: action.visible}); return {visibleHighlights: action.visible};
default: },
return state; };
}
} var actions = util.actionTypes(update);
/** Set whether the app is the sidebar */ /** Set whether the app is the sidebar */
function setAppIsSidebar(isSidebar) { function setAppIsSidebar(isSidebar) {
......
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