Commit f39b1c4f authored by Robert Knight's avatar Robert Knight

Convert remaining sidebar store modules to TypeScript

This completes the conversion of the sidebar's non-test files from JSDoc types
to native TypeScript syntax.
parent 631372ee
import { replaceURLParams } from '../../util/url'; import { replaceURLParams } from '../../util/url';
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
const initialState = /** @type {Record<string, string>|null} */ (null); export type State = Record<string, string> | null;
/** @typedef {typeof initialState} State */ const initialState: State = null;
const reducers = { const reducers = {
/** UPDATE_LINKS(state: State, action: { links: Record<string, string> }) {
* @param {State} state
* @param {{ links: Record<string, string> }} action
*/
UPDATE_LINKS(state, action) {
return { return {
...action.links, ...action.links,
}; };
...@@ -20,9 +16,9 @@ const reducers = { ...@@ -20,9 +16,9 @@ const reducers = {
/** /**
* Update the link map * Update the link map
* *
* @param {Record<string, string>} links - Link map fetched from the `/api/links` endpoint * @param links - Link map fetched from the `/api/links` endpoint
*/ */
function updateLinks(links) { function updateLinks(links: Record<string, string>) {
return makeAction(reducers, 'UPDATE_LINKS', { links }); return makeAction(reducers, 'UPDATE_LINKS', { links });
} }
...@@ -30,12 +26,12 @@ function updateLinks(links) { ...@@ -30,12 +26,12 @@ function updateLinks(links) {
* Render a service link (URL) using the given `params` * Render a service link (URL) using the given `params`
* *
* Returns an empty string if links have not been fetched yet. * Returns an empty string if links have not been fetched yet.
*
* @param {State} state
* @param {string} linkName
* @param {Record<string, string>} [params]
*/ */
function getLink(state, linkName, params = {}) { function getLink(
state: State,
linkName: string,
params: Record<string, string> = {},
) {
if (!state) { if (!state) {
return ''; return '';
} }
...@@ -51,7 +47,7 @@ function getLink(state, linkName, params = {}) { ...@@ -51,7 +47,7 @@ function getLink(state, linkName, params = {}) {
return url; return url;
} }
export const linksModule = createStoreModule(initialState, { export const linksModule = createStoreModule(initialState as State, {
namespace: 'links', namespace: 'links',
reducers, reducers,
actionCreators: { actionCreators: {
......
...@@ -2,53 +2,47 @@ ...@@ -2,53 +2,47 @@
* This module contains state related to real-time updates received via the * This module contains state related to real-time updates received via the
* WebSocket connection to h's real-time API. * WebSocket connection to h's real-time API.
*/ */
import type { Dispatch } from 'redux';
/**
* @typedef {import('../../../types/api').Annotation} Annotation
* @typedef {import('./annotations').State} AnnotationsState
* @typedef {import('./groups').State} GroupsState
* @typedef {import('./route').State} RouteState
*/
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { hasOwn } from '../../../shared/has-own'; import { hasOwn } from '../../../shared/has-own';
import type { Annotation } from '../../../types/api';
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
import type { State as AnnotationsState } from './annotations';
import { annotationsModule } from './annotations'; import { annotationsModule } from './annotations';
import type { State as GroupsState } from './groups';
import { groupsModule } from './groups'; import { groupsModule } from './groups';
import type { State as RouteState } from './route';
import { routeModule } from './route'; import { routeModule } from './route';
/** export type AnnotationMap = Record<string, Annotation>;
* @typedef {Record<string, Annotation>} AnnotationMap export type BooleanMap = Record<string, boolean>;
* @typedef {Record<string, boolean>} BooleanMap
*/
const initialState = { export type State = {
/** /**
* Map of ID -> updated annotation for updates that have been received over * Map of ID -> updated annotation for updates that have been received over
* the WebSocket but not yet applied (ie. saved to the "annotations" store * the WebSocket but not yet applied (ie. saved to the "annotations" store
* module and shown in the UI). * module and shown in the UI).
*
* @type {AnnotationMap}
*/ */
pendingUpdates: {}, pendingUpdates: AnnotationMap;
/** /**
* Set of IDs of annotations which have been deleted but for which the * Set of IDs of annotations which have been deleted but for which the
* deletion has not yet been applied * deletion has not yet been applied
*
* @type {BooleanMap}
*/ */
pendingDeletions: {}, pendingDeletions: BooleanMap;
}; };
/** @typedef {typeof initialState} State */ const initialState: State = {
pendingUpdates: {},
pendingDeletions: {},
};
const reducers = { const reducers = {
/** RECEIVE_REAL_TIME_UPDATES(
* @param {State} state state: State,
* @param {{ pendingUpdates: AnnotationMap, pendingDeletions: BooleanMap }} action action: { pendingUpdates: AnnotationMap; pendingDeletions: BooleanMap },
*/ ) {
RECEIVE_REAL_TIME_UPDATES(state, action) {
return { return {
pendingUpdates: { ...action.pendingUpdates }, pendingUpdates: { ...action.pendingUpdates },
pendingDeletions: { ...action.pendingDeletions }, pendingDeletions: { ...action.pendingDeletions },
...@@ -59,11 +53,10 @@ const reducers = { ...@@ -59,11 +53,10 @@ const reducers = {
return { pendingUpdates: {}, pendingDeletions: {} }; return { pendingUpdates: {}, pendingDeletions: {} };
}, },
/** ADD_ANNOTATIONS(
* @param {State} state state: State,
* @param {{ annotations: Annotation[] }} action { annotations }: { annotations: Annotation[] },
*/ ) {
ADD_ANNOTATIONS(state, { annotations }) {
// Discard any pending updates which conflict with an annotation added // Discard any pending updates which conflict with an annotation added
// locally or fetched via an API call. // locally or fetched via an API call.
// //
...@@ -72,7 +65,7 @@ const reducers = { ...@@ -72,7 +65,7 @@ const reducers = {
// annotation that has been deleted on the server. // annotation that has been deleted on the server.
const pendingUpdates = { ...state.pendingUpdates }; const pendingUpdates = { ...state.pendingUpdates };
for (let ann of annotations) { for (const ann of annotations) {
if (ann.id) { if (ann.id) {
delete pendingUpdates[ann.id]; delete pendingUpdates[ann.id];
} }
...@@ -81,18 +74,17 @@ const reducers = { ...@@ -81,18 +74,17 @@ const reducers = {
return { pendingUpdates }; return { pendingUpdates };
}, },
/** REMOVE_ANNOTATIONS(
* @param {State} state state: State,
* @param {{ annotationsToRemove: Annotation[] }} action { annotationsToRemove }: { annotationsToRemove: Annotation[] },
*/ ) {
REMOVE_ANNOTATIONS(state, { annotationsToRemove }) {
// Discard any pending updates which conflict with an annotation removed // Discard any pending updates which conflict with an annotation removed
// locally. // locally.
const pendingUpdates = { ...state.pendingUpdates }; const pendingUpdates = { ...state.pendingUpdates };
const pendingDeletions = { ...state.pendingDeletions }; const pendingDeletions = { ...state.pendingDeletions };
for (let ann of annotationsToRemove) { for (const ann of annotationsToRemove) {
if (ann.id) { if (ann.id) {
delete pendingUpdates[ann.id]; delete pendingUpdates[ann.id];
delete pendingDeletions[ann.id]; delete pendingDeletions[ann.id];
...@@ -112,20 +104,23 @@ const reducers = { ...@@ -112,20 +104,23 @@ const reducers = {
/** /**
* Record pending updates representing changes on the server that the client * Record pending updates representing changes on the server that the client
* has been notified about but has not yet applied. * has been notified about but has not yet applied.
*
* @param {object} args
* @param {Annotation[]} [args.updatedAnnotations]
* @param {Annotation[]} [args.deletedAnnotations]
*/ */
function receiveRealTimeUpdates({ function receiveRealTimeUpdates({
updatedAnnotations = [], updatedAnnotations = [],
deletedAnnotations = [], deletedAnnotations = [],
}: {
updatedAnnotations?: Annotation[];
deletedAnnotations?: Annotation[];
}) { }) {
/** return (
* @param {import('redux').Dispatch} dispatch dispatch: Dispatch,
* @param {() => { realTimeUpdates: State, annotations: AnnotationsState, groups: GroupsState, route: RouteState }} getState getState: () => {
*/ realTimeUpdates: State;
return (dispatch, getState) => { annotations: AnnotationsState;
groups: GroupsState;
route: RouteState;
},
) => {
const pendingUpdates = { ...getState().realTimeUpdates.pendingUpdates }; const pendingUpdates = { ...getState().realTimeUpdates.pendingUpdates };
const pendingDeletions = { ...getState().realTimeUpdates.pendingDeletions }; const pendingDeletions = { ...getState().realTimeUpdates.pendingDeletions };
...@@ -141,11 +136,11 @@ function receiveRealTimeUpdates({ ...@@ -141,11 +136,11 @@ function receiveRealTimeUpdates({
ann.group === groupsModule.selectors.focusedGroupId(groupState) || ann.group === groupsModule.selectors.focusedGroupId(groupState) ||
routeModule.selectors.route(routeState) !== 'sidebar' routeModule.selectors.route(routeState) !== 'sidebar'
) { ) {
pendingUpdates[/** @type {string} */ (ann.id)] = ann; pendingUpdates[ann.id!] = ann;
} }
}); });
deletedAnnotations.forEach(ann => { deletedAnnotations.forEach(ann => {
const id = /** @type {string} */ (ann.id); const id = ann.id!;
// Discard any pending but not-yet-applied updates for this annotation // Discard any pending but not-yet-applied updates for this annotation
delete pendingUpdates[id]; delete pendingUpdates[id];
...@@ -179,20 +174,16 @@ function clearPendingUpdates() { ...@@ -179,20 +174,16 @@ function clearPendingUpdates() {
/** /**
* Return added or updated annotations received via the WebSocket * Return added or updated annotations received via the WebSocket
* which have not been applied to the local state. * which have not been applied to the local state.
*
* @param {State} state
*/ */
function pendingUpdates(state) { function pendingUpdates(state: State) {
return state.pendingUpdates; return state.pendingUpdates;
} }
/** /**
* Return IDs of annotations which have been deleted on the server but not * Return IDs of annotations which have been deleted on the server but not
* yet removed from the local state. * yet removed from the local state.
*
* @param {State} state
*/ */
function pendingDeletions(state) { function pendingDeletions(state: State) {
return state.pendingDeletions; return state.pendingDeletions;
} }
...@@ -200,8 +191,7 @@ function pendingDeletions(state) { ...@@ -200,8 +191,7 @@ function pendingDeletions(state) {
* Return a total count of pending updates and deletions. * Return a total count of pending updates and deletions.
*/ */
const pendingUpdateCount = createSelector( const pendingUpdateCount = createSelector(
/** @param {State} state */ (state: State) => [state.pendingUpdates, state.pendingDeletions],
state => [state.pendingUpdates, state.pendingDeletions],
([pendingUpdates, pendingDeletions]) => ([pendingUpdates, pendingDeletions]) =>
Object.keys(pendingUpdates).length + Object.keys(pendingDeletions).length, Object.keys(pendingUpdates).length + Object.keys(pendingDeletions).length,
); );
...@@ -209,22 +199,16 @@ const pendingUpdateCount = createSelector( ...@@ -209,22 +199,16 @@ const pendingUpdateCount = createSelector(
/** /**
* Return true if an annotation has been deleted on the server but the deletion * Return true if an annotation has been deleted on the server but the deletion
* has not yet been applied. * has not yet been applied.
*
* @param {State} state
* @param {string} id
*/ */
function hasPendingDeletion(state, id) { function hasPendingDeletion(state: State, id: string) {
return hasOwn(state.pendingDeletions, id); return hasOwn(state.pendingDeletions, id);
} }
/** /**
* Return true if an annotation has been created on the server, but it has not * Return true if an annotation has been created on the server, but it has not
* yet been applied. * yet been applied.
*
* @param {State} state
* @return {boolean}
*/ */
function hasPendingUpdates(state) { function hasPendingUpdates(state: State): boolean {
return Object.keys(state.pendingUpdates).length > 0; return Object.keys(state.pendingUpdates).length > 0;
} }
......
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
/** export type RouteName =
* @typedef {'annotation'|'notebook'|'profile'|'sidebar'|'stream'} RouteName | 'annotation'
*/ | 'notebook'
| 'profile'
| 'sidebar'
| 'stream';
const initialState = { export type State = {
/** /** The current route. */
* The current route. name: RouteName | null;
*
* @type {RouteName|null}
*/
name: null,
/** /**
* Parameters of the current route. * Parameters of the current route.
...@@ -18,20 +17,20 @@ const initialState = { ...@@ -18,20 +17,20 @@ const initialState = {
* - The "annotation" route has an "id" (annotation ID) parameter. * - The "annotation" route has an "id" (annotation ID) parameter.
* - The "stream" route has a "q" (query) parameter. * - The "stream" route has a "q" (query) parameter.
* - The "sidebar" route has no parameters. * - The "sidebar" route has no parameters.
*
* @type {Record<string, string | undefined>}
*/ */
params: {}, params: Record<string, string | undefined>;
}; };
/** @typedef {typeof initialState} State */ const initialState: State = {
name: null,
params: {},
};
const reducers = { const reducers = {
/** CHANGE_ROUTE(
* @param {State} state state: State,
* @param {{ name: RouteName, params: Record<string, string> }} action { name, params }: { name: RouteName; params: Record<string, string> },
*/ ) {
CHANGE_ROUTE(state, { name, params }) {
return { name, params }; return { name, params };
}, },
}; };
...@@ -39,29 +38,25 @@ const reducers = { ...@@ -39,29 +38,25 @@ const reducers = {
/** /**
* Change the active route. * Change the active route.
* *
* @param {RouteName} name - Name of the route to activate. See `initialState` for possible values * @param name - Name of the route to activate. See `initialState` for possible values
* @param {Record<string,string>} params - Parameters associated with the route * @param params - Parameters associated with the route
*/ */
function changeRoute(name, params = {}) { function changeRoute(name: RouteName, params: Record<string, string> = {}) {
return makeAction(reducers, 'CHANGE_ROUTE', { name, params }); return makeAction(reducers, '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: State) {
return state.name; return state.name;
} }
/** /**
* 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: State) {
return state.params; return state.params;
} }
......
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import type { Annotation } from '../../../types/api';
import type { SidebarSettings } from '../../../types/config';
import type { TabName } from '../../../types/sidebar';
import * as metadata from '../../helpers/annotation-metadata'; import * as metadata from '../../helpers/annotation-metadata';
import { countIf, trueKeys, toTrueMap } from '../../util/collections'; import { countIf, trueKeys, toTrueMap } from '../../util/collections';
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
/** type BooleanMap = Record<string, boolean>;
* @typedef {import('../../../types/api').Annotation} Annotation type SortKey = 'Location' | 'Newest' | 'Oldest';
* @typedef {import('../../../types/config').SidebarSettings} SidebarSettings
* @typedef {import("../../../types/sidebar").TabName} TabName
*/
/**
* @typedef {Record<string, boolean>} BooleanMap
* @typedef {'Location'|'Newest'|'Oldest'} SortKey
*/
/** /**
* Default sort keys for each tab. * Default sort keys for each tab.
*
* @type {Record<TabName, SortKey>}
*/ */
const TAB_SORTKEY_DEFAULT = { const TAB_SORTKEY_DEFAULT: Record<TabName, SortKey> = {
annotation: 'Location', annotation: 'Location',
note: 'Oldest', note: 'Oldest',
orphan: 'Location', orphan: 'Location',
}; };
/** @param {SidebarSettings} settings */ function initialSelection(settings: SidebarSettings): BooleanMap {
function initialSelection(settings) { const selection: BooleanMap = {};
/** @type {BooleanMap} */
const selection = {};
// TODO: Do not take into account existence of `settings.query` here // TODO: Do not take into account existence of `settings.query` here
// once root-thread-building is fully updated: the decision of whether // once root-thread-building is fully updated: the decision of whether
// selection trumps any query is not one for the store to make // selection trumps any query is not one for the store to make
...@@ -39,57 +31,54 @@ function initialSelection(settings) { ...@@ -39,57 +31,54 @@ function initialSelection(settings) {
return selection; return selection;
} }
/** @param {SidebarSettings} settings */ export type State = {
function initialState(settings) { /**
* A set of annotations that are currently "selected" by the user —
* these will supersede other filters/selections.
*/
selected: BooleanMap;
/**
* Explicitly-expanded or -collapsed annotations (threads). A collapsed
* annotation thread will not show its replies; an expanded thread will
* show its replies. Note that there are other factors affecting
* collapsed states, e.g., top-level threads are collapsed by default
* until explicitly expanded.
*/
expanded: BooleanMap;
/**
* Set of threads that have been "forced" visible by the user
* (e.g. by clicking on "Show x more" button) even though they may not
* match the currently-applied filters.
*/
forcedVisible: BooleanMap;
selectedTab: TabName;
/**
* Sort order for annotations.
*/
sortKey: SortKey;
/**
* ID or tag of an annotation that should be given keyboard focus.
*/
focusRequest: string | null;
};
function initialState(settings: SidebarSettings): State {
return { return {
/**
* A set of annotations that are currently "selected" by the user —
* these will supersede other filters/selections.
*/
selected: initialSelection(settings), selected: initialSelection(settings),
// Explicitly-expanded or -collapsed annotations (threads). A collapsed
// annotation thread will not show its replies; an expanded thread will
// show its replies. Note that there are other factors affecting
// collapsed states, e.g., top-level threads are collapsed by default
// until explicitly expanded.
expanded: initialSelection(settings), expanded: initialSelection(settings),
/**
* Set of threads that have been "forced" visible by the user
* (e.g. by clicking on "Show x more" button) even though they may not
* match the currently-applied filters.
*
* @type {BooleanMap}
*/
forcedVisible: {}, forcedVisible: {},
/** @type {TabName} */
selectedTab: 'annotation', selectedTab: 'annotation',
/**
* Sort order for annotations.
*
* @type {SortKey}
*/
sortKey: TAB_SORTKEY_DEFAULT.annotation, sortKey: TAB_SORTKEY_DEFAULT.annotation,
/**
* ID or tag of an annotation that should be given keyboard focus.
*
* @type {string|null}
*/
focusRequest: null, focusRequest: null,
}; };
} }
/** @typedef {ReturnType<initialState>} State */ function setTab(newTab: TabName, oldTab: TabName) {
/**
* @param {TabName} newTab
* @param {TabName} oldTab
*/
const setTab = (newTab, oldTab) => {
// Do nothing if the "new tab" is the same as the tab already selected. // Do nothing if the "new tab" is the same as the tab already selected.
// This will avoid resetting the `sortKey`, too. // This will avoid resetting the `sortKey`, too.
if (oldTab === newTab) { if (oldTab === newTab) {
...@@ -97,9 +86,9 @@ const setTab = (newTab, oldTab) => { ...@@ -97,9 +86,9 @@ const setTab = (newTab, oldTab) => {
} }
return { return {
selectedTab: newTab, selectedTab: newTab,
sortKey: /** @type {SortKey} */ (TAB_SORTKEY_DEFAULT[newTab]), sortKey: TAB_SORTKEY_DEFAULT[newTab],
}; };
}; }
const resetSelection = () => { const resetSelection = () => {
return { return {
...@@ -117,63 +106,35 @@ const reducers = { ...@@ -117,63 +106,35 @@ const reducers = {
return resetSelection(); return resetSelection();
}, },
/** SELECT_ANNOTATIONS(state: State, action: { selection: BooleanMap }) {
* @param {State} state
* @param {{ selection: BooleanMap }} action
*/
SELECT_ANNOTATIONS(state, action) {
return { selected: action.selection }; return { selected: action.selection };
}, },
/** SELECT_TAB(state: State, action: { tab: TabName }) {
* @param {State} state
* @param {{ tab: TabName }} action
*/
SELECT_TAB(state, action) {
return setTab(action.tab, state.selectedTab); return setTab(action.tab, state.selectedTab);
}, },
/** SET_EXPANDED(state: State, action: { id: string; expanded: boolean }) {
* @param {State} state
* @param {{ id: string, expanded: boolean }} action
*/
SET_EXPANDED(state, action) {
const newExpanded = { ...state.expanded }; const newExpanded = { ...state.expanded };
newExpanded[action.id] = action.expanded; newExpanded[action.id] = action.expanded;
return { expanded: newExpanded }; return { expanded: newExpanded };
}, },
/** SET_ANNOTATION_FOCUS_REQUEST(state: State, action: { id: string }) {
* @param {State} state
* @param {{ id: string }} action
*/
SET_ANNOTATION_FOCUS_REQUEST(state, action) {
return { focusRequest: action.id }; return { focusRequest: action.id };
}, },
/** SET_FORCED_VISIBLE(state: State, action: { id: string; visible: boolean }) {
* @param {State} state
* @param {{ id: string, visible: boolean }} action
*/
SET_FORCED_VISIBLE(state, action) {
return { return {
forcedVisible: { ...state.forcedVisible, [action.id]: action.visible }, forcedVisible: { ...state.forcedVisible, [action.id]: action.visible },
}; };
}, },
/** SET_SORT_KEY(state: State, action: { key: SortKey }) {
* @param {State} state
* @param {{ key: SortKey }} action
*/
SET_SORT_KEY(state, action) {
return { sortKey: action.key }; return { sortKey: action.key };
}, },
/** TOGGLE_SELECTED_ANNOTATIONS(state: State, action: { toggleIds: string[] }) {
* @param {State} state
* @param {{ toggleIds: string[] }} action
*/
TOGGLE_SELECTED_ANNOTATIONS(state, action) {
const selection = { ...state.selected }; const selection = { ...state.selected };
action.toggleIds.forEach(id => { action.toggleIds.forEach(id => {
selection[id] = !selection[id]; selection[id] = !selection[id];
...@@ -187,11 +148,11 @@ const reducers = { ...@@ -187,11 +148,11 @@ const reducers = {
* Automatically select the Page Notes tab, for convenience, if all of the * Automatically select the Page Notes tab, for convenience, if all of the
* top-level annotations in `action.annotations` are Page Notes and the * top-level annotations in `action.annotations` are Page Notes and the
* previous annotation count was 0 (i.e. collection empty). * previous annotation count was 0 (i.e. collection empty).
*
* @param {State} state
* @param {{ annotations: Annotation[], currentAnnotationCount: number }} action
*/ */
ADD_ANNOTATIONS(state, action) { ADD_ANNOTATIONS(
state: State,
action: { annotations: Annotation[]; currentAnnotationCount: number },
) {
const topLevelAnnotations = action.annotations.filter( const topLevelAnnotations = action.annotations.filter(
annotation => !metadata.isReply(annotation), annotation => !metadata.isReply(annotation),
); );
...@@ -220,11 +181,13 @@ const reducers = { ...@@ -220,11 +181,13 @@ const reducers = {
return resetSelection(); return resetSelection();
}, },
/** REMOVE_ANNOTATIONS(
* @param {State} state state: State,
* @param {{ annotationsToRemove: Annotation[], remainingAnnotations: Annotation[] }} action action: {
*/ annotationsToRemove: Annotation[];
REMOVE_ANNOTATIONS(state, action) { remainingAnnotations: Annotation[];
},
) {
let newTab = state.selectedTab; let newTab = state.selectedTab;
// If the orphans tab is selected but no remaining annotations are orphans, // If the orphans tab is selected but no remaining annotations are orphans,
// switch back to annotations tab // switch back to annotations tab
...@@ -235,8 +198,7 @@ const reducers = { ...@@ -235,8 +198,7 @@ const reducers = {
newTab = 'annotation'; newTab = 'annotation';
} }
/** @param {BooleanMap} collection */ const removeAnns = (collection: BooleanMap) => {
const removeAnns = collection => {
action.annotationsToRemove.forEach(annotation => { action.annotationsToRemove.forEach(annotation => {
if (annotation.id) { if (annotation.id) {
delete collection[annotation.id]; delete collection[annotation.id];
...@@ -268,11 +230,10 @@ function clearSelection() { ...@@ -268,11 +230,10 @@ function clearSelection() {
* Set the currently selected annotation IDs. This will replace the current * Set the currently selected annotation IDs. This will replace the current
* selection. All provided annotation ids will be set to `true` in the selection. * selection. All provided annotation ids will be set to `true` in the selection.
* *
* @param {string[]} ids - Identifiers of annotations to select * @param ids - Identifiers of annotations to select
*/ */
function selectAnnotations(ids) { function selectAnnotations(ids: string[]) {
/** @param {import('redux').Dispatch} dispatch */ return (dispatch: Dispatch) => {
return dispatch => {
dispatch(clearSelection()); dispatch(clearSelection());
dispatch( dispatch(
makeAction(reducers, 'SELECT_ANNOTATIONS', { selection: toTrueMap(ids) }), makeAction(reducers, 'SELECT_ANNOTATIONS', { selection: toTrueMap(ids) }),
...@@ -285,10 +246,8 @@ function selectAnnotations(ids) { ...@@ -285,10 +246,8 @@ function selectAnnotations(ids) {
* *
* Once the UI has processed this request, it should be cleared with * Once the UI has processed this request, it should be cleared with
* {@link clearAnnotationFocusRequest}. * {@link clearAnnotationFocusRequest}.
*
* @param {string} id
*/ */
function setAnnotationFocusRequest(id) { function setAnnotationFocusRequest(id: string) {
return makeAction(reducers, 'SET_ANNOTATION_FOCUS_REQUEST', { id }); return makeAction(reducers, 'SET_ANNOTATION_FOCUS_REQUEST', { id });
} }
...@@ -301,20 +260,18 @@ function clearAnnotationFocusRequest() { ...@@ -301,20 +260,18 @@ function clearAnnotationFocusRequest() {
/** /**
* Set the currently-selected tab to `tabKey`. * Set the currently-selected tab to `tabKey`.
*
* @param {TabName} tabKey
*/ */
function selectTab(tabKey) { function selectTab(tabKey: TabName) {
return makeAction(reducers, 'SELECT_TAB', { tab: tabKey }); return makeAction(reducers, 'SELECT_TAB', { tab: tabKey });
} }
/** /**
* Set the expanded state for a single annotation/thread. * Set the expanded state for a single annotation/thread.
* *
* @param {string} id - annotation (or thread) id * @param id - annotation (or thread) id
* @param {boolean} expanded - `true` for expanded replies, `false` to collapse * @param expanded - `true` for expanded replies, `false` to collapse
*/ */
function setExpanded(id, expanded) { function setExpanded(id: string, expanded: boolean) {
return makeAction(reducers, 'SET_EXPANDED', { id, expanded }); return makeAction(reducers, 'SET_EXPANDED', { id, expanded });
} }
...@@ -323,20 +280,18 @@ function setExpanded(id, expanded) { ...@@ -323,20 +280,18 @@ function setExpanded(id, expanded) {
* not be visible because of applied filters. Set the force-visibility for a * not be visible because of applied filters. Set the force-visibility for a
* single thread, without affecting other forced-visible threads. * single thread, without affecting other forced-visible threads.
* *
* @param {string} id - Thread id * @param id - Thread id
* @param {boolean} visible - Should this annotation be visible, even if it * @param visible - Should this annotation be visible, even if it conflicts
* conflicts with current filters? * with current filters?
*/ */
function setForcedVisible(id, visible) { function setForcedVisible(id: string, visible: boolean) {
return makeAction(reducers, 'SET_FORCED_VISIBLE', { id, visible }); return makeAction(reducers, 'SET_FORCED_VISIBLE', { id, visible });
} }
/** /**
* Sets the sort key for the annotation list. * Sets the sort key for the annotation list.
*
* @param {SortKey} key
*/ */
function setSortKey(key) { function setSortKey(key: SortKey) {
return makeAction(reducers, 'SET_SORT_KEY', { key }); return makeAction(reducers, 'SET_SORT_KEY', { key });
} }
...@@ -344,29 +299,25 @@ function setSortKey(key) { ...@@ -344,29 +299,25 @@ function setSortKey(key) {
* Toggle the selected state for the annotations in `toggledAnnotations`: * Toggle the selected state for the annotations in `toggledAnnotations`:
* unselect any that are selected; select any that are unselected. * unselect any that are selected; select any that are unselected.
* *
* @param {string[]} toggleIds - identifiers of annotations to toggle * @param toggleIds - identifiers of annotations to toggle
*/ */
function toggleSelectedAnnotations(toggleIds) { function toggleSelectedAnnotations(toggleIds: string[]) {
return makeAction(reducers, 'TOGGLE_SELECTED_ANNOTATIONS', { toggleIds }); return makeAction(reducers, 'TOGGLE_SELECTED_ANNOTATIONS', { toggleIds });
} }
/** /**
* Retrieve map of expanded/collapsed annotations (threads) * Retrieve map of expanded/collapsed annotations (threads)
*
* @param {State} state
*/ */
function expandedMap(state) { function expandedMap(state: State) {
return state.expanded; return state.expanded;
} }
/** @param {State} state */ function annotationFocusRequest(state: State) {
function annotationFocusRequest(state) {
return state.focusRequest; return state.focusRequest;
} }
const forcedVisibleThreads = createSelector( const forcedVisibleThreads = createSelector(
/** @param {State} state */ (state: State) => state.forcedVisible,
state => state.forcedVisible,
forcedVisible => trueKeys(forcedVisible), forcedVisible => trueKeys(forcedVisible),
); );
...@@ -374,29 +325,24 @@ const forcedVisibleThreads = createSelector( ...@@ -374,29 +325,24 @@ const forcedVisibleThreads = createSelector(
* Are any annotations currently selected? * Are any annotations currently selected?
*/ */
const hasSelectedAnnotations = createSelector( const hasSelectedAnnotations = createSelector(
/** @param {State} state */ (state: State) => state.selected,
state => state.selected,
selection => trueKeys(selection).length > 0, selection => trueKeys(selection).length > 0,
); );
const selectedAnnotations = createSelector( const selectedAnnotations = createSelector(
/** @param {State} state */ (state: State) => state.selected,
state => state.selected,
selection => trueKeys(selection), selection => trueKeys(selection),
); );
/** /**
* Return the currently-selected tab * Return the currently-selected tab
*
* @param {State} state
*/ */
function selectedTab(state) { function selectedTab(state: State) {
return state.selectedTab; return state.selectedTab;
} }
const selectionState = createSelector( const selectionState = createSelector(
/** @param {State} state */ (state: State) => state,
state => state,
selection => { selection => {
return { return {
expanded: expandedMap(selection), expanded: expandedMap(selection),
...@@ -410,10 +356,8 @@ const selectionState = createSelector( ...@@ -410,10 +356,8 @@ const selectionState = createSelector(
/** /**
* Retrieve the current sort option key. * Retrieve the current sort option key.
*
* @param {State} state
*/ */
function sortKey(state) { function sortKey(state: State) {
return state.sortKey; return state.sortKey;
} }
...@@ -421,11 +365,9 @@ function sortKey(state) { ...@@ -421,11 +365,9 @@ function sortKey(state) {
* Retrieve applicable sort options for the currently-selected tab. * Retrieve applicable sort options for the currently-selected tab.
*/ */
const sortKeys = createSelector( const sortKeys = createSelector(
/** @param {State} state */ (state: State) => state.selectedTab,
state => state.selectedTab,
selectedTab => { selectedTab => {
/** @type {SortKey[]} */ const sortKeysForTab: SortKey[] = ['Newest', 'Oldest'];
const sortKeysForTab = ['Newest', 'Oldest'];
if (selectedTab !== 'note') { if (selectedTab !== 'note') {
// Location is inapplicable to Notes tab // Location is inapplicable to Notes tab
sortKeysForTab.push('Location'); sortKeysForTab.push('Location');
......
import type { Profile } from '../../../types/api';
import type { SidebarSettings } from '../../../types/config';
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
/** export type State = {
* @typedef {import('../../../types/api').Profile} Profile defaultAuthority: string;
* @typedef {import('../../../types/config').SidebarSettings} SidebarSettings profile: Profile;
*/ };
/** /**
* A dummy profile returned by the `profile` selector before the real profile * A dummy profile returned by the `profile` selector before the real profile
* is fetched. * is fetched.
*
* @type Profile
*/ */
const initialProfile = { const initialProfile: Profile = {
/** A map of features that are enabled for the current user. */ /** A map of features that are enabled for the current user. */
features: {}, features: {},
/** A map of preference names and values. */ /** A map of preference names and values. */
...@@ -22,14 +22,7 @@ const initialProfile = { ...@@ -22,14 +22,7 @@ const initialProfile = {
userid: null, userid: null,
}; };
/** function initialState(settings: SidebarSettings) {
* @typedef State
* @prop {string} defaultAuthority
* @prop {Profile} profile
*/
/** @param {SidebarSettings} settings */
function initialState(settings) {
return { return {
/** /**
* The app's default authority (user identity provider), from settings, * The app's default authority (user identity provider), from settings,
...@@ -48,11 +41,7 @@ function initialState(settings) { ...@@ -48,11 +41,7 @@ function initialState(settings) {
} }
const reducers = { const reducers = {
/** UPDATE_PROFILE(state: State, action: { profile: Profile }) {
* @param {State} state
* @param {{ profile: Profile }} action
*/
UPDATE_PROFILE(state, action) {
return { return {
profile: { ...action.profile }, profile: { ...action.profile },
}; };
...@@ -61,37 +50,29 @@ const reducers = { ...@@ -61,37 +50,29 @@ const reducers = {
/** /**
* Update the profile information for the current user. * Update the profile information for the current user.
*
* @param {Profile} profile
*/ */
function updateProfile(profile) { function updateProfile(profile: Profile) {
return makeAction(reducers, 'UPDATE_PROFILE', { profile }); return makeAction(reducers, 'UPDATE_PROFILE', { profile });
} }
/** function defaultAuthority(state: State) {
* @param {State} state
*/
function defaultAuthority(state) {
return state.defaultAuthority; return state.defaultAuthority;
} }
/** /**
* Return true if a user is logged in and false otherwise. * Return true if a user is logged in and false otherwise.
*
* @param {State} state
*/ */
function isLoggedIn(state) { function isLoggedIn(state: State) {
return state.profile.userid !== null; return state.profile.userid !== null;
} }
/** /**
* Return true if a given feature flag is enabled for the current user. * Return true if a given feature flag is enabled for the current user.
* *
* @param {State} state * @param feature - The name of the feature flag. This matches the name of the
* @param {string} feature - The name of the feature flag. This matches the * feature flag as declared in the Hypothesis service.
* name of the feature flag as declared in the Hypothesis service.
*/ */
function isFeatureEnabled(state, feature) { function isFeatureEnabled(state: State, feature: string) {
return !!state.profile.features[feature]; return !!state.profile.features[feature];
} }
...@@ -99,10 +80,8 @@ function isFeatureEnabled(state, feature) { ...@@ -99,10 +80,8 @@ function isFeatureEnabled(state, feature) {
* Return true if the user's profile has been fetched. This can be used to * Return true if the user's profile has been fetched. This can be used to
* distinguish the dummy profile returned by `profile()` on startup from a * distinguish the dummy profile returned by `profile()` on startup from a
* logged-out user profile returned by the server. * logged-out user profile returned by the server.
*
* @param {State} state
*/ */
function hasFetchedProfile(state) { function hasFetchedProfile(state: State) {
return state.profile !== initialProfile; return state.profile !== initialProfile;
} }
...@@ -113,10 +92,8 @@ function hasFetchedProfile(state) { ...@@ -113,10 +92,8 @@ function hasFetchedProfile(state) {
* *
* If the profile has not yet been fetched yet, a dummy logged-out profile is * If the profile has not yet been fetched yet, a dummy logged-out profile is
* returned. This allows code to skip a null check. * returned. This allows code to skip a null check.
*
* @param {State} state
*/ */
function profile(state) { function profile(state: State) {
return state.profile; return state.profile;
} }
......
...@@ -7,13 +7,10 @@ ...@@ -7,13 +7,10 @@
* extant `SidebarPanel` components. Only one panel (as keyed by `panelName`) * extant `SidebarPanel` components. Only one panel (as keyed by `panelName`)
* may be "active" (open) at one time. * may be "active" (open) at one time.
*/ */
import type { PanelName } from '../../../types/sidebar';
/**
* @typedef {import("../../../types/sidebar").PanelName} PanelName
*/
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
const initialState = { export type State = {
/** /**
* The `panelName` of the currently-active sidebar panel. * The `panelName` of the currently-active sidebar panel.
* Only one `panelName` may be active at a time, but it is valid (though not * Only one `panelName` may be active at a time, but it is valid (though not
...@@ -22,28 +19,20 @@ const initialState = { ...@@ -22,28 +19,20 @@ const initialState = {
* *
* e.g. If `activePanelName` were `foobar`, all `SidebarPanel` components * e.g. If `activePanelName` were `foobar`, all `SidebarPanel` components
* with `panelName` of `foobar` would be active, and thus visible. * with `panelName` of `foobar` would be active, and thus visible.
*
* @type {PanelName|null}
*/ */
activePanelName: null, activePanelName: PanelName | null;
}; };
/** @typedef {typeof initialState} State */ const initialState: State = {
activePanelName: null,
};
const reducers = { const reducers = {
/** OPEN_SIDEBAR_PANEL(state: State, action: { panelName: PanelName }) {
* @param {State} state
* @param {{ panelName: PanelName }} action
*/
OPEN_SIDEBAR_PANEL(state, action) {
return { activePanelName: action.panelName }; return { activePanelName: action.panelName };
}, },
/** CLOSE_SIDEBAR_PANEL(state: State, action: { panelName: PanelName }) {
* @param {State} state
* @param {{ panelName: PanelName }} action
*/
CLOSE_SIDEBAR_PANEL(state, action) {
let activePanelName = state.activePanelName; let activePanelName = state.activePanelName;
if (action.panelName === activePanelName) { if (action.panelName === activePanelName) {
// `action.panelName` is indeed the currently-active panel; deactivate // `action.panelName` is indeed the currently-active panel; deactivate
...@@ -55,11 +44,10 @@ const reducers = { ...@@ -55,11 +44,10 @@ const reducers = {
}; };
}, },
/** TOGGLE_SIDEBAR_PANEL(
* @param {State} state state: State,
* @param {{ panelName: PanelName, panelState?: boolean }} action action: { panelName: PanelName; panelState?: boolean },
*/ ) {
TOGGLE_SIDEBAR_PANEL: function (state, action) {
let activePanelName; let activePanelName;
// Is the panel in question currently the active panel? // Is the panel in question currently the active panel?
const panelIsActive = state.activePanelName === action.panelName; const panelIsActive = state.activePanelName === action.panelName;
...@@ -88,19 +76,15 @@ const reducers = { ...@@ -88,19 +76,15 @@ const reducers = {
/** /**
* Designate `panelName` as the currently-active panel name * Designate `panelName` as the currently-active panel name
*
* @param {PanelName} panelName
*/ */
function openSidebarPanel(panelName) { function openSidebarPanel(panelName: PanelName) {
return makeAction(reducers, 'OPEN_SIDEBAR_PANEL', { panelName }); return makeAction(reducers, 'OPEN_SIDEBAR_PANEL', { panelName });
} }
/** /**
* `panelName` should not be the active panel * `panelName` should not be the active panel
*
* @param {PanelName} panelName
*/ */
function closeSidebarPanel(panelName) { function closeSidebarPanel(panelName: PanelName) {
return makeAction(reducers, 'CLOSE_SIDEBAR_PANEL', { panelName }); return makeAction(reducers, 'CLOSE_SIDEBAR_PANEL', { panelName });
} }
...@@ -108,11 +92,10 @@ function closeSidebarPanel(panelName) { ...@@ -108,11 +92,10 @@ function closeSidebarPanel(panelName) {
* Toggle a sidebar panel from its current state, or set it to the * Toggle a sidebar panel from its current state, or set it to the
* designated `panelState`. * designated `panelState`.
* *
* @param {PanelName} panelName * @param panelState -
* @param {boolean} [panelState] -
* Should the panel be active? Omit this prop to simply toggle the value. * Should the panel be active? Omit this prop to simply toggle the value.
*/ */
function toggleSidebarPanel(panelName, panelState) { function toggleSidebarPanel(panelName: PanelName, panelState?: boolean) {
return makeAction(reducers, 'TOGGLE_SIDEBAR_PANEL', { return makeAction(reducers, 'TOGGLE_SIDEBAR_PANEL', {
panelName, panelName,
panelState, panelState,
...@@ -121,11 +104,8 @@ function toggleSidebarPanel(panelName, panelState) { ...@@ -121,11 +104,8 @@ function toggleSidebarPanel(panelName, panelState) {
/** /**
* Is the panel indicated by `panelName` currently active (open)? * Is the panel indicated by `panelName` currently active (open)?
*
* @param {State} state
* @param {PanelName} panelName
*/ */
function isSidebarPanelOpen(state, panelName) { function isSidebarPanelOpen(state: State, panelName: PanelName) {
return state.activePanelName === panelName; return state.activePanelName === panelName;
} }
......
import type { ToastMessage } from '../../../shared/components/BaseToastMessages';
import { createStoreModule, makeAction } from '../create-store'; import { createStoreModule, makeAction } from '../create-store';
/**
* @typedef {import('../../../shared/components/BaseToastMessages').ToastMessage} ToastMessage
*/
/** /**
* A store module for managing a collection of toast messages. This module * A store module for managing a collection of toast messages. This module
* maintains state only; it's up to other layers to handle the management * maintains state only; it's up to other layers to handle the management
* and interactions with these messages. * and interactions with these messages.
*/ */
const initialState = { export type State = {
/** @type {ToastMessage[]} */ messages: ToastMessage[];
messages: [],
}; };
/** @typedef {typeof initialState} State */ const initialState: State = {
messages: [],
};
const reducers = { const reducers = {
/** ADD_MESSAGE(state: State, action: { message: ToastMessage }) {
* @param {State} state
* @param {{ message: ToastMessage }} action
*/
ADD_MESSAGE(state, action) {
return { return {
messages: state.messages.concat({ ...action.message }), messages: state.messages.concat({ ...action.message }),
}; };
}, },
/** REMOVE_MESSAGE(state: State, action: { id: string }) {
* @param {State} state
* @param {{ id: string }} action
*/
REMOVE_MESSAGE(state, action) {
const updatedMessages = state.messages.filter( const updatedMessages = state.messages.filter(
message => message.id !== action.id, message => message.id !== action.id,
); );
return { messages: updatedMessages }; return { messages: updatedMessages };
}, },
/** UPDATE_MESSAGE(state: State, action: { message: ToastMessage }) {
* @param {State} state
* @param {{ message: ToastMessage }} action
*/
UPDATE_MESSAGE(state, action) {
const updatedMessages = state.messages.map(message => { const updatedMessages = state.messages.map(message => {
if (message.id && message.id === action.message.id) { if (message.id && message.id === action.message.id) {
return { ...action.message }; return { ...action.message };
...@@ -56,28 +42,21 @@ const reducers = { ...@@ -56,28 +42,21 @@ const reducers = {
/** Actions */ /** Actions */
/** function addMessage(message: ToastMessage) {
* @param {ToastMessage} message
*/
function addMessage(message) {
return makeAction(reducers, 'ADD_MESSAGE', { message }); return makeAction(reducers, 'ADD_MESSAGE', { message });
} }
/** /**
* Remove the `message` with the corresponding `id` property value. * Remove the `message` with the corresponding `id` property value.
*
* @param {string} id
*/ */
function removeMessage(id) { function removeMessage(id: string) {
return makeAction(reducers, 'REMOVE_MESSAGE', { id }); return makeAction(reducers, 'REMOVE_MESSAGE', { id });
} }
/** /**
* Update the `message` object (lookup is by `id`). * Update the `message` object (lookup is by `id`).
*
* @param {ToastMessage} message
*/ */
function updateMessage(message) { function updateMessage(message: ToastMessage) {
return makeAction(reducers, 'UPDATE_MESSAGE', { message }); return makeAction(reducers, 'UPDATE_MESSAGE', { message });
} }
...@@ -85,20 +64,15 @@ function updateMessage(message) { ...@@ -85,20 +64,15 @@ function updateMessage(message) {
/** /**
* Retrieve a message by `id` * Retrieve a message by `id`
*
* @param {State} state
* @param {string} id
*/ */
function getMessage(state, id) { function getMessage(state: State, id: string) {
return state.messages.find(message => message.id === id); return state.messages.find(message => message.id === id);
} }
/** /**
* Retrieve all current messages * Retrieve all current messages
*
* @param {State} state
*/ */
function getMessages(state) { function getMessages(state: State) {
return state.messages; return state.messages;
} }
...@@ -106,12 +80,8 @@ function getMessages(state) { ...@@ -106,12 +80,8 @@ function getMessages(state) {
* Return boolean indicating whether a message with the same type and message * Return boolean indicating whether a message with the same type and message
* text exists in the state's collection of messages. This matches messages * text exists in the state's collection of messages. This matches messages
* by content, not by ID (true uniqueness). * by content, not by ID (true uniqueness).
*
* @param {State} state
* @param {string} type
* @param {string} text
*/ */
function hasMessage(state, type, text) { function hasMessage(state: State, type: ToastMessage['type'], text: string) {
return state.messages.some(message => { return state.messages.some(message => {
return message.type === type && message.message === text; return message.type === type && message.message === text;
}); });
......
...@@ -5,22 +5,20 @@ import { createStoreModule, makeAction } from '../create-store'; ...@@ -5,22 +5,20 @@ import { createStoreModule, makeAction } from '../create-store';
* sidebar. * sidebar.
*/ */
const initialState = { export type State = {
/** /**
* Has the sidebar ever been opened? NB: This is not necessarily the * Has the sidebar ever been opened? NB: This is not necessarily the
* current state of the sidebar, but tracks whether it has ever been open * current state of the sidebar, but tracks whether it has ever been open
*/ */
sidebarHasOpened: false, sidebarHasOpened: boolean;
}; };
/** @typedef {typeof initialState} State */ const initialState: State = {
sidebarHasOpened: false,
};
const reducers = { const reducers = {
/** SET_SIDEBAR_OPENED(state: State, action: { opened: boolean }) {
* @param {State} state
* @param {{ opened: boolean }} action
*/
SET_SIDEBAR_OPENED(state, action) {
if (action.opened === true) { if (action.opened === true) {
// If the sidebar is open, track that it has ever been opened // If the sidebar is open, track that it has ever been opened
return { sidebarHasOpened: true }; return { sidebarHasOpened: true };
...@@ -30,15 +28,11 @@ const reducers = { ...@@ -30,15 +28,11 @@ const reducers = {
}, },
}; };
/** function setSidebarOpened(opened: boolean) {
* @param {boolean} opened - If the sidebar is open
*/
function setSidebarOpened(opened) {
return makeAction(reducers, 'SET_SIDEBAR_OPENED', { opened }); return makeAction(reducers, 'SET_SIDEBAR_OPENED', { opened });
} }
/** @param {State} state */ function hasSidebarOpened(state: State) {
function hasSidebarOpened(state) {
return state.sidebarHasOpened; return state.sidebarHasOpened;
} }
......
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