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