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