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');
var uiConstants = require('../ui-constants');
var selection = require('./selection');
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',
};
var util = require('./util');
/**
* Return a copy of `current` with all matching annotations in `annotations`
......@@ -87,88 +76,85 @@ function init() {
};
}
function update(state, action) {
switch (action.type) {
case actions.ADD_ANNOTATIONS:
{
var updatedIDs = {};
var updatedTags = {};
var update = {
ADD_ANNOTATIONS: function (state, action) {
var updatedIDs = {};
var updatedTags = {};
var added = [];
var unchanged = [];
var updated = [];
var added = [];
var unchanged = [];
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) {
var existing;
if (existing) {
// Merge the updated annotation with the private fields from the local
// annotation
updated.push(Object.assign({}, existing, annot));
if (annot.id) {
existing = findByID(state.annotations, annot.id);
updatedIDs[annot.id] = true;
}
if (!existing && annot.$$tag) {
existing = findByTag(state.annotations, annot.$$tag);
if (existing.$$tag) {
updatedTags[existing.$$tag] = true;
}
} else {
added.push(initializeAnnot(annot));
}
});
if (existing) {
// Merge the updated annotation with the private fields from the local
// annotation
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) {
if (!updatedIDs[annot.id] && !updatedTags[annot.$$tag]) {
unchanged.push(annot);
}
});
state.annotations.forEach(function (annot) {
if (!updatedIDs[annot.id] && !updatedTags[annot.$$tag]) {
unchanged.push(annot);
}
});
return {annotations: added.concat(updated).concat(unchanged)};
},
return Object.assign({}, state, {
annotations: added.concat(updated).concat(unchanged),
});
REMOVE_ANNOTATIONS: function (state, action) {
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 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;
var tabUpdateFn = selection.update.SELECT_TAB;
return Object.assign(
{annotations: annots},
tabUpdateFn(state, selection.selectTab(selectedTab))
);
},
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(
{},
state,
{annotations: annots},
selection.selectTabHelper(state, selectedTab)
);
}
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;
}
}
});
return {annotations: annotations};
},
};
var actions = util.actionTypes(update);
/** Add annotations to the currently displayed set. */
function addAnnotations(annotations, now) {
......
......@@ -31,11 +31,11 @@ function init(settings) {
);
}
var update = util.composeReducers([
var update = util.createReducer(Object.assign(
annotations.update,
selection.update,
viewer.update,
]);
viewer.update
));
module.exports = {
init: init,
......
......@@ -14,6 +14,8 @@ var immutable = require('seamless-immutable');
var uiConstants = require('../ui-constants');
var util = require('./util');
/**
* Default starting tab.
*/
......@@ -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) {
return {
// Contains a map of annotation tag:true pairs.
......@@ -127,37 +92,64 @@ function init(settings) {
};
}
function update(state, action) {
switch (action.type) {
case actions.CLEAR_SELECTION:
return Object.assign({}, state, {
filterQuery: null,
selectedAnnotationMap: null,
});
case actions.SELECT_ANNOTATIONS:
return Object.assign({}, state, {selectedAnnotationMap: action.selection});
case actions.FOCUS_ANNOTATIONS:
return Object.assign({}, state, {focusedAnnotationMap: action.focused});
case actions.SET_FORCE_VISIBLE:
return Object.assign({}, state, {forceVisible: action.forceVisible});
case actions.SET_EXPANDED:
return Object.assign({}, state, {expanded: action.expanded});
case actions.HIGHLIGHT_ANNOTATIONS:
return Object.assign({}, state, {highlighted: action.highlighted});
case actions.SELECT_TAB:
return Object.assign({}, state, selectTabHelper(state, action.tab));
case actions.SET_FILTER_QUERY:
return Object.assign({}, state, {
var update = {
CLEAR_SELECTION: function () {
return {filterQuery: null, selectedAnnotationMap: null};
},
SELECT_ANNOTATIONS: function (state, action) {
return {selectedAnnotationMap: action.selection};
},
FOCUS_ANNOTATIONS: function (state, action) {
return {focusedAnnotationMap: action.focused};
},
SET_FORCE_VISIBLE: function (state, action) {
return {forceVisible: action.forceVisible};
},
SET_EXPANDED: function (state, action) {
return {expanded: action.expanded};
},
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,
forceVisible: {},
expanded: {},
});
case actions.SET_SORT_KEY:
return Object.assign({}, state, {sortKey: action.key});
default:
return state;
}
}
};
},
SET_SORT_KEY: function (state, action) {
return {sortKey: action.key};
},
};
var actions = util.actionTypes(update);
function select(annotations) {
return {
......@@ -325,7 +317,4 @@ module.exports = {
// Selectors
hasSelectedAnnotations: hasSelectedAnnotations,
isAnnotationSelected: isAnnotationSelected,
// Helpers
selectTabHelper: selectTabHelper,
};
'use strict';
/**
* Compose a list of `(state, action) => new state` reducer functions
* into a single reducer.
* Return an object where each key in `updateFns` is mapped to the key itself.
*/
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 reducers.reduce(function (state, reducer) {
return reducer(state, action);
}, state);
var fn = updateFns[action.type];
if (!fn) {
return state;
}
return Object.assign({}, state, fn(state, action));
};
}
module.exports = {
composeReducers: composeReducers,
actionTypes: actionTypes,
createReducer: createReducer,
};
'use strict';
var util = require('./util');
/**
* This module defines actions and state related to the display mode of the
* 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() {
return {
// Flag that indicates whether the app is the sidebar and connected to
......@@ -28,16 +17,16 @@ function init() {
};
}
function update(state, action) {
switch (action.type) {
case actions.SET_SIDEBAR:
return Object.assign({}, state, {isSidebar: action.isSidebar});
case actions.SET_HIGHLIGHTS_VISIBLE:
return Object.assign({}, state, {visibleHighlights: action.visible});
default:
return state;
}
}
var update = {
SET_SIDEBAR: function (state, action) {
return {isSidebar: action.isSidebar};
},
SET_HIGHLIGHTS_VISIBLE: function (state, action) {
return {visibleHighlights: action.visible};
},
};
var actions = util.actionTypes(update);
/** Set whether the app is the sidebar */
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