Commit 56bfa23b authored by Kyle Keating's avatar Kyle Keating

Add toggle button to focus-mode-header

Minor change to the component to add a button that enables or disables the focused mode for the focusedUser passed into the settings object.
parent 8e1700fa
...@@ -97,6 +97,16 @@ function LiveReloadServer(port, config) { ...@@ -97,6 +97,16 @@ function LiveReloadServer(port, config) {
window.hypothesisConfig = function () { window.hypothesisConfig = function () {
return { return {
liveReloadServer: 'ws://' + appHost + ':${port}', liveReloadServer: 'ws://' + appHost + ':${port}',
// Force into focused user mode
// Example focused user mode
// focus: {
// user: {
// username: 'foo',
// authority: 'lms',
// displayName: 'Foo Bar',
// }
// },
// Open the sidebar when the page loads // Open the sidebar when the page loads
openSidebar: true, openSidebar: true,
......
...@@ -24,6 +24,7 @@ function configFrom(window_) { ...@@ -24,6 +24,7 @@ function configFrom(window_) {
'enableExperimentalNewNoteButton' 'enableExperimentalNewNoteButton'
), ),
group: settings.group, group: settings.group,
focus: settings.hostPageSetting('focus'),
theme: settings.hostPageSetting('theme'), theme: settings.hostPageSetting('theme'),
usernameUrl: settings.hostPageSetting('usernameUrl'), usernameUrl: settings.hostPageSetting('usernameUrl'),
onLayoutChange: settings.hostPageSetting('onLayoutChange'), onLayoutChange: settings.hostPageSetting('onLayoutChange'),
......
'use strict'; 'use strict';
const { createElement } = require('preact'); const { createElement } = require('preact');
const propTypes = require('prop-types');
const { withServices } = require('../util/service-context'); const useStore = require('../store/use-store');
function FocusedModeHeader({ settings }) { function FocusedModeHeader() {
const store = useStore(store => ({
actions: {
setFocusModeFocused: store.setFocusModeFocused,
},
selectors: {
focusModeFocused: store.focusModeFocused,
focusModeUserPrettyName: store.focusModeUserPrettyName,
},
}));
const toggleFocusedMode = () => {
store.actions.setFocusModeFocused(!store.selectors.focusModeFocused());
};
const buttonText = () => {
if (store.selectors.focusModeFocused()) {
return `Annotations by ${store.selectors.focusModeUserPrettyName()}`;
} else {
return 'All annotations';
}
};
return ( return (
<div className="focused-mode-header"> <div className="focused-mode-header">
Showing annotations by{' '} <button
<span className="focused-mode-header__user">{settings.focusedUser}</span> onClick={toggleFocusedMode}
className="primary-action-btn primary-action-btn--short"
title={`Toggle to show annotations only by ${store.selectors.focusModeUserPrettyName()}`}
>
{buttonText()}
</button>
</div> </div>
); );
} }
FocusedModeHeader.propTypes = {
// Injected services.
settings: propTypes.object.isRequired,
};
FocusedModeHeader.injectedProps = ['settings']; FocusedModeHeader.propTypes = {};
module.exports = withServices(FocusedModeHeader); module.exports = FocusedModeHeader;
...@@ -126,7 +126,7 @@ function SidebarContentController( ...@@ -126,7 +126,7 @@ function SidebarContentController(
); );
this.showFocusedHeader = () => { this.showFocusedHeader = () => {
return store.getState().focusedMode; return store.focusModeEnabled();
}; };
this.showSelectedTabs = function() { this.showSelectedTabs = function() {
...@@ -136,7 +136,7 @@ function SidebarContentController( ...@@ -136,7 +136,7 @@ function SidebarContentController(
store.getState().filterQuery store.getState().filterQuery
) { ) {
return false; return false;
} else if (store.getState().focusedMode) { } else if (store.focusModeFocused()) {
return false; return false;
} else { } else {
return true; return true;
......
'use strict';
const { shallow } = require('enzyme');
const { createElement } = require('preact');
const FocusedModeHeader = require('../focused-mode-header');
describe('FocusedModeHeader', function() {
let fakeStore;
function createComponent() {
return shallow(<FocusedModeHeader />);
}
beforeEach(function() {
fakeStore = {
selection: {
focusMode: {
enabled: true,
focused: true,
},
},
focusModeFocused: sinon.stub().returns(false),
focusModeUserPrettyName: sinon.stub().returns('Fake User'),
setFocusModeFocused: sinon.stub(),
};
FocusedModeHeader.$imports.$mock({
'../store/use-store': callback => callback(fakeStore),
});
});
afterEach(() => {
FocusedModeHeader.$imports.$restore();
});
it('creates the component', () => {
const wrapper = createComponent();
assert.include(wrapper.text(), 'All annotations');
});
it("sets the button's text to the user's name when focused", () => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
const wrapper = createComponent();
assert.include(wrapper.text(), 'Annotations by Fake User');
});
describe('clicking the button shall toggle the focused mode', function() {
it('when focused is false, toggle to true', () => {
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledWith(fakeStore.setFocusModeFocused, true);
});
it('when focused is true, toggle to false', () => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledWith(fakeStore.setFocusModeFocused, false);
});
});
});
...@@ -158,6 +158,17 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -158,6 +158,17 @@ describe('sidebar.components.sidebar-content', function() {
}); });
}); });
describe('showFocusedHeader', () => {
it('returns true if focus mode is enabled', () => {
store.focusModeEnabled = sinon.stub().returns(true);
assert.isTrue(ctrl.showFocusedHeader());
});
it('returns false if focus mode is not enabled', () => {
store.focusModeEnabled = sinon.stub().returns(false);
assert.isFalse(ctrl.showFocusedHeader());
});
});
function connectFrameAndPerformInitialFetch() { function connectFrameAndPerformInitialFetch() {
setFrames([{ uri: 'https://a-page.com' }]); setFrames([{ uri: 'https://a-page.com' }]);
$scope.$digest(); $scope.$digest();
......
...@@ -10,9 +10,6 @@ function hostPageConfig(window) { ...@@ -10,9 +10,6 @@ function hostPageConfig(window) {
const configJSON = queryString.parse(window.location.search).config; const configJSON = queryString.parse(window.location.search).config;
const config = JSON.parse(configJSON || '{}'); const config = JSON.parse(configJSON || '{}');
// temp: hack to force user
config.focusedUser = 'kyle';
// Known configuration parameters which we will import from the host page. // Known configuration parameters which we will import from the host page.
// Note that since the host page is untrusted code, the filtering needs to // Note that since the host page is untrusted code, the filtering needs to
// be done here. // be done here.
...@@ -42,7 +39,7 @@ function hostPageConfig(window) { ...@@ -42,7 +39,7 @@ function hostPageConfig(window) {
'enableExperimentalNewNoteButton', 'enableExperimentalNewNoteButton',
// Forces the sidebar to filter annotations to a single user. // Forces the sidebar to filter annotations to a single user.
'focusedUser', 'focus',
// Fetch config from a parent frame. // Fetch config from a parent frame.
'requestConfigFromFrame', 'requestConfigFromFrame',
......
...@@ -39,7 +39,7 @@ const sortFns = { ...@@ -39,7 +39,7 @@ const sortFns = {
* The root thread is then displayed by viewer.html * The root thread is then displayed by viewer.html
*/ */
// @ngInject // @ngInject
function RootThread($rootScope, settings, store, searchFilter, viewFilter) { function RootThread($rootScope, store, searchFilter, viewFilter) {
/** /**
* Build the root conversation thread from the given UI state. * Build the root conversation thread from the given UI state.
* *
...@@ -48,21 +48,31 @@ function RootThread($rootScope, settings, store, searchFilter, viewFilter) { ...@@ -48,21 +48,31 @@ function RootThread($rootScope, settings, store, searchFilter, viewFilter) {
*/ */
function buildRootThread(state) { function buildRootThread(state) {
const sortFn = sortFns[state.sortKey]; const sortFn = sortFns[state.sortKey];
const shouldFilterThread = () => {
// is there a query or focused truthy value from the config?
return state.filterQuery || store.focusModeFocused();
};
let filterFn; let filterFn;
if (state.filterQuery || state.focusedMode) { if (shouldFilterThread()) {
const userFilter = {}; // optional user filter object for focused mode
// look for a unique username, if present, add it to the user filter
const focusedUsername = store.focusModeUsername(); // may be null if no focused user
if (focusedUsername) {
// focused user found, add it to the filter object
userFilter.user = focusedUsername;
}
const filters = searchFilter.generateFacetedFilter( const filters = searchFilter.generateFacetedFilter(
state.filterQuery, state.filterQuery,
settings.focusedUser userFilter
); );
filterFn = function(annot) { filterFn = function(annot) {
return viewFilter.filter([annot], filters).length > 0; return viewFilter.filter([annot], filters).length > 0;
}; };
} }
let threadFilterFn; let threadFilterFn;
const hasFilters = state.filterQuery || state.focusedMode; if (state.isSidebar && !shouldFilterThread()) {
if (state.isSidebar && !hasFilters) {
threadFilterFn = function(thread) { threadFilterFn = function(thread) {
if (!thread.annotation) { if (!thread.annotation) {
return false; return false;
......
...@@ -127,9 +127,12 @@ function toObject(searchtext) { ...@@ -127,9 +127,12 @@ function toObject(searchtext) {
* facet. * facet.
* *
* @param {string} searchtext * @param {string} searchtext
* @param {object} focusFilters - Map of the filter objects keyed to array values.
* Currently, only the `user` filter key is supported.
*
* @return {Object} * @return {Object}
*/ */
function generateFacetedFilter(searchtext, focusedUser) { function generateFacetedFilter(searchtext, focusFilters = {}) {
let terms; let terms;
const any = []; const any = [];
const quote = []; const quote = [];
...@@ -138,8 +141,7 @@ function generateFacetedFilter(searchtext, focusedUser) { ...@@ -138,8 +141,7 @@ function generateFacetedFilter(searchtext, focusedUser) {
const tag = []; const tag = [];
const text = []; const text = [];
const uri = []; const uri = [];
const user = focusedUser ? [focusedUser] : []; const user = focusFilters.user ? [focusFilters.user] : [];
if (searchtext) { if (searchtext) {
terms = tokenize(searchtext); terms = tokenize(searchtext);
for (const term of terms) { for (const term of terms) {
......
...@@ -28,6 +28,7 @@ describe('rootThread', function() { ...@@ -28,6 +28,7 @@ describe('rootThread', function() {
let fakeStore; let fakeStore;
let fakeBuildThread; let fakeBuildThread;
let fakeSearchFilter; let fakeSearchFilter;
let fakeSettings;
let fakeViewFilter; let fakeViewFilter;
let $rootScope; let $rootScope;
...@@ -65,6 +66,8 @@ describe('rootThread', function() { ...@@ -65,6 +66,8 @@ describe('rootThread', function() {
getDraftIfNotEmpty: sinon.stub().returns(null), getDraftIfNotEmpty: sinon.stub().returns(null),
removeDraft: sinon.stub(), removeDraft: sinon.stub(),
createAnnotation: sinon.stub(), createAnnotation: sinon.stub(),
focusModeFocused: sinon.stub().returns(false),
focusModeUsername: sinon.stub().returns({}),
}; };
fakeBuildThread = sinon.stub().returns(fixtures.emptyThread); fakeBuildThread = sinon.stub().returns(fixtures.emptyThread);
...@@ -73,6 +76,8 @@ describe('rootThread', function() { ...@@ -73,6 +76,8 @@ describe('rootThread', function() {
generateFacetedFilter: sinon.stub(), generateFacetedFilter: sinon.stub(),
}; };
fakeSettings = {};
fakeViewFilter = { fakeViewFilter = {
filter: sinon.stub(), filter: sinon.stub(),
}; };
...@@ -81,6 +86,7 @@ describe('rootThread', function() { ...@@ -81,6 +86,7 @@ describe('rootThread', function() {
.module('app', []) .module('app', [])
.value('store', fakeStore) .value('store', fakeStore)
.value('searchFilter', fakeSearchFilter) .value('searchFilter', fakeSearchFilter)
.value('settings', fakeSettings)
.value('viewFilter', fakeViewFilter) .value('viewFilter', fakeViewFilter)
.service('rootThread', rootThreadFactory); .service('rootThread', rootThreadFactory);
...@@ -340,6 +346,26 @@ describe('rootThread', function() { ...@@ -340,6 +346,26 @@ describe('rootThread', function() {
}); });
}); });
describe('when the focus user is present', () => {
it("generates a thread filter focused on the user's annotations", () => {
fakeBuildThread.reset();
const filters = [{ user: { terms: ['acct:bill@localhost'] } }];
const annotation = annotationFixtures.defaultAnnotation();
fakeSearchFilter.generateFacetedFilter.returns(filters);
fakeStore.focusModeFocused = sinon.stub().returns(true);
rootThread.thread(fakeStore.state);
const filterFn = fakeBuildThread.args[0][1].filterFn;
fakeViewFilter.filter.returns([annotation]);
assert.isTrue(filterFn(annotation));
assert.calledWith(
fakeViewFilter.filter,
sinon.match([annotation]),
filters
);
});
});
context('when annotation events occur', function() { context('when annotation events occur', function() {
const annot = annotationFixtures.defaultAnnotation(); const annot = annotationFixtures.defaultAnnotation();
......
...@@ -185,5 +185,23 @@ describe('sidebar.search-filter', () => { ...@@ -185,5 +185,23 @@ describe('sidebar.search-filter', () => {
} }
}); });
}); });
it('filters to a focused user', () => {
const filter = searchFilter.generateFacetedFilter(null, {
user: 'fakeusername',
});
// Remove empty facets.
Object.keys(filter).forEach(k => {
if (filter[k].terms.length === 0) {
delete filter[k];
}
});
assert.deepEqual(filter, {
user: {
operator: 'or',
terms: ['fakeusername'],
},
});
});
}); });
}); });
...@@ -113,7 +113,12 @@ function init(settings) { ...@@ -113,7 +113,12 @@ function init(settings) {
selectedTab: TAB_DEFAULT, selectedTab: TAB_DEFAULT,
focusedMode: !!settings.focusedUser, focusMode: {
enabled: settings.hasOwnProperty('focus'), // readonly
focused: true,
// Copy over the focus confg from settings object
config: { ...(settings.focus ? settings.focus : {}) },
},
// Key by which annotations are currently sorted. // Key by which annotations are currently sorted.
sortKey: TAB_SORTKEY_DEFAULT[TAB_DEFAULT], sortKey: TAB_SORTKEY_DEFAULT[TAB_DEFAULT],
...@@ -148,6 +153,15 @@ const update = { ...@@ -148,6 +153,15 @@ const update = {
return { focusedAnnotationMap: action.focused }; return { focusedAnnotationMap: action.focused };
}, },
SET_FOCUS_MODE_FOCUSED: function(state, action) {
return {
focusMode: {
...state.focusMode,
focused: action.focused,
},
};
},
SET_FORCE_VISIBLE: function(state, action) { SET_FORCE_VISIBLE: function(state, action) {
return { forceVisible: action.forceVisible }; return { forceVisible: action.forceVisible };
}, },
...@@ -309,6 +323,16 @@ function setFilterQuery(query) { ...@@ -309,6 +323,16 @@ function setFilterQuery(query) {
}; };
} }
/**
* Set the focused to only show annotations by the focused user.
*/
function setFocusModeFocused(focused) {
return {
type: actions.SET_FOCUS_MODE_FOCUSED,
focused,
};
}
/** Sets the sort key for the annotation list. */ /** Sets the sort key for the annotation list. */
function setSortKey(key) { function setSortKey(key) {
return { return {
...@@ -355,6 +379,55 @@ const getFirstSelectedAnnotationId = createSelector( ...@@ -355,6 +379,55 @@ const getFirstSelectedAnnotationId = createSelector(
function filterQuery(state) { function filterQuery(state) {
return state.filterQuery; return state.filterQuery;
} }
/**
* Returns the on/off state of the focus mode. This can be toggled on or off to
* filter to the focused user.
*
* @return {boolean}
*/
function focusModeFocused(state) {
return state.focusMode.enabled && state.focusMode.focused;
}
/**
* Returns the value of the focus mode from the config.
*
* @return {boolean}
*/
function focusModeEnabled(state) {
return state.focusMode.enabled;
}
/**
* Returns the username of the focused mode or null if none is found.
*
* @return {object}
*/
function focusModeUsername(state) {
if (state.focusMode.config.user && state.focusMode.config.user.username) {
return state.focusMode.config.user.username;
}
return null;
}
/**
* Returns the display name for a user or the username
* if display name is not present. If both are missing
* then this returns an empty string.
*
* @return {string}
*/
function focusModeUserPrettyName(state) {
const user = state.focusMode.config.user;
if (!user) {
return '';
} else if (user.displayName) {
return user.displayName;
} else if (user.username) {
return user.username;
} else {
return '';
}
}
module.exports = { module.exports = {
init: init, init: init,
...@@ -369,6 +442,7 @@ module.exports = { ...@@ -369,6 +442,7 @@ module.exports = {
selectTab: selectTab, selectTab: selectTab,
setCollapsed: setCollapsed, setCollapsed: setCollapsed,
setFilterQuery: setFilterQuery, setFilterQuery: setFilterQuery,
setFocusModeFocused: setFocusModeFocused,
setForceVisible: setForceVisible, setForceVisible: setForceVisible,
setSortKey: setSortKey, setSortKey: setSortKey,
toggleSelectedAnnotations: toggleSelectedAnnotations, toggleSelectedAnnotations: toggleSelectedAnnotations,
...@@ -377,6 +451,10 @@ module.exports = { ...@@ -377,6 +451,10 @@ module.exports = {
selectors: { selectors: {
hasSelectedAnnotations, hasSelectedAnnotations,
filterQuery, filterQuery,
focusModeFocused,
focusModeEnabled,
focusModeUsername,
focusModeUserPrettyName,
isAnnotationSelected, isAnnotationSelected,
getFirstSelectedAnnotationId, getFirstSelectedAnnotationId,
}, },
......
...@@ -186,6 +186,84 @@ describe('store/modules/selection', () => { ...@@ -186,6 +186,84 @@ describe('store/modules/selection', () => {
}); });
}); });
describe('setFocusModeFocused()', function() {
it('sets the focus mode to enabled', function() {
store.setFocusModeFocused(true);
assert.equal(store.getState().focusMode.focused, true);
});
it('sets the focus mode to not enabled', function() {
store = createStore([selection], [{ focus: { user: {} } }]);
store.setFocusModeFocused(false);
assert.equal(store.getState().focusMode.focused, false);
});
});
describe('focusModeEnabled()', function() {
it('should be true when the focus setting is present', function() {
store = createStore([selection], [{ focus: { user: {} } }]);
assert.equal(store.focusModeEnabled(), true);
});
it('should be false when the focus setting is not present', function() {
assert.equal(store.focusModeEnabled(), false);
});
});
describe('focusModeFocused()', function() {
it('should return true by default when focus mode is enabled', function() {
store = createStore([selection], [{ focus: { user: {} } }]);
assert.equal(store.getState().focusMode.enabled, true);
assert.equal(store.getState().focusMode.focused, true);
assert.equal(store.focusModeFocused(), true);
});
it('should return false by default when focus mode is not enabled', function() {
assert.equal(store.getState().focusMode.enabled, false);
assert.equal(store.getState().focusMode.focused, true);
assert.equal(store.focusModeFocused(), false);
});
});
describe('focusModeUserPrettyName()', function() {
it('should return false by default when focus mode is not enabled', function() {
store = createStore(
[selection],
[{ focus: { user: { displayName: 'FakeDisplayName' } } }]
);
assert.equal(store.focusModeUserPrettyName(), 'FakeDisplayName');
});
it('should the username when displayName is missing', function() {
store = createStore(
[selection],
[{ focus: { user: { username: 'FakeUserName' } } }]
);
assert.equal(store.focusModeUserPrettyName(), 'FakeUserName');
});
it('should an return empty string when user object has no names', function() {
store = createStore([selection], [{ focus: { user: {} } }]);
assert.equal(store.focusModeUserPrettyName(), '');
});
it('should an return empty string when there is no focus object', function() {
assert.equal(store.focusModeUserPrettyName(), '');
});
});
describe('focusModeUsername()', function() {
it('should return the user name when present', function() {
store = createStore(
[selection],
[{ focus: { user: { username: 'FakeUserName' } } }]
);
assert.equal(store.focusModeUsername(), 'FakeUserName');
});
it('should return null when the username is not present', function() {
store = createStore([selection], [{ focus: { user: {} } }]);
assert.isNull(store.focusModeUsername());
});
it('should return null when the user object is not present', function() {
assert.isNull(store.focusModeUsername());
});
});
describe('highlightAnnotations()', function() { describe('highlightAnnotations()', function() {
it('sets the highlighted annotations', function() { it('sets the highlighted annotations', function() {
store.highlightAnnotations(['id1', 'id2']); store.highlightAnnotations(['id1', 'id2']);
......
.focused-mode-header { .focused-mode-header {
padding: 5px; margin-bottom: 10px;
.focused-mode-header__user { button {
font-weight: 800; width: 100%;
height: 24px;
margin-right: 10px;
&:focus {
outline: none;
}
} }
} }
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