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 { 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