Commit 7bc88d9c authored by Robert Knight's avatar Robert Knight

Specify type of `state` arguments to `createSelector` calls

The newest version of the `createSelector` typings require the type of
the input argument to be determined in order for the whole call to
typecheck. This commit implements a pattern where:

- A `State` type is added in various store modules, typically defined as
  `typeof initialState`, that defines the state of that store module's
  type.
- The type of selector function params in `createSelector` calls is
  specified with `@param {State} state` and the return type is inferred

The upside of these changes is that types of selector functions are
determined much more robustly - previously it was possible to have
incorrect types specified via JSDoc.

In the process an existing minor bug was determined where an
`isHighlight` call in `ThreadList` was passed an unexpected type of
object (the `annotation` property of a `Draft`) for which `isHighlight`
did not crash but would always return false. I have simply suppressed
this existing issue for the moment, but it will need to be fixed
separately.
parent c3436b27
......@@ -96,6 +96,8 @@ function ThreadList({ threads }) {
// list is the most recently created one.
const newAnnotations = store
.unsavedAnnotations()
// @ts-expect-error - FIXME: The `isHighlight` check here will always fail
// because `unsavedAnnotations` returns objects that do not have a `$highlight` property.
.filter(ann => !ann.id && !isHighlight(ann));
if (!newAnnotations.length) {
return null;
......
......@@ -114,6 +114,8 @@ const initialState = {
nextTag: 1,
};
/** @typedef {typeof initialState} State */
const reducers = {
ADD_ANNOTATIONS: function (state, action) {
const updatedIDs = {};
......@@ -415,10 +417,9 @@ function updateFlagStatus(id, isFlagged) {
/**
* Count the number of annotations (as opposed to notes or orphans)
*
* @type {(state: any) => number}
*/
const annotationCount = createSelector(
/** @param {State} state */
state => state.annotations,
annotations => countIf(annotations, metadata.isAnnotation)
);
......@@ -470,20 +471,18 @@ function findIDsForTags(state, tags) {
/**
* Retrieve currently-focused annotation identifiers
*
* @type {(state: any) => string[]}
*/
const focusedAnnotations = createSelector(
/** @param {State} state */
state => state.focused,
focused => trueKeys(focused)
);
/**
* Retrieve currently-highlighted annotation identifiers
*
* @type {(state: any) => string[]}
*/
const highlightedAnnotations = createSelector(
/** @param {State} state */
state => state.highlighted,
highlighted => trueKeys(highlighted)
);
......@@ -504,6 +503,7 @@ function isAnnotationFocused(state, $tag) {
* @type {(state: any) => boolean}
*/
const isWaitingToAnchorAnnotations = createSelector(
/** @param {State} state */
state => state.annotations,
annotations => annotations.some(metadata.isWaitingToAnchor)
);
......@@ -511,10 +511,9 @@ const isWaitingToAnchorAnnotations = createSelector(
/**
* Return all loaded annotations that are not highlights and have not been saved
* to the server
*
* @type {(state: any) => Annotation[]}
*/
const newAnnotations = createSelector(
/** @param {State} state */
state => state.annotations,
annotations =>
annotations.filter(ann => metadata.isNew(ann) && !metadata.isHighlight(ann))
......@@ -523,10 +522,9 @@ const newAnnotations = createSelector(
/**
* Return all loaded annotations that are highlights and have not been saved
* to the server
*
* @type {(state: any) => Annotation[]}
*/
const newHighlights = createSelector(
/** @param {State} state */
state => state.annotations,
annotations =>
annotations.filter(ann => metadata.isNew(ann) && metadata.isHighlight(ann))
......@@ -534,20 +532,18 @@ const newHighlights = createSelector(
/**
* Count the number of page notes currently in the collection
*
@type {(state: any) => number}
*/
const noteCount = createSelector(
/** @param {State} state */
state => state.annotations,
annotations => countIf(annotations, metadata.isPageNote)
);
/**
* Count the number of orphans currently in the collection
*
* @type {(state: any) => number}
*/
const orphanCount = createSelector(
/** @param {State} state */
state => state.annotations,
annotations => countIf(annotations, metadata.isOrphan)
);
......
......@@ -15,6 +15,8 @@ import { removeAnnotations } from './annotations';
/** @type {Draft[]} */
const initialState = [];
/** @typedef {typeof initialState} State */
/**
* Helper class to encapsulate the draft properties and a few simple methods.
*
......@@ -177,10 +179,9 @@ function getDraftIfNotEmpty(state, annotation) {
/**
* Returns a list of draft annotations which have no id.
*
* @type {(state: any) => Annotation[]}
*/
const unsavedAnnotations = createSelector(
/** @param {State} state */
state => state,
drafts => drafts.filter(d => !d.annotation.id).map(d => d.annotation)
);
......
......@@ -46,7 +46,7 @@ import { createStoreModule } from '../create-store';
*/
/**
* @typedef {Record<FilterKey, FilterOption>|{}} Filters
* @typedef {Record<FilterKey, FilterOption>} Filters
*/
/**
......@@ -59,10 +59,7 @@ import { createStoreModule } from '../create-store';
function initialState(settings) {
const focusConfig = settings.focus || {};
return {
/**
* @type {Filters}
*/
filters: {},
filters: /** @type {Filters} */ ({}),
// immediately activate focus mode if there is a valid config
focusActive: isValidFocusConfig(focusConfig),
......@@ -73,6 +70,8 @@ function initialState(settings) {
};
}
/** @typedef {ReturnType<typeof initialState>} State */
/**
* Given the provided focusConfig: is it a valid configuration for focus?
* At this time, a `user` filter is required.
......@@ -93,7 +92,7 @@ function isValidFocusConfig(focusConfig) {
*/
function focusFiltersFromConfig(focusConfig) {
if (!isValidFocusConfig(focusConfig)) {
return {};
return /** @type {Filters} */ ({});
}
const userFilterValue =
focusConfig.user.username || focusConfig.user.userid || '';
......@@ -145,7 +144,7 @@ const reducers = {
CLEAR_SELECTION: function () {
return {
filters: {},
filters: /** @type {Filters} */ ({}),
focusActive: false,
query: null,
};
......@@ -221,7 +220,9 @@ function filterQuery(state) {
* @type {(state: any) => FocusState}
*/
const focusState = createSelector(
/** @param {State} state */
state => state.focusActive,
/** @param {State} state */
state => state.focusFilters,
(focusActive, focusFilters) => {
return {
......
......@@ -23,6 +23,8 @@ import { createStoreModule } from '../create-store';
/** @type {Frame[]} */
const initialState = [];
/** @typedef {typeof initialState} State */
const reducers = {
CONNECT_FRAME: function (state, action) {
return [...state, action.frame];
......@@ -99,10 +101,9 @@ function frames(state) {
* for that purpose.
*
* This may be `null` during startup.
*
* @type {(state: any) => Frame|null}
*/
const mainFrame = createSelector(
/** @param {State} state */
state => state,
// Sub-frames will all have a "frame identifier" set. The main frame is the
......@@ -142,16 +143,14 @@ const createShallowEqualSelector = createSelectorCreator(
/**
* Memoized selector will return the same array (of URIs) reference unless the
* values of the array change (are not shallow-equal).
*
* @type {(state: any) => string[]}
*/
const searchUris = createShallowEqualSelector(
frames => {
return frames.reduce(
/** @param {State} frames */
frames =>
frames.reduce(
(uris, frame) => uris.concat(searchUrisForFrame(frame)),
[]
);
},
/** @type {string[]} */ ([])
),
uris => uris
);
......
......@@ -7,6 +7,7 @@ import session from './session';
/**
* @typedef {import('../../../types/api').Group} Group
* @typedef {import('./session').State} SessionState
*/
const initialState = {
......@@ -23,6 +24,8 @@ const initialState = {
focusedGroupId: null,
};
/** @typedef {typeof initialState} State */
const reducers = {
FOCUS_GROUP(state, action) {
const group = state.groups.find(g => g.id === action.id);
......@@ -139,10 +142,9 @@ function getGroup(state, id) {
/**
* Return groups the user isn't a member of that are scoped to the URI.
*
* @type {(state: any) => Group[]}
*/
const getFeaturedGroups = createSelector(
/** @param {State} state */
state => state.groups,
groups => groups.filter(group => !group.isMember && group.isScopedToUri)
);
......@@ -151,10 +153,9 @@ const getFeaturedGroups = createSelector(
* Return groups that are scoped to the uri. This is used to return the groups
* that show up in the old groups menu. This should be removed once the new groups
* menu is permanent.
*
* @type {(state: any) => Group[]}
*/
const getInScopeGroups = createSelector(
/** @param {State} state */
state => state.groups,
groups => groups.filter(g => g.isScopedToUri)
);
......@@ -163,10 +164,9 @@ const getInScopeGroups = createSelector(
/**
* Return groups the logged in user is a member of.
*
* @type {(state: any) => Group[]}
*/
const getMyGroups = createSelector(
/** @param {{ groups: State, session: SessionState }} rootState */
rootState => rootState.groups.groups,
rootState => session.selectors.isLoggedIn(rootState.session),
(groups, loggedIn) => {
......@@ -181,10 +181,9 @@ const getMyGroups = createSelector(
/**
* Return groups that don't show up in Featured and My Groups.
*
* @type {(state: any) => Group[]}
*/
const getCurrentlyViewingGroups = createSelector(
/** @param {{ groups: State, session: SessionState }} rootState */
rootState => allGroups(rootState.groups),
rootState => getMyGroups(rootState),
rootState => getFeaturedGroups(rootState.groups),
......
......@@ -35,6 +35,8 @@ const initialState = {
pendingDeletions: {},
};
/** @typedef {typeof initialState} State */
const reducers = {
RECEIVE_REAL_TIME_UPDATES(state, action) {
return {
......@@ -175,6 +177,7 @@ function pendingDeletions(state) {
* @type {(state: any) => number}
*/
const pendingUpdateCount = createSelector(
/** @param {State} state */
state => [state.pendingUpdates, state.pendingDeletions],
([pendingUpdates, pendingDeletions]) =>
Object.keys(pendingUpdates).length + Object.keys(pendingDeletions).length
......
......@@ -9,12 +9,12 @@
*/
/**
* @typedef SelectionState
* @prop {Record<string,boolean>} expanded
* @prop {string[]} forcedVisible
* @prop {string[]} selected
* @prop {string} sortKey
* @prop {'annotation'|'note'|'orphan'} selectedTab
* @typedef State
* @prop {Record<string, boolean>} expanded
* @prop {Record<string, boolean>} forcedVisible
* @prop {Record<string, boolean>} selected
* @prop {string} sortKey
* @prop {'annotation'|'note'|'orphan'} selectedTab
*/
import { createSelector } from 'reselect';
......@@ -34,6 +34,7 @@ const TAB_SORTKEY_DEFAULT = {
};
function initialSelection(settings) {
/** @type {Record<string, boolean>} */
const selection = {};
// TODO: Do not take into account existence of `settings.query` here
// once root-thread-building is fully updated: the decision of whether
......@@ -44,6 +45,7 @@ function initialSelection(settings) {
return selection;
}
/** @return {State} */
function initialState(settings) {
return {
/**
......@@ -309,28 +311,23 @@ function expandedMap(state) {
return state.expanded;
}
/**
* @type {(state: any) => string[]}
*/
const forcedVisibleThreads = createSelector(
/** @param {State} state */
state => state.forcedVisible,
forcedVisible => trueKeys(forcedVisible)
);
/**
* Are any annotations currently selected?
*
* @type {(state: any) => boolean}
*/
const hasSelectedAnnotations = createSelector(
/** @param {State} state */
state => state.selected,
selection => trueKeys(selection).length > 0
);
/**
* @type {(state: any) => string[]}
*/
const selectedAnnotations = createSelector(
/** @param {State} state */
state => state.selected,
selection => trueKeys(selection)
);
......@@ -344,10 +341,8 @@ function selectedTab(state) {
return state.selectedTab;
}
/**
* @return {SelectionState}
*/
const selectionState = createSelector(
/** @param {State} state */
state => state,
selection => {
return {
......@@ -372,10 +367,9 @@ function sortKey(state) {
/**
* Retrieve applicable sort options for the currently-selected tab.
*
* @type {(state: any) => string[]}
*/
const sortKeys = createSelector(
/** @param {State} state */
state => state.selectedTab,
selectedTab => {
const sortKeysForTab = ['Newest', 'Oldest'];
......
......@@ -23,6 +23,13 @@ const initialProfile = {
userid: null,
};
/**
* @typedef State
* @prop {string} defaultAuthority
* @prop {Profile} profile
*/
/** @return {State} */
function initialState(settings) {
return {
/**
......
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