Commit 13914b8d authored by Robert Knight's avatar Robert Knight

Add missing types to annotation and route store modules

 - Add state and action types to reducers

 - Replace Array.forEach calls with for..of loops to enable better type
   inference.

 - Replace plain objects used as sets with ES sets
parent 31ac4c8a
...@@ -3,7 +3,17 @@ ...@@ -3,7 +3,17 @@
* sidebar. * sidebar.
*/ */
import { createSelector } from 'reselect';
import * as metadata from '../../helpers/annotation-metadata';
import { isSaved } from '../../helpers/annotation-metadata';
import { countIf, toTrueMap, trueKeys } from '../../util/collections';
import { createStoreModule, makeAction } from '../create-store';
import { routeModule } from './route';
/** /**
* @typedef {'anchored'|'orphan'|'timeout'} AnchorStatus
* @typedef {import('../../../types/api').Annotation} Annotation * @typedef {import('../../../types/api').Annotation} Annotation
* @typedef {import('../../../types/api').SavedAnnotation} SavedAnnotation * @typedef {import('../../../types/api').SavedAnnotation} SavedAnnotation
*/ */
...@@ -15,16 +25,6 @@ ...@@ -15,16 +25,6 @@
* @prop {string} [$tag] - local-generated identifier * @prop {string} [$tag] - local-generated identifier
*/ */
import { createSelector } from 'reselect';
import * as metadata from '../../helpers/annotation-metadata';
import { isSaved } from '../../helpers/annotation-metadata';
import { countIf, toTrueMap, trueKeys } from '../../util/collections';
import * as util from '../util';
import { createStoreModule } from '../create-store';
import { routeModule } from './route';
/** /**
* Return a copy of `current` with all matching annotations in `annotations` * Return a copy of `current` with all matching annotations in `annotations`
* removed (matched on identifier—`id` or `$tag`) * removed (matched on identifier—`id` or `$tag`)
...@@ -36,19 +36,19 @@ import { routeModule } from './route'; ...@@ -36,19 +36,19 @@ import { routeModule } from './route';
* @param {AnnotationStub[]} annotations * @param {AnnotationStub[]} annotations
*/ */
function excludeAnnotations(current, annotations) { function excludeAnnotations(current, annotations) {
const ids = {}; const ids = new Set();
const tags = {}; const tags = new Set();
annotations.forEach(annot => { for (let annot of annotations) {
if (annot.id) { if (annot.id) {
ids[annot.id] = true; ids.add(annot.id);
} }
if (annot.$tag) { if (annot.$tag) {
tags[annot.$tag] = true; tags.add(annot.$tag);
} }
}); }
return current.filter(annot => { return current.filter(annot => {
const shouldRemove = const shouldRemove =
(annot.id && annot.id in ids) || (annot.$tag && annot.$tag in tags); (annot.id && ids.has(annot.id)) || (annot.$tag && tags.has(annot.$tag));
return !shouldRemove; return !shouldRemove;
}); });
} }
...@@ -125,17 +125,20 @@ const initialState = { ...@@ -125,17 +125,20 @@ const initialState = {
/** @typedef {typeof initialState} State */ /** @typedef {typeof initialState} State */
const reducers = { const reducers = {
/** @param {State} state */ /**
* @param {State} state
* @param {{ annotations: Annotation[], currentAnnotationCount: number }} action
*/
ADD_ANNOTATIONS(state, action) { ADD_ANNOTATIONS(state, action) {
const updatedIDs = {}; const updatedIDs = new Set();
const updatedTags = {}; const updatedTags = new Set();
const added = []; const added = [];
const unchanged = []; const unchanged = [];
const updated = []; const updated = [];
let nextTag = state.nextTag; let nextTag = state.nextTag;
action.annotations.forEach(annot => { for (let annot of action.annotations) {
let existing; let existing;
if (annot.id) { if (annot.id) {
existing = findByID(state.annotations, annot.id); existing = findByID(state.annotations, annot.id);
...@@ -149,22 +152,22 @@ const reducers = { ...@@ -149,22 +152,22 @@ const reducers = {
// annotation // annotation
updated.push(Object.assign({}, existing, annot)); updated.push(Object.assign({}, existing, annot));
if (annot.id) { if (annot.id) {
updatedIDs[annot.id] = true; updatedIDs.add(annot.id);
} }
if (existing.$tag) { if (existing.$tag) {
updatedTags[existing.$tag] = true; updatedTags.add(existing.$tag);
} }
} else { } else {
added.push(initializeAnnotation(annot, 't' + nextTag)); added.push(initializeAnnotation(annot, 't' + nextTag));
++nextTag; ++nextTag;
} }
}); }
state.annotations.forEach(annot => { for (let annot of state.annotations) {
if (!updatedIDs[annot.id] && !updatedTags[annot.$tag]) { if (!updatedIDs.has(annot.id) && !updatedTags.has(annot.$tag)) {
unchanged.push(annot); unchanged.push(annot);
} }
}); }
return { return {
annotations: added.concat(updated).concat(unchanged), annotations: added.concat(updated).concat(unchanged),
...@@ -176,12 +179,18 @@ const reducers = { ...@@ -176,12 +179,18 @@ const reducers = {
return { annotations: [], focused: {}, highlighted: {} }; return { annotations: [], focused: {}, highlighted: {} };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ focusedTags: string[] }} action
*/
FOCUS_ANNOTATIONS(state, action) { FOCUS_ANNOTATIONS(state, action) {
return { focused: toTrueMap(action.focusedTags) }; return { focused: toTrueMap(action.focusedTags) };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ id: string }} action
*/
HIDE_ANNOTATION(state, action) { HIDE_ANNOTATION(state, action) {
const anns = state.annotations.map(ann => { const anns = state.annotations.map(ann => {
if (ann.id !== action.id) { if (ann.id !== action.id) {
...@@ -192,19 +201,28 @@ const reducers = { ...@@ -192,19 +201,28 @@ const reducers = {
return { annotations: anns }; return { annotations: anns };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ highlighted: Record<string, boolean> }} action
*/
HIGHLIGHT_ANNOTATIONS(state, action) { HIGHLIGHT_ANNOTATIONS(state, action) {
return { highlighted: action.highlighted }; return { highlighted: action.highlighted };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ annotationsToRemove: AnnotationStub[], remainingAnnotations: Annotation[] }} action
*/
REMOVE_ANNOTATIONS(state, action) { REMOVE_ANNOTATIONS(state, action) {
return { return {
annotations: [...action.remainingAnnotations], annotations: [...action.remainingAnnotations],
}; };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ id: string }} action
*/
UNHIDE_ANNOTATION(state, action) { UNHIDE_ANNOTATION(state, action) {
const anns = state.annotations.map(ann => { const anns = state.annotations.map(ann => {
if (ann.id !== action.id) { if (ann.id !== action.id) {
...@@ -215,7 +233,10 @@ const reducers = { ...@@ -215,7 +233,10 @@ const reducers = {
return { annotations: anns }; return { annotations: anns };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ statusUpdates: Record<string, AnchorStatus> }} action
*/
UPDATE_ANCHOR_STATUS(state, action) { UPDATE_ANCHOR_STATUS(state, action) {
const annotations = state.annotations.map(annot => { const annotations = state.annotations.map(annot => {
if (!action.statusUpdates.hasOwnProperty(annot.$tag)) { if (!action.statusUpdates.hasOwnProperty(annot.$tag)) {
...@@ -232,7 +253,10 @@ const reducers = { ...@@ -232,7 +253,10 @@ const reducers = {
return { annotations }; return { annotations };
}, },
/** @param {State} state */ /**
* @param {State} state
* @param {{ id: string, isFlagged: boolean }} action
*/
UPDATE_FLAG_STATUS(state, action) { UPDATE_FLAG_STATUS(state, action) {
const annotations = state.annotations.map(annot => { const annotations = state.annotations.map(annot => {
const match = annot.id && annot.id === action.id; const match = annot.id && annot.id === action.id;
...@@ -260,8 +284,6 @@ const reducers = { ...@@ -260,8 +284,6 @@ const reducers = {
}, },
}; };
const actions = util.actionTypes(reducers);
/* Action creators */ /* Action creators */
/** /**
...@@ -270,6 +292,10 @@ const actions = util.actionTypes(reducers); ...@@ -270,6 +292,10 @@ const actions = util.actionTypes(reducers);
* @param {Annotation[]} annotations - Array of annotation objects to add. * @param {Annotation[]} annotations - Array of annotation objects to add.
*/ */
function addAnnotations(annotations) { function addAnnotations(annotations) {
/**
* @param {import('redux').Dispatch} dispatch
* @param {() => { annotations: State, route: import('./route').State }} getState
*/
return function (dispatch, getState) { return function (dispatch, getState) {
const added = annotations.filter(annot => { const added = annotations.filter(annot => {
return ( return (
...@@ -277,11 +303,12 @@ function addAnnotations(annotations) { ...@@ -277,11 +303,12 @@ function addAnnotations(annotations) {
); );
}); });
dispatch({ dispatch(
type: actions.ADD_ANNOTATIONS, makeAction(reducers, 'ADD_ANNOTATIONS', {
annotations, annotations,
currentAnnotationCount: getState().annotations.annotations.length, currentAnnotationCount: getState().annotations.annotations.length,
}); })
);
// If we're not in the sidebar, we're done here. // If we're not in the sidebar, we're done here.
// FIXME Split the annotation-adding from the anchoring code; possibly // FIXME Split the annotation-adding from the anchoring code; possibly
...@@ -322,7 +349,7 @@ function addAnnotations(annotations) { ...@@ -322,7 +349,7 @@ function addAnnotations(annotations) {
/** Set the currently displayed annotations to the empty set. */ /** Set the currently displayed annotations to the empty set. */
function clearAnnotations() { function clearAnnotations() {
return { type: actions.CLEAR_ANNOTATIONS }; return makeAction(reducers, 'CLEAR_ANNOTATIONS', undefined);
} }
/** /**
...@@ -333,10 +360,7 @@ function clearAnnotations() { ...@@ -333,10 +360,7 @@ function clearAnnotations() {
* @param {string[]} tags - Identifiers of annotations to focus * @param {string[]} tags - Identifiers of annotations to focus
*/ */
function focusAnnotations(tags) { function focusAnnotations(tags) {
return { return makeAction(reducers, 'FOCUS_ANNOTATIONS', { focusedTags: tags });
type: actions.FOCUS_ANNOTATIONS,
focusedTags: tags,
};
} }
/** /**
...@@ -348,10 +372,7 @@ function focusAnnotations(tags) { ...@@ -348,10 +372,7 @@ function focusAnnotations(tags) {
* @param {string} id * @param {string} id
*/ */
function hideAnnotation(id) { function hideAnnotation(id) {
return { return makeAction(reducers, 'HIDE_ANNOTATION', { id });
type: actions.HIDE_ANNOTATION,
id,
};
} }
/** /**
...@@ -365,10 +386,9 @@ function hideAnnotation(id) { ...@@ -365,10 +386,9 @@ function hideAnnotation(id) {
* @param {string[]} ids - annotations to highlight * @param {string[]} ids - annotations to highlight
*/ */
function highlightAnnotations(ids) { function highlightAnnotations(ids) {
return { return makeAction(reducers, 'HIGHLIGHT_ANNOTATIONS', {
type: actions.HIGHLIGHT_ANNOTATIONS,
highlighted: toTrueMap(ids), highlighted: toTrueMap(ids),
}; });
} }
/** /**
...@@ -379,16 +399,21 @@ function highlightAnnotations(ids) { ...@@ -379,16 +399,21 @@ function highlightAnnotations(ids) {
* only contain an `id` property. * only contain an `id` property.
*/ */
export function removeAnnotations(annotations) { export function removeAnnotations(annotations) {
/**
* @param {import('redux').Dispatch} dispatch
* @param {() => { annotations: State }} getState
*/
return (dispatch, getState) => { return (dispatch, getState) => {
const remainingAnnotations = excludeAnnotations( const remainingAnnotations = excludeAnnotations(
getState().annotations.annotations, getState().annotations.annotations,
annotations annotations
); );
dispatch({ dispatch(
type: actions.REMOVE_ANNOTATIONS, makeAction(reducers, 'REMOVE_ANNOTATIONS', {
annotationsToRemove: annotations, annotationsToRemove: annotations,
remainingAnnotations, remainingAnnotations,
}); })
);
}; };
} }
...@@ -401,23 +426,16 @@ export function removeAnnotations(annotations) { ...@@ -401,23 +426,16 @@ export function removeAnnotations(annotations) {
* @param {string} id * @param {string} id
*/ */
function unhideAnnotation(id) { function unhideAnnotation(id) {
return { return makeAction(reducers, 'UNHIDE_ANNOTATION', { id });
type: actions.UNHIDE_ANNOTATION,
id,
};
} }
/** /**
* Update the anchoring status of an annotation * Update the anchoring status of an annotation
* *
* @param {{ [tag: string]: 'anchored'|'orphan'|'timeout'} } statusUpdates - A * @param {Record<string, AnchorStatus>} statusUpdates - Map of annotation tag to orphan status
* map of annotation tag to orphan status
*/ */
function updateAnchorStatus(statusUpdates) { function updateAnchorStatus(statusUpdates) {
return { return makeAction(reducers, 'UPDATE_ANCHOR_STATUS', { statusUpdates });
type: actions.UPDATE_ANCHOR_STATUS,
statusUpdates,
};
} }
/** /**
...@@ -429,11 +447,7 @@ function updateAnchorStatus(statusUpdates) { ...@@ -429,11 +447,7 @@ function updateAnchorStatus(statusUpdates) {
* *
*/ */
function updateFlagStatus(id, isFlagged) { function updateFlagStatus(id, isFlagged) {
return { return makeAction(reducers, 'UPDATE_FLAG_STATUS', { id, isFlagged });
type: actions.UPDATE_FLAG_STATUS,
id,
isFlagged,
};
} }
/* Selectors */ /* Selectors */
...@@ -487,12 +501,12 @@ function findAnnotationByID(state, id) { ...@@ -487,12 +501,12 @@ function findAnnotationByID(state, id) {
*/ */
function findIDsForTags(state, tags) { function findIDsForTags(state, tags) {
const ids = []; const ids = [];
tags.forEach(tag => { for (let tag of tags) {
const annot = findByTag(state.annotations, tag); const annot = findByTag(state.annotations, tag);
if (annot && annot.id) { if (annot && annot.id) {
ids.push(annot.id); ids.push(annot.id);
} }
}); }
return ids; return ids;
} }
......
import { actionTypes } from '../util'; import { createStoreModule, makeAction } from '../create-store';
import { createStoreModule } from '../create-store';
/** /**
* @typedef {'annotation'|'notebook'|'sidebar'|'stream'} RouteName * @typedef {'annotation'|'notebook'|'sidebar'|'stream'} RouteName
...@@ -26,30 +24,32 @@ const initialState = { ...@@ -26,30 +24,32 @@ const initialState = {
params: {}, params: {},
}; };
/** @typedef {typeof initialState} State */
const reducers = { const reducers = {
/**
* @param {State} state
* @param {{ name: RouteName, params: Record<string, string> }} action
*/
CHANGE_ROUTE(state, { name, params }) { CHANGE_ROUTE(state, { name, params }) {
return { name, params }; return { name, params };
}, },
}; };
const actions = actionTypes(reducers);
/** /**
* Change the active route. * Change the active route.
* *
* @param {string} name - Name of the route to activate. See `initialState` for possible values * @param {RouteName} name - Name of the route to activate. See `initialState` for possible values
* @param {Record<string,string>} params - Parameters associated with the route * @param {Record<string,string>} params - Parameters associated with the route
*/ */
function changeRoute(name, params = {}) { function changeRoute(name, params = {}) {
return { return makeAction(reducers, 'CHANGE_ROUTE', { name, params });
type: actions.CHANGE_ROUTE,
name,
params,
};
} }
/** /**
* Return the name of the current route. * Return the name of the current route.
*
* @param {State} state
*/ */
function route(state) { function route(state) {
return state.name; return state.name;
...@@ -58,6 +58,8 @@ function route(state) { ...@@ -58,6 +58,8 @@ function route(state) {
/** /**
* Return any parameters for the current route, extracted from the path and * Return any parameters for the current route, extracted from the path and
* query string. * query string.
*
* @param {State} state
*/ */
function routeParams(state) { function routeParams(state) {
return state.params; return state.params;
......
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