Unverified Commit 877597b6 authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1336 from hypothesis/annotation-counts

Add annotation counts and iterate on UX for user-focused mode
parents 20f10cf6 39b5a6f4
......@@ -32,11 +32,12 @@ function FocusedModeHeader() {
<div className="focused-mode-header__filter-status">
{selectors.focusModeFocused ? (
<span>
Annotations by <strong>{selectors.focusModeUserPrettyName}</strong>{' '}
only
Showing <strong>{selectors.focusModeUserPrettyName}</strong> only
</span>
) : (
<span>Everybody&rsquo;s annotations</span>
<span>
Showing <strong>all</strong>
</span>
)}
</div>
);
......@@ -50,7 +51,7 @@ function FocusedModeHeader() {
})();
return (
<div className="focused-mode-header sheet">
<div className="focused-mode-header sheet sheet--short">
{filterStatus}
<button onClick={toggleFocusedMode} className="focused-mode-header__btn">
{buttonText}
......
......@@ -8,6 +8,12 @@ const { withServices } = require('../util/service-context');
const uiConstants = require('../ui-constants');
const useStore = require('../store/use-store');
/**
* Of the annotations in the thread `annThread`, how many
* are currently `visible` in the browser (sidebar)?
*
* TODO: This function should be a selector or a reusable util
*/
const countVisibleAnns = annThread => {
return annThread.children.reduce(
function(count, child) {
......@@ -22,22 +28,23 @@ const countVisibleAnns = annThread => {
* any search results were found.
* */
function SearchStatusBar({ rootThread }) {
const {
directLinkedGroupFetchFailed,
filterQuery,
selectedAnnotationMap,
selectedTab,
} = useStore(store => ({
const actions = useStore(store => ({
clearSelection: store.clearSelection,
}));
const storeState = useStore(store => ({
annotationCount: store.annotationCount(),
directLinkedGroupFetchFailed: store.getRootState().directLinked
.directLinkedGroupFetchFailed,
filterQuery: store.getRootState().selection.filterQuery,
focusModeFocused: store.focusModeFocused(),
focusModeUserPrettyName: store.focusModeUserPrettyName(),
noteCount: store.noteCount(),
selectedAnnotationMap: store.getRootState().selection.selectedAnnotationMap,
selectedTab: store.getRootState().selection.selectedTab,
}));
const clearSelection = useStore(store => store.clearSelection);
const filterActive = !!filterQuery;
const annotationCount = useStore(store => store.annotationCount());
const noteCount = useStore(store => store.noteCount());
const filterActive = !!storeState.filterQuery;
const thread = useStore(store => rootThread.thread(store.getRootState()));
......@@ -45,23 +52,33 @@ function SearchStatusBar({ rootThread }) {
return countVisibleAnns(thread);
}, [thread]);
const filterResults = () => {
const resultsCount = visibleCount;
switch (resultsCount) {
const filterResults = (() => {
switch (visibleCount) {
case 0:
return 'No results for "' + filterQuery + '"';
return `No results for "${storeState.filterQuery}"`;
case 1:
return '1 search result';
default:
return resultsCount + ' search results';
return `${visibleCount} search results`;
}
};
})();
const focusResults = (() => {
switch (visibleCount) {
case 0:
return `No annotations for ${storeState.focusModeUserPrettyName}`;
case 1:
return 'Showing 1 annotation';
default:
return `Showing ${visibleCount} annotations`;
}
})();
const areNotAllAnnotationsVisible = () => {
if (directLinkedGroupFetchFailed) {
if (storeState.directLinkedGroupFetchFailed) {
return true;
}
const selection = selectedAnnotationMap;
const selection = storeState.selectedAnnotationMap;
if (!selection) {
return false;
}
......@@ -74,35 +91,44 @@ function SearchStatusBar({ rootThread }) {
<div className="search-status-bar">
<button
className="primary-action-btn primary-action-btn--short"
onClick={clearSelection}
onClick={actions.clearSelection}
title="Clear the search filter and show all annotations"
>
<i className="primary-action-btn__icon h-icon-close" />
Clear search
</button>
<span>{filterResults()}</span>
<span>{filterResults}</span>
</div>
)}
{!filterActive && storeState.focusModeFocused && (
<div className="search-status-bar">
<strong>{focusResults}</strong>
</div>
)}
{!filterActive && areNotAllAnnotationsVisible() && (
<div className="search-status-bar">
<button
className="primary-action-btn primary-action-btn--short"
onClick={clearSelection}
onClick={actions.clearSelection}
title="Clear the selection and show all annotations"
>
{selectedTab === uiConstants.TAB_ORPHANS && (
{storeState.selectedTab === uiConstants.TAB_ORPHANS && (
<Fragment>Show all annotations and notes</Fragment>
)}
{selectedTab === uiConstants.TAB_ANNOTATIONS && (
{storeState.selectedTab === uiConstants.TAB_ANNOTATIONS && (
<Fragment>
Show all annotations
{annotationCount > 1 && <span> ({annotationCount})</span>}
{storeState.annotationCount > 1 && (
<span> ({storeState.annotationCount})</span>
)}
</Fragment>
)}
{selectedTab === uiConstants.TAB_NOTES && (
{storeState.selectedTab === uiConstants.TAB_NOTES && (
<Fragment>
Show all notes
{noteCount > 1 && <span> ({noteCount})</span>}
{storeState.noteCount > 1 && (
<span> ({storeState.noteCount})</span>
)}
</Fragment>
)}
</button>
......
......@@ -48,7 +48,7 @@ describe('FocusedModeHeader', function() {
it("should render status text indicating only that user's annotations are visible", () => {
const wrapper = createComponent();
assert.match(wrapper.text(), /Annotations by.+Fake User/);
assert.match(wrapper.text(), /Showing.+Fake User.+only/);
});
it('should render a button allowing the user to view all annotations', () => {
......@@ -68,7 +68,7 @@ describe('FocusedModeHeader', function() {
it("should render status text indicating that all user's annotations are visible", () => {
const wrapper = createComponent();
assert.match(wrapper.text(), /Everybody.*s annotations/);
assert.match(wrapper.text(), /Showing.+all/);
});
it("should render a button allowing the user to view only focus user's annotations", () => {
......
......@@ -26,6 +26,8 @@ describe('SearchStatusBar', () => {
directLinked: {},
}),
annotationCount: sinon.stub().returns(1),
focusModeFocused: sinon.stub().returns(false),
focusModeUserPrettyName: sinon.stub().returns('Fake User'),
noteCount: sinon.stub().returns(0),
};
......@@ -116,6 +118,52 @@ describe('SearchStatusBar', () => {
});
});
context('user-focused mode applied', () => {
beforeEach(() => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
});
it('should not display a clear/show-all-annotations button when user-focused', () => {
const wrapper = createComponent({});
const buttons = wrapper.find('button');
assert.equal(buttons.length, 0);
});
[
{
description:
'shows pluralized annotation count when multiple annotations match for user',
children: [
{ id: '1', visible: true, children: [] },
{ id: '2', visible: true, children: [] },
],
expected: 'Showing 2 annotations',
},
{
description:
'shows single annotation count when one annotation matches for user',
children: [{ id: '1', visible: true, children: [] }],
expected: 'Showing 1 annotation',
},
{
description:
'shows "no annotations" wording when no annotations match for user',
children: [],
expected: 'No annotations for Fake User',
},
].forEach(test => {
it(test.description, () => {
fakeRootThread.thread.returns({
children: test.children,
});
const wrapper = createComponent({});
const resultText = wrapper.find('strong').text();
assert.equal(resultText, test.expected);
});
});
});
it('displays "Show all annotations" button when a direct-linked group fetch fails', () => {
fakeStore.getRootState.returns({
selection: {
......
......@@ -12,11 +12,6 @@
height: 30px;
padding-left: 10px;
padding-right: 10px;
background-color: $grey-2;
color: $grey-5;
&:hover:enabled {
background-color: $grey-3;
}
}
&__filter-status {
......
......@@ -129,6 +129,10 @@ hypothesis-app {
}
}
.sheet--short {
padding: 0.5em 1em;
}
.sheet--is-theme-clean {
padding-left: 30px;
padding-bottom: 30px;
......
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