Commit e9b81457 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Split `selection` store into `selection`, `filters`

The `selection` store module has become overlong and
its responsibilities aren't clear. We know we'll be
adding some more filtering capabilities to the app
in the next short while, and that would make
`selection` even more complex and heavy.

Split into two store modules: `selection` and
`filters`.

Temporarily re-implement `rootSelector`s that
are needed for generating thread and filter
state for components.
parent 899a2831
......@@ -9,7 +9,7 @@ import useRootThread from './hooks/use-root-thread';
import useStore from '../store/use-store';
/**
* @typedef {import('../store/modules/selection').FilterState} FilterState
* @typedef {import('../store/modules/filters').FilterState} FilterState
* @typedef {import('../util/build-thread').Thread} Thread
*/
......
......@@ -36,6 +36,7 @@ import annotations from './modules/annotations';
import defaults from './modules/defaults';
import directLinked from './modules/direct-linked';
import drafts from './modules/drafts';
import filters from './modules/filters';
import frames from './modules/frames';
import groups from './modules/groups';
import links from './modules/links';
......@@ -57,6 +58,7 @@ import viewer from './modules/viewer';
* @typedef {import("./modules/defaults").DefaultsStore} DefaultsStore
* @typedef {import("./modules/direct-linked").DirectLinkedStore} DirectLinkedStore
* @typedef {import("./modules/drafts").DraftsStore} DraftsStore
* @typedef {import("./modules/filters").FiltersStore} FiltersStore
* @typedef {import("./modules/frames").FramesStore} FramesStore
* @typedef {import("./modules/groups").GroupsStore} GroupsStore
* @typedef {import("./modules/links").LinksStore} LinksStore
......@@ -76,6 +78,7 @@ import viewer from './modules/viewer';
* DefaultsStore &
* DirectLinkedStore &
* DraftsStore &
* FiltersStore &
* FramesStore &
* GroupsStore &
* LinksStore &
......@@ -109,6 +112,7 @@ export default function store(settings) {
defaults,
directLinked,
drafts,
filters,
frames,
links,
groups,
......
import { createSelector } from 'reselect';
import { actionTypes } from '../util';
import { trueKeys } from '../../util/collections';
/**
* @typedef FocusConfig
* @prop {User} user
*/
/**
* @typedef FocusedUser
* @prop {string} filter - The identifier to use for filtering annotations
* derived from either a userId or a username. This may take the
* form of a username, e.g. 'oakbucket', or a userid
* @prop {string} displayName
*/
/**
* @typedef User
* @prop {string} [userid]
* @prop {string} [username]
* @prop {string} displayName - User's display name
*/
/**
* @typedef FocusState
* @prop {boolean} configured - Focus config contains valid `user` and
* is good to go
* @prop {boolean} active - Focus mode is currently applied
* @prop {FocusedUser} [user] - User to focus on (filter annotations for)
*/
/**
* TODO potentially split into `FilterState` and `FocusState`?
* @typedef FilterState
* @prop {string|null} filterQuery
* @prop {boolean} focusActive
* @prop {boolean} focusConfigured
* @prop {string|null} focusDisplayName
* @prop {number} forcedVisibleCount
* @prop {number} selectedCount
*/
/**
* Configure (user-)focused mode. User-focus mode may be set in one of two
* ways:
* - A `focus` object containing a valid `user` object is present in the
* application's `settings` object during initialization time
* - A `user` object is given to the `changeFocusedUser` action (this
* is implemented via an RPC method call)
* For focus mode to be considered configured, it must have a valid `user`.
* A successfully-configured focus mode will be set to `active` immediately
* and may be toggled via `toggleFocusMode`.
*
* @param {FocusConfig} focusConfig
* @return {FocusState}
*/
function setFocus(focusConfig) {
const focusDefaultState = {
configured: false,
active: false,
};
// To be able to apply a focused mode, a `user` object must be present,
// and that user object must have either a `username` or a `userid`
const focusUser = focusConfig.user || {};
const userFilter = focusUser.username || focusUser.userid;
// If that requirement is not met, we can't configure/activate focus mode
if (!userFilter) {
return focusDefaultState;
}
return {
configured: true,
active: true, // Activate valid focus mode immediately
user: {
filter: userFilter,
displayName: focusUser.displayName || userFilter || '',
},
};
}
function init(settings) {
return {
filters: {},
focus: setFocus(settings.focus || /** @type FocusConfig */ ({})),
query: settings.query || null,
};
}
const update = {
CHANGE_FOCUS_MODE_USER: function (state, action) {
return {
focus: setFocus({ user: action.user }),
};
},
SET_FILTER_QUERY: function (state, action) {
return { query: action.query };
},
SET_FOCUS_MODE: function (state, action) {
const active =
typeof action.active !== 'undefined'
? action.active
: !state.focus.active;
return {
focus: {
...state.focus,
active,
},
};
},
// Actions defined in other modules
CLEAR_SELECTION: function (state) {
return {
filters: {},
focus: {
...state.focus,
active: false,
},
query: null,
};
},
};
const actions = actionTypes(update);
// Action creators
/** Set the query used to filter displayed annotations. */
function setFilterQuery(query) {
return dispatch => {
dispatch({
type: actions.SET_FILTER_QUERY,
query: query,
});
};
}
/**
* Clears any applied filters, changes the focused user and sets
* focused enabled to `true`.
*
* @param {User} user - The user to focus on
*/
function changeFocusModeUser(user) {
return { type: actions.CHANGE_FOCUS_MODE_USER, user };
}
/**
* Toggle whether or not a (user-)focus mode is applied, either inverting the
* current active state or setting it to a target `active` state, if provided.
*
* @param {boolean} [active] - Optional `active` state for focus mode
*/
function toggleFocusMode(active) {
return {
type: actions.SET_FOCUS_MODE,
active,
};
}
// Selectors
function filterQuery(state) {
return state.query;
}
/**
* Returns the display name for a user or the userid
* if display name is not present. If both are missing
* then this returns an empty string.
*
* @return {string}
*/
function focusModeUserPrettyName(state) {
if (!state.focus.configured) {
return '';
}
return state.focus.user.displayName;
}
/**
* Summary of applied filters
*
* @type {(state: any) => FilterState}
*/
const filterState = createSelector(
rootState => rootState.selection,
rootState => rootState.filters,
(selection, filters) => {
// TODO FIXME
const forcedVisibleCount = trueKeys(selection.forcedVisible).length;
// TODO FIXME
const selectedCount = trueKeys(selection.selected).length;
return {
filterQuery: filters.query,
focusActive: filters.focus.active,
focusConfigured: filters.focus.configured,
focusDisplayName: focusModeUserPrettyName(filters),
forcedVisibleCount,
selectedCount,
};
}
);
/**
* @typedef FiltersStore
*
* // Actions
* @prop {typeof changeFocusModeUser} changeFocusModeUser
* @prop {typeof setFilterQuery} setFilterQuery
* @prop {typeof toggleFocusMode} toggleFocusMode
*
* // Selectors
* @prop {() => string|null} filterQuery
*
* // Root Selectors
* @prop {() => FilterState} filterState
*
*/
export default {
init,
namespace: 'filters',
update,
actions: {
changeFocusModeUser,
setFilterQuery,
toggleFocusMode,
},
selectors: {
filterQuery,
},
rootSelectors: {
filterState,
},
};
......@@ -7,34 +7,6 @@
* @typedef {import('../../../types/api').Annotation} Annotation
*/
/**
* @typedef User
* @prop {string} [userid]
* @prop {string} [username]
* @prop {string} displayName - User's display name
*/
/**
* @typedef FocusedUser
* @prop {string} filter - The identifier to use for filtering annotations
* derived from either a userId or a username. This may take the
* form of a username, e.g. 'oakbucket', or a userid
* @prop {string} displayName
*/
/**
* @typedef FocusState
* @prop {boolean} configured - Focus config contains valid `user` and
* is good to go
* @prop {boolean} active - Focus mode is currently applied
* @prop {FocusedUser} [user] - User to focus on (filter annotations for)
*/
/**
* @typedef FocusConfig
* @prop {User} user
*/
/**
* @typedef ThreadState
* @prop {Annotation[]} annotations
......@@ -49,16 +21,6 @@
* @prop {string} route
*/
/**
* @typedef FilterState
* @prop {string|null} filterQuery
* @prop {boolean} focusActive
* @prop {boolean} focusConfigured
* @prop {string|null} focusDisplayName
* @prop {number} forcedVisibleCount
* @prop {number} selectedCount
*/
import { createSelector } from 'reselect';
import uiConstants from '../../ui-constants';
......@@ -86,46 +48,6 @@ function initialSelection(settings) {
return selection;
}
/**
* Configure (user-)focused mode. User-focus mode may be set in one of two
* ways:
* - A `focus` object containing a valid `user` object is present in the
* application's `settings` object during initialization time
* - A `user` object is given to the `changeFocusedUser` action (this
* is implemented via an RPC method call)
* For focus mode to be considered configured, it must have a valid `user`.
* A successfully-configured focus mode will be set to `active` immediately
* and may be toggled via `toggleFocusMode`.
*
* @param {FocusConfig} focusConfig
* @return {FocusState}
*/
function setFocus(focusConfig) {
const focusDefaultState = {
configured: false,
active: false,
};
// To be able to apply a focused mode, a `user` object must be present,
// and that user object must have either a `username` or a `userid`
const focusUser = focusConfig.user || {};
const userFilter = focusUser.username || focusUser.userid;
// If that requirement is not met, we can't configure/activate focus mode
if (!userFilter) {
return focusDefaultState;
}
return {
configured: true,
active: true, // Activate valid focus mode immediately
user: {
filter: userFilter,
displayName: focusUser.displayName || userFilter || '',
},
};
}
function init(settings) {
return {
/**
......@@ -152,9 +74,6 @@ function init(settings) {
// match the currently-applied filters
forcedVisible: {},
filterQuery: settings.query || null,
focusMode: setFocus(settings.focus || /** @type FocusConfig */ ({})),
selectedTab: uiConstants.TAB_ANNOTATIONS,
// Key by which annotations are currently sorted.
sortKey: TAB_SORTKEY_DEFAULT[uiConstants.TAB_ANNOTATIONS],
......@@ -179,26 +98,18 @@ const setTab = (newTab, oldTab) => {
*/
const resetSelection = () => {
return {
filterQuery: null,
forcedVisible: {},
selected: {},
};
};
const update = {
CHANGE_FOCUS_MODE_USER: function (state, action) {
return {
...resetSelection(),
focusMode: setFocus({ user: action.user }),
};
},
CLEAR_SELECTION: function () {
return resetSelection();
},
SELECT_ANNOTATIONS: function (state, action) {
return { ...resetSelection(), selected: action.selection };
return { selected: action.selection };
},
SELECT_TAB: function (state, action) {
......@@ -211,24 +122,6 @@ const update = {
return { expanded: newExpanded };
},
SET_FILTER_QUERY: function (state, action) {
return { ...resetSelection(), expanded: {}, filterQuery: action.query };
},
SET_FOCUS_MODE: function (state, action) {
const active =
typeof action.active !== 'undefined'
? action.active
: !state.focusMode.active;
return {
...resetSelection(),
focusMode: {
...state.focusMode,
active,
},
};
},
SET_FORCED_VISIBLE: function (state, action) {
return {
forcedVisible: { ...state.forcedVisible, [action.id]: action.visible },
......@@ -247,6 +140,8 @@ const update = {
return { selected: selection };
},
/** Actions defined in other modules */
/**
* Automatically select the Page Notes tab, for convenience, if all of the
* top-level annotations in `action.annotations` are Page Notes and the
......@@ -265,6 +160,18 @@ const update = {
return {};
},
CHANGE_FOCUS_MODE_USER: function () {
return resetSelection();
},
SET_FILTER_QUERY: function () {
return { ...resetSelection(), expanded: {} };
},
SET_FOCUS_MODE: function () {
return resetSelection();
},
REMOVE_ANNOTATIONS: function (state, action) {
let newTab = state.selectedTab;
// If the orphans tab is selected but no remaining annotations are orphans,
......@@ -300,16 +207,6 @@ const actions = util.actionTypes(update);
/* Action Creators */
/**
* Clears any applied filters, changes the focused user and sets
* focused enabled to `true`.
*
* @param {User} user - The user to focus on
*/
function changeFocusModeUser(user) {
return { type: actions.CHANGE_FOCUS_MODE_USER, user };
}
/**
* Clear all selected annotations and filters. This will leave user-focus
* alone, however.
......@@ -328,7 +225,7 @@ function clearSelection() {
*/
function selectAnnotations(ids) {
return dispatch => {
dispatch({ type: actions.SET_FOCUS_MODE, active: false });
dispatch({ type: actions.CLEAR_SELECTION });
dispatch({
type: actions.SELECT_ANNOTATIONS,
selection: toTrueMap(ids),
......@@ -362,14 +259,6 @@ function setExpanded(id, expanded) {
};
}
/** Set the query used to filter displayed annotations. */
function setFilterQuery(query) {
return {
type: actions.SET_FILTER_QUERY,
query: query,
};
}
/**
* A user may "force" an annotation to be visible, even if it would be otherwise
* not be visible because of applied filters. Set the force-visibility for a
......@@ -395,19 +284,6 @@ function setSortKey(key) {
};
}
/**
* Toggle whether or not a (user-)focus mode is applied, either inverting the
* current active state or setting it to a target `active` state, if provided.
*
* @param {boolean} [active] - Optional `active` state for focus mode
*/
function toggleFocusMode(active) {
return {
type: actions.SET_FOCUS_MODE,
active,
};
}
/**
* Toggle the selected state for the annotations in `toggledAnnotations`:
* unselect any that are selected; select any that are unselected.
......@@ -432,56 +308,6 @@ function expandedMap(state) {
return state.expanded;
}
function filterQuery(state) {
return state.filterQuery;
}
/**
* Is a focus mode currently applied?
*
* @return {boolean}
*/
function focusModeActive(state) {
return state.focusMode.active;
}
/**
* Does the state have a configured focus mode? That is, does it have a valid
* focus mode filter that could be applied (regardless of whether it is currently
* active)?
*
* @return {boolean}
*/
function focusModeConfigured(state) {
return state.focusMode.configured;
}
/**
* Returns the user identifier for a focused user or `null` if no focused user.
*
* @return {string|null}
*/
function focusModeUserFilter(state) {
if (!focusModeActive(state)) {
return null;
}
return state.focusMode.user.filter;
}
/**
* Returns the display name for a user or the userid
* if display name is not present. If both are missing
* then this returns an empty string.
*
* @return {string}
*/
function focusModeUserPrettyName(state) {
if (!focusModeConfigured(state)) {
return '';
}
return state.focusMode.user.displayName;
}
/**
* @type {(state: any) => string[]}
*/
......@@ -522,21 +348,6 @@ const selectedAnnotations = createSelector(
selection => trueKeys(selection)
);
/**
* Is any sort of filtering currently applied to the list of annotations? This
* includes a search query, but also if annotations are selected or a user
* is focused.
*
* @type {(state: any) => boolean}
*/
const hasAppliedFilter = createSelector(
filterQuery,
focusModeActive,
hasSelectedAnnotations,
(filterQuery, focusModeActive, hasSelectedAnnotations) =>
!!filterQuery || focusModeActive || hasSelectedAnnotations
);
/**
* Return the currently-selected tab
*
......@@ -569,46 +380,37 @@ function sortKeys(state) {
return sortKeysForTab;
}
/**
* Summary of applied filters
*
* @type {(state: any) => FilterState}
*/
const filterState = createSelector(
state => state,
selection => {
return {
filterQuery: filterQuery(selection),
focusActive: focusModeActive(selection),
focusConfigured: focusModeConfigured(selection),
focusDisplayName: focusModeUserPrettyName(selection),
forcedVisibleCount: forcedVisibleAnnotations(selection).length,
selectedCount: selectedAnnotations(selection).length,
};
}
);
/* Selectors that take root state */
/**
* Retrieve state needed to calculate the root thread
*
* TODO: Refactor
* - Remove route entirely and make that logic the responsibility of a caller
* - Remove all filter-related properties. Those should come from `filterState`
* - Likely remove `annotations` as well
* - Likely rename this to `selectionState`, and it may not need to be a
* `rootSelector`
*
* @type {(rootState: any) => ThreadState}
*/
const threadState = createSelector(
rootState => rootState.annotations.annotations,
rootState => rootState.route.name,
rootState => rootState.selection,
(annotations, routeName, selection) => {
const filters = /** @type {Object.<string,string>} */ ({});
const userFilter = focusModeUserFilter(selection);
rootState => rootState.filters,
(annotations, routeName, selection, filters) => {
const setFilters = /** @type {Object.<string,string>} */ ({});
// TODO FIXME
const userFilter = filters.focus.active && filters.focus.user.filter;
if (userFilter) {
filters.user = userFilter;
setFilters.user = userFilter;
}
// TODO remove filterQuery, filters from this returned object
const selectionState = {
expanded: expandedMap(selection),
filterQuery: filterQuery(selection),
filters,
filterQuery: filters.query,
filters: setFilters,
forcedVisible: forcedVisibleAnnotations(selection),
selected: selectedAnnotations(selection),
sortKey: sortKey(selection),
......@@ -618,32 +420,45 @@ const threadState = createSelector(
}
);
/**
* Is any sort of filtering currently applied to the list of annotations? This
* includes a search query, but also if annotations are selected or a user
* is focused.
*
* TODO: FIXME/refactor — this may need to be split into two selectors across
* two store modules that calling code needs to combine. It also may be
* logic that doesn't belong at all at the store level
*
* @type {(state: any) => boolean}
*/
const hasAppliedFilter = createSelector(
rootState => rootState.selection,
rootState => rootState.filters,
(selection, filters) => {
return (
!!filters.query ||
filters.focus.active ||
hasSelectedAnnotations(selection)
);
}
);
/**
* @typedef SelectionStore
*
* // Actions
* @prop {typeof changeFocusModeUser} changeFocusModeUser
* @prop {typeof clearSelection} clearSelection
* @prop {typeof selectAnnotations} selectAnnotations
* @prop {typeof selectTab} selectTab
* @prop {typeof setExpanded} setExpanded
* @prop {typeof setFilterQuery} setFilterQuery
* @prop {typeof setForcedVisible} setForcedVisible
* @prop {typeof setSortKey} setSortKey
* @prop {typeof toggleFocusMode} toggleFocusMode
* @prop {typeof toggleSelectedAnnotations} toggleSelectedAnnotations
*
* // Selectors
* @prop {() => Object<string,boolean>} expandedMap
* @prop {() => string|null} filterQuery
* @prop {() => FilterState} filterState
* @prop {() => boolean} focusModeActive
* @prop {() => boolean} focusModeConfigured
* @prop {() => string|null} focusModeUserFilter
* @prop {() => string} focusModeUserPrettyName
* @prop {() => string[]} forcedVisibleAnnotations
* @prop {() => string|null} getFirstSelectedAnnotationId
* @prop {() => boolean} hasAppliedFilter
* @prop {() => boolean} hasSelectedAnnotations
* @prop {() => string[]} selectedAnnotations
* @prop {() => string} selectedTab
......@@ -651,6 +466,7 @@ const threadState = createSelector(
* @prop {() => string[]} sortKeys
*
* // Root Selectors
* @prop {() => boolean} hasAppliedFilter
* @prop {() => ThreadState} threadState
*
*/
......@@ -661,29 +477,19 @@ export default {
update: update,
actions: {
changeFocusModeUser,
clearSelection,
selectAnnotations,
selectTab,
setExpanded,
setFilterQuery,
setForcedVisible,
setSortKey,
toggleFocusMode,
toggleSelectedAnnotations,
},
selectors: {
expandedMap,
filterQuery,
filterState,
focusModeActive,
focusModeConfigured,
focusModeUserFilter,
focusModeUserPrettyName,
forcedVisibleAnnotations,
getFirstSelectedAnnotationId,
hasAppliedFilter,
hasSelectedAnnotations,
selectedAnnotations,
selectedTab,
......@@ -692,6 +498,7 @@ export default {
},
rootSelectors: {
hasAppliedFilter,
threadState,
},
};
// import uiConstants from '../../../ui-constants';
import createStore from '../../create-store';
import filters from '../filters';
import selection from '../selection';
// import * as fixtures from '../../../test/annotation-fixtures';
describe('sidebar/store/modules/filters', () => {
let store;
let fakeSettings = [{}, {}];
const getFiltersState = () => {
return store.getState().filters;
};
beforeEach(() => {
store = createStore([filters, selection], fakeSettings);
});
describe('actions', () => {
describe('changeFocusModeUser', () => {
it('sets the focused user and activates focus', () => {
store.toggleFocusMode(false);
store.changeFocusModeUser({
username: 'testuser',
displayName: 'Test User',
});
const focusState = getFiltersState().focus;
assert.isTrue(focusState.active);
assert.isTrue(focusState.configured);
assert.equal(focusState.user.filter, 'testuser');
assert.equal(focusState.user.displayName, 'Test User');
});
// When the LMS app wants the client to disable focus mode it sends a
// changeFocusModeUser() RPC call with {username: undefined, displayName:
// undefined}:
//
// https://github.com/hypothesis/lms/blob/d6b88fd7e375a4b23899117556b3e39cfe18986b/lms/static/scripts/frontend_apps/components/LMSGrader.js#L46
//
// This is the LMS app's way of asking the client to disable focus mode.
it('deactivates and disables focus if username is undefined', () => {
store.toggleFocusMode(true);
store.changeFocusModeUser({
username: undefined,
displayName: undefined,
});
const focusState = getFiltersState().focus;
assert.isFalse(focusState.active);
assert.isFalse(focusState.configured);
});
});
describe('setFilterQuery', () => {
it('sets the filter query', () => {
store.setFilterQuery('a-query');
assert.equal(getFiltersState().query, 'a-query');
assert.equal(store.filterQuery(), 'a-query');
});
});
describe('toggleFocusMode', () => {
it('toggles the current active state if called without arguments', () => {
store.toggleFocusMode(false);
store.toggleFocusMode();
const focusState = getFiltersState().focus;
assert.isTrue(focusState.active);
});
it('toggles the current active state to designated state', () => {
store.toggleFocusMode(true);
store.toggleFocusMode(false);
const focusState = getFiltersState().focus;
assert.isFalse(focusState.active);
});
});
describe('CLEAR_SELECTION', () => {
it('responds to CLEAR_SELECTION by clearing filters and focus', () => {
store.changeFocusModeUser({
username: 'testuser',
displayName: 'Test User',
});
store.toggleFocusMode(true);
let focusState = getFiltersState().focus;
assert.isTrue(focusState.active);
store.clearSelection();
focusState = getFiltersState().focus;
assert.isFalse(focusState.active);
});
});
});
describe('selectors', () => {
describe('filterState', () => {
it('returns the current filter query', () => {
store.setFilterQuery('doodah, doodah');
assert.equal(store.filterState().filterQuery, 'doodah, doodah');
});
it('returns user focus information', () => {
store.changeFocusModeUser({
username: 'filbert',
displayName: 'Pantomime Nutball',
});
const filterState = store.filterState();
assert.isTrue(filterState.focusActive);
assert.isTrue(filterState.focusConfigured);
assert.equal(filterState.focusDisplayName, 'Pantomime Nutball');
});
it('returns a count of forced-visible annotations', () => {
store.setForcedVisible('kaboodle', true);
store.setForcedVisible('stampy', false);
assert.equal(store.filterState().forcedVisibleCount, 1);
});
it('returns a count of selected annotations', () => {
store.selectAnnotations(['tabulature', 'felonious']);
assert.equal(store.filterState().selectedCount, 2);
});
it('returns empty filter states when no filters active', () => {
const filterState = store.filterState();
assert.isFalse(filterState.focusActive);
assert.isFalse(filterState.focusConfigured);
assert.isEmpty(filterState.focusDisplayName);
assert.isNull(filterState.filterQuery);
assert.equal(filterState.forcedVisibleCount, 0);
assert.equal(filterState.selectedCount, 0);
});
});
});
});
import uiConstants from '../../../ui-constants';
import createStore from '../../create-store';
import annotations from '../annotations';
import filters from '../filters';
import selection from '../selection';
import route from '../route';
import * as fixtures from '../../../test/annotation-fixtures';
......@@ -14,7 +15,7 @@ describe('sidebar/store/modules/selection', () => {
};
beforeEach(() => {
store = createStore([annotations, selection, route], fakeSettings);
store = createStore([annotations, filters, selection, route], fakeSettings);
});
describe('getFirstSelectedAnnotationId', function () {
......@@ -66,7 +67,7 @@ describe('sidebar/store/modules/selection', () => {
it('returns true if user-focused mode is active', () => {
store = createStore(
[selection],
[filters, selection],
[{ focus: { user: { username: 'somebody' } } }]
);
......@@ -75,7 +76,7 @@ describe('sidebar/store/modules/selection', () => {
it('returns false if user-focused mode is configured but inactive', () => {
store = createStore(
[selection],
[filters, selection],
[{ focus: { user: { username: 'somebody' } } }]
);
store.toggleFocusMode(false);
......@@ -119,47 +120,6 @@ describe('sidebar/store/modules/selection', () => {
});
});
describe('filterState', () => {
it('returns the current filter query', () => {
store.setFilterQuery('doodah, doodah');
assert.equal(store.filterState().filterQuery, 'doodah, doodah');
});
it('returns user focus information', () => {
store.changeFocusModeUser({
username: 'filbert',
displayName: 'Pantomime Nutball',
});
const filterState = store.filterState();
assert.isTrue(filterState.focusActive);
assert.isTrue(filterState.focusConfigured);
assert.equal(filterState.focusDisplayName, 'Pantomime Nutball');
});
it('returns a count of forced-visible annotations', () => {
store.setForcedVisible('kaboodle', true);
store.setForcedVisible('stampy', false);
assert.equal(store.filterState().forcedVisibleCount, 1);
});
it('returns a count of selected annotations', () => {
store.selectAnnotations(['tabulature', 'felonious']);
assert.equal(store.filterState().selectedCount, 2);
});
it('returns empty filter states when no filters active', () => {
const filterState = store.filterState();
assert.isFalse(filterState.focusActive);
assert.isFalse(filterState.focusConfigured);
assert.isEmpty(filterState.focusDisplayName);
assert.isNull(filterState.filterQuery);
assert.equal(filterState.forcedVisibleCount, 0);
assert.equal(filterState.selectedCount, 0);
});
});
describe('threadState', () => {
it('returns the current annotations in rootState', () => {
const myAnnotation = fixtures.defaultAnnotation();
......@@ -268,199 +228,59 @@ describe('sidebar/store/modules/selection', () => {
});
});
describe('#REMOVE_ANNOTATIONS', function () {
it('removing an annotation should also remove it from the selection', function () {
describe('CHANGE_FOCUS_MODE_USER', () => {
it('clears selection', () => {
store.selectAnnotations([1, 2, 3]);
store.setForcedVisible(2, true);
store.setForcedVisible(1, true);
store.setExpanded(1, true);
store.setExpanded(2, true);
store.removeAnnotations([{ id: 2 }]);
assert.deepEqual(getSelectionState().selected, {
1: true,
3: true,
});
assert.deepEqual(store.forcedVisibleAnnotations(), ['1']);
assert.deepEqual(store.expandedMap(), { 1: true });
});
});
describe('setFilterQuery()', function () {
it('sets the filter query', function () {
store.setFilterQuery('a-query');
assert.equal(getSelectionState().filterQuery, 'a-query');
});
it('resets the force-visible and expanded sets', function () {
store.setForcedVisible('123', true);
store.setExpanded('456', true);
store.setFilterQuery('some-query');
assert.deepEqual(getSelectionState().forcedVisible, {});
assert.deepEqual(getSelectionState().expanded, {});
});
});
describe('changeFocusModeUser', function () {
it('sets the focused user and enables focus mode', function () {
store.toggleFocusMode(false);
store.changeFocusModeUser({
username: 'testuser',
displayName: 'Test User',
});
assert.equal(store.focusModeUserFilter(), 'testuser');
assert.equal(store.focusModeUserPrettyName(), 'Test User');
assert.equal(store.focusModeActive(), true);
assert.equal(store.focusModeConfigured(), true);
});
// When the LMS app wants the client to disable focus mode it sends a
// changeFocusModeUser() RPC call with {username: undefined, displayName:
// undefined}:
//
// https://github.com/hypothesis/lms/blob/d6b88fd7e375a4b23899117556b3e39cfe18986b/lms/static/scripts/frontend_apps/components/LMSGrader.js#L46
//
// This is the LMS app's way of asking the client to disable focus mode.
it('disables focus mode if username is undefined', function () {
store.toggleFocusMode(true);
store.changeFocusModeUser({
username: undefined,
displayName: undefined,
});
assert.equal(store.focusModeActive(), false);
assert.equal(store.focusModeConfigured(), false);
});
it('clears other applied selections', () => {
store.toggleFocusMode(true);
store.setForcedVisible('someAnnotationId');
store.setFilterQuery('somequery');
store.changeFocusModeUser({
username: 'testuser',
displayName: 'Test User',
});
assert.isEmpty(getSelectionState().forcedVisible);
assert.isNull(store.filterQuery());
});
});
describe('toggleFocusMode', function () {
it('toggles the current active state if called without arguments', function () {
store.toggleFocusMode(false);
store.toggleFocusMode();
assert.isTrue(store.focusModeActive());
});
it('toggles the current active state to designated state', function () {
store.toggleFocusMode(true);
store.toggleFocusMode(false);
assert.isFalse(store.focusModeActive());
});
});
describe('focusModeConfigured', function () {
it('should be true when the focus setting is present and user valid', function () {
store = createStore(
[selection],
[{ focus: { user: { username: 'anybody' } } }]
);
assert.isTrue(store.focusModeConfigured());
});
it('should be false when the focus setting is present but user object invalid', function () {
store = createStore([selection], [{ focus: { user: {} } }]);
assert.isFalse(store.focusModeConfigured());
});
it('should be false when the focus setting is not present', function () {
assert.equal(store.focusModeConfigured(), false);
assert.isEmpty(store.selectedAnnotations());
assert.isEmpty(store.forcedVisibleAnnotations());
});
});
describe('focusModeActive', function () {
it('should return true by default when focus mode is active', function () {
store = createStore(
[selection],
[{ focus: { user: { username: 'anybody' } } }]
);
assert.equal(getSelectionState().focusMode.configured, true);
assert.equal(getSelectionState().focusMode.active, true);
assert.equal(store.focusModeActive(), true);
});
describe('SET_FILTER_QUERY', () => {
it('clears selection', () => {
store.selectAnnotations([1, 2, 3]);
store.setForcedVisible(2, true);
it('should return false when focus config is not valid', () => {
store = createStore(
[selection],
[{ focus: { user: { blerp: 'anybody' } } }]
);
assert.isFalse(store.focusModeActive());
});
store.setFilterQuery('foobar');
it('should return false by default when focus mode is not active', function () {
assert.equal(store.focusModeActive(), false);
assert.isEmpty(store.selectedAnnotations());
assert.isEmpty(store.forcedVisibleAnnotations());
});
});
describe('focusModeUserPrettyName', function () {
it('returns `displayName` when available', function () {
store = createStore(
[selection],
[
{
focus: {
user: { username: 'anybody', displayName: 'FakeDisplayName' },
},
},
]
);
assert.equal(store.focusModeUserPrettyName(), 'FakeDisplayName');
});
it('returns the `username` when `displayName` is missing', function () {
store = createStore(
[selection],
[{ focus: { user: { username: 'anybody', userid: 'nobody' } } }]
);
assert.equal(store.focusModeUserPrettyName(), 'anybody');
});
describe('SET_FOCUS_MODE', () => {
it('clears selection', () => {
store.selectAnnotations([1, 2, 3]);
store.setForcedVisible(2, true);
it('returns the `userid` if `displayName` and `username` are missing', () => {
store = createStore(
[selection],
[{ focus: { user: { userid: 'nobody' } } }]
);
assert.equal(store.focusModeUserPrettyName(), 'nobody');
});
store.toggleFocusMode(true);
it('returns empty string if focus mode is not configured', () => {
store = createStore([selection], [{ focus: {} }]);
assert.equal(store.focusModeUserPrettyName(), '');
assert.isEmpty(store.selectedAnnotations());
assert.isEmpty(store.forcedVisibleAnnotations());
});
});
describe('focusModeUserFilter', function () {
it('should return the user identifier when present', function () {
store = createStore(
[selection],
[{ focus: { user: { userid: 'acct:userid@authority' } } }]
);
assert.equal(store.focusModeUserFilter(), 'acct:userid@authority');
});
it('should return null when no filter available', function () {
store = createStore([selection], [{ focus: { user: {} } }]);
assert.isNull(store.focusModeUserFilter());
});
it('should return null when the user object is not present', function () {
assert.isNull(store.focusModeUserFilter());
describe('#REMOVE_ANNOTATIONS', function () {
it('removing an annotation should also remove it from the selection', function () {
store.selectAnnotations([1, 2, 3]);
store.setForcedVisible(2, true);
store.setForcedVisible(1, true);
store.setExpanded(1, true);
store.setExpanded(2, true);
store.removeAnnotations([{ id: 2 }]);
assert.deepEqual(getSelectionState().selected, {
1: true,
3: true,
});
it('should return the username when present but no userid', function () {
// remove once LMS no longer sends username in RPC or config
// https://github.com/hypothesis/client/issues/1516
store = createStore(
[selection],
[{ focus: { user: { username: 'fake_user_name' } } }]
);
assert.equal(store.focusModeUserFilter(), 'fake_user_name');
assert.deepEqual(store.forcedVisibleAnnotations(), ['1']);
assert.deepEqual(store.expandedMap(), { 1: true });
});
});
......
......@@ -61,7 +61,7 @@ describe('store', function () {
it('sets `filterQuery` to null', () => {
store.clearSelection();
assert.isNull(store.getState().selection.filterQuery);
assert.isNull(store.getState().filters.query);
});
it('sets `directLinkedGroupFetchFailed` to false', () => {
......
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