Commit c8b3aa87 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Hide inaccurate annotation counts when in user-focused mode

- Do not render the count of annotations or notes that will be visible
  if a selection-mode filter is cleared when also in user-focused mode,
  as this number is inaccurate.
- Refactor component and tests to make different “modes” more codified
parent ee29a02d
'use strict';
const { Fragment, createElement } = require('preact');
const { createElement } = require('preact');
const propTypes = require('prop-types');
const { useMemo } = require('preact/hooks');
......@@ -11,8 +11,6 @@ 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(
......@@ -24,113 +22,164 @@ const countVisibleAnns = annThread => {
};
/**
* A bar where the user can clear a selection or search and see whether
* any search results were found.
* UI for displaying information about the currently-applied filtering of
* annotations, and, in some cases, a mechanism for clearing the filter(s).
* */
function SearchStatusBar({ rootThread }) {
const thread = useStore(store => rootThread.thread(store.getRootState()));
const actions = useStore(store => ({
clearSelection: store.clearSelection,
}));
const storeState = useStore(store => ({
annotationCount: store.annotationCount(),
const counts = useStore(store => ({
annotations: store.annotationCount(),
notes: store.noteCount(),
}));
const {
directLinkedGroupFetchFailed,
filterQuery,
focusModeFocused,
focusModeUserPrettyName,
selectionMap,
selectedTab,
} = useStore(store => ({
directLinkedGroupFetchFailed: store.getRootState().directLinked
.directLinkedGroupFetchFailed,
filterQuery: store.getRootState().selection.filterQuery,
focusModeFocused: store.focusModeFocused(),
focusModeUserPrettyName: store.focusModeUserPrettyName(),
noteCount: store.noteCount(),
selectedAnnotationMap: store.getRootState().selection.selectedAnnotationMap,
selectionMap: store.getRootState().selection.selectedAnnotationMap,
selectedTab: store.getRootState().selection.selectedTab,
}));
const filterActive = !!storeState.filterQuery;
const thread = useStore(store => rootThread.thread(store.getRootState()));
// The search status bar UI represents multiple "modes" of filtering
const modes = {
/**
* @type {Boolean}
* A search (filter) query, visible to the user in the search bar, is
* currently applied
*/
filtered: !!filterQuery,
/**
* @type {Boolean}
* The client has a currently-applied focus on a single user. Superseded by
* `filtered` mode.
*/
focused: focusModeFocused && !filterQuery,
/**
* @type {Boolean}
* 0 - n annotations are currently "selected", by, e.g. clicking on highlighted
* text in the host page, direct-linking to an annotation, etc. Superseded by
* `filtered` mode.
*/
selected: (() => {
if (directLinkedGroupFetchFailed) {
return true;
}
return (
!!selectionMap && Object.keys(selectionMap).length > 0 && !filterQuery
);
})(),
};
const visibleCount = useMemo(() => {
return countVisibleAnns(thread);
}, [thread]);
const filterResults = (() => {
// Each "mode" has corresponding descriptive text about the number of
// matching/applicable annotations and, sometimes, a way to clear the
// filter
const modeText = {
filtered: (() => {
switch (visibleCount) {
case 0:
return `No results for "${storeState.filterQuery}"`;
return `No results for "${filterQuery}"`;
case 1:
return '1 search result';
default:
return `${visibleCount} search results`;
}
})();
const focusResults = (() => {
})(),
focused: (() => {
switch (visibleCount) {
case 0:
return `No annotations for ${storeState.focusModeUserPrettyName}`;
return `No annotations for ${focusModeUserPrettyName}`;
case 1:
return 'Showing 1 annotation';
default:
return `Showing ${visibleCount} annotations`;
}
})();
const areNotAllAnnotationsVisible = () => {
if (storeState.directLinkedGroupFetchFailed) {
return true;
})(),
selected: (() => {
// Generate the proper text to show on the clear-selection button.
// For non-user-focused modes, we can display the number of annotations
// that will be visible if the selection is cleared (`counts.annotations`)
// but this number is inaccurate/misleading when also focused on a user.
let selectedText;
switch (selectedTab) {
case uiConstants.TAB_ORPHANS:
selectedText = 'Show all annotations and notes';
break;
case uiConstants.TAB_NOTES:
selectedText = 'Show all notes';
if (counts.notes > 1 && !modes.focused) {
selectedText += ` (${counts.notes})`;
} else if (modes.focused) {
selectedText += ` by ${focusModeUserPrettyName}`;
}
break;
case uiConstants.TAB_ANNOTATIONS:
selectedText = 'Show all annotations';
if (counts.annotations > 1 && !modes.focused) {
selectedText += ` (${counts.annotations})`;
} else if (modes.focused) {
selectedText = `Show all annotations by ${focusModeUserPrettyName}`;
}
const selection = storeState.selectedAnnotationMap;
if (!selection) {
return false;
break;
}
return Object.keys(selection).length > 0;
return selectedText;
})(),
};
const btnProps = {
className: 'primary-action-btn primary-action-btn--short',
onClick: actions.clearSelection,
};
return (
<div>
{filterActive && (
{modes.filtered && (
<div className="search-status-bar">
<button
className="primary-action-btn primary-action-btn--short"
onClick={actions.clearSelection}
title="Clear the search filter and show all annotations"
{...btnProps}
>
<i className="primary-action-btn__icon h-icon-close" />
Clear search
</button>
<span>{filterResults}</span>
<span className="search-status-bar__filtered-text">
{modeText.filtered}
</span>
</div>
)}
{!filterActive && storeState.focusModeFocused && (
{modes.focused && (
<div className="search-status-bar">
<strong>{focusResults}</strong>
<span className="search-status-bar__focused-text">
<strong>{modeText.focused}</strong>
</span>
</div>
)}
{!filterActive && areNotAllAnnotationsVisible() && (
{modes.selected && (
<div className="search-status-bar">
<button
className="primary-action-btn primary-action-btn--short"
onClick={actions.clearSelection}
title="Clear the selection and show all annotations"
{...btnProps}
>
{storeState.selectedTab === uiConstants.TAB_ORPHANS && (
<Fragment>Show all annotations and notes</Fragment>
)}
{storeState.selectedTab === uiConstants.TAB_ANNOTATIONS && (
<Fragment>
Show all annotations
{storeState.annotationCount > 1 && (
<span> ({storeState.annotationCount})</span>
)}
</Fragment>
)}
{storeState.selectedTab === uiConstants.TAB_NOTES && (
<Fragment>
Show all notes
{storeState.noteCount > 1 && (
<span> ({storeState.noteCount})</span>
)}
</Fragment>
)}
<span className="search-status-bar__selected-text">
{modeText.selected}
</span>
</button>
</div>
)}
......
......@@ -40,6 +40,20 @@ describe('SearchStatusBar', () => {
SearchStatusBar.$imports.$restore();
});
context('user search query is applied', () => {
beforeEach(() => {
fakeStore.getRootState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
directLinked: {
directLinkedGroupFetchFailed: false,
},
});
fakeStore.annotationCount.returns(3);
});
[
{
description:
......@@ -97,16 +111,6 @@ describe('SearchStatusBar', () => {
fakeRootThread.thread.returns({
children: test.children,
});
fakeStore.getRootState.returns({
selection: {
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
directLinked: {
directLinkedGroupFetchFailed: false,
},
});
fakeStore.annotationCount.returns(3);
const wrapper = createComponent({});
......@@ -117,6 +121,7 @@ describe('SearchStatusBar', () => {
assert.equal(searchResultsText, test.expectedText);
});
});
});
context('user-focused mode applied', () => {
beforeEach(() => {
......@@ -129,6 +134,7 @@ describe('SearchStatusBar', () => {
const buttons = wrapper.find('button');
assert.equal(buttons.length, 0);
});
[
{
description:
......@@ -157,18 +163,25 @@ describe('SearchStatusBar', () => {
children: test.children,
});
const wrapper = createComponent({});
const resultText = wrapper.find('strong').text();
const resultText = wrapper
.find('.search-status-bar__focused-text')
.text();
assert.equal(resultText, test.expected);
});
});
it('should not display user-focused mode text if filtered mode is (also) applied', () => {
fakeRootThread.thread.returns({
children: [
{ id: '1', visible: true, children: [] },
{ id: '2', visible: true, children: [] },
],
});
it('displays "Show all annotations" button when a direct-linked group fetch fails', () => {
fakeStore.getRootState.returns({
selection: {
filterQuery: null,
selectedAnnotationMap: { annId: true },
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
directLinked: {},
......@@ -176,100 +189,143 @@ describe('SearchStatusBar', () => {
const wrapper = createComponent({});
const buttonText = wrapper.find('button').text();
assert.equal(buttonText, 'Show all annotations');
const focusedTextEl = wrapper.find('.search-status-bar__focused-text');
const filteredTextEl = wrapper.find('.search-status-bar__filtered-text');
assert.isFalse(focusedTextEl.exists());
assert.isTrue(filteredTextEl.exists());
});
});
it('displays "Show all annotations" button when there are selected annotations', () => {
context('selected-annotation(s) mode applied', () => {
context('selected mode only', () => {
[
{
description:
'should display show-all-annotation button with annotation count',
tab: 'annotation',
annotationCount: 5,
noteCount: 3,
buttonText: 'Show all annotations (5)',
},
{
description:
'should display show-all-annotation button with annotation and notes count',
tab: 'orphan',
annotationCount: 5,
noteCount: 3,
buttonText: 'Show all annotations and notes',
},
{
description: 'should display show-all-notes button with note count',
tab: 'note',
annotationCount: 3,
noteCount: 3,
buttonText: 'Show all notes (3)',
},
{
description:
'should display show-all-annotation button with no count',
tab: 'annotation',
annotationCount: 1,
noteCount: 3,
buttonText: 'Show all annotations',
},
{
description: 'should display show-all-notes button with no count',
tab: 'note',
annotationCount: 1,
noteCount: 1,
buttonText: 'Show all notes',
},
].forEach(test => {
it(test.description, () => {
fakeStore.getRootState.returns({
selection: {
filterQuery: null,
selectedAnnotationMap: { annId: true },
selectedTab: 'annotation',
},
directLinked: {
directLinkedGroupFetchFailed: false,
selectedTab: test.tab,
},
directLinked: {},
});
fakeStore.annotationCount.returns(test.annotationCount);
fakeStore.noteCount.returns(test.noteCount);
const wrapper = createComponent({});
const buttonText = wrapper.find('button').text();
assert.equal(buttonText, 'Show all annotations');
const button = wrapper.find('button');
const selectedText = wrapper.find(
'.search-status-bar__selected-text'
);
assert.isTrue(button.exists());
assert.equal(button.text(), test.buttonText);
assert.isTrue(selectedText.exists());
});
});
});
[null, {}].forEach(selectedAnnotationMap => {
it('does not display "Show all annotations" button when there are no selected annotations', () => {
context('combined with applied user-focused mode', () => {
[
{ tab: 'annotation', buttonText: 'Show all annotations by Fake User' },
{ tab: 'orphan', buttonText: 'Show all annotations and notes' },
{ tab: 'note', buttonText: 'Show all notes by Fake User' },
].forEach(test => {
it(`displays correct text for tab '${test.tab}', without count`, () => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
fakeStore.getRootState.returns({
selection: {
filterQuery: null,
selectedAnnotationMap: selectedAnnotationMap,
selectedTab: 'annotation',
selectedTab: test.tab,
},
directLinked: {
directLinkedGroupFetchFailed: false,
directLinkedGroupFetchFailed: true,
},
});
fakeStore.annotationCount.returns(5);
fakeStore.noteCount.returns(3);
const wrapper = createComponent({});
const buttons = wrapper.find('button');
assert.equal(buttons.length, 0);
const button = wrapper.find('button');
const selectedText = wrapper.find(
'.search-status-bar__selected-text'
);
assert.isTrue(button.exists());
assert.equal(button.text(), test.buttonText);
assert.isTrue(selectedText.exists());
});
});
});
[
{
description:
'displays "Show all annotations and notes" button when the orphans tab is selected',
selectedTab: 'orphan',
totalAnnotations: 1,
totalNotes: 1,
expectedText: 'Show all annotations and notes',
},
{
description:
'displays "Show all notes" button when the notes tab is selected',
selectedTab: 'note',
totalAnnotations: 1,
totalNotes: 1,
expectedText: 'Show all notes',
},
{
description:
'displays "Show all notes (2)" button when the notes tab is selected and there are two notes',
selectedTab: 'note',
totalAnnotations: 2,
totalNotes: 2,
expectedText: 'Show all notes (2)',
},
{
description:
'displays "Show all annotations (2)" button when the notes tab is selected and there are two annotations',
selectedTab: 'annotation',
totalAnnotations: 2,
totalNotes: 2,
expectedText: 'Show all annotations (2)',
},
].forEach(test => {
it(test.description, () => {
context('combined with applied query filter', () => {
// Applied-query mode wins out here; no selection UI rendered
it('does not show selected-mode elements', () => {
fakeStore.focusModeFocused = sinon.stub().returns(true);
fakeStore.getRootState.returns({
selection: {
filterQuery: null,
selectedAnnotationMap: { annId: true },
selectedTab: test.selectedTab,
filterQuery: 'tag:foo',
selectedTab: 'annotation',
},
directLinked: {
directLinkedGroupFetchFailed: false,
},
});
fakeStore.noteCount.returns(test.totalNotes);
fakeStore.annotationCount.returns(test.totalAnnotations);
fakeStore.annotationCount.returns(5);
fakeStore.noteCount.returns(3);
const wrapper = createComponent({});
const buttonText = wrapper.find('button').text();
assert.equal(buttonText, test.expectedText);
const button = wrapper.find('button');
const selectedText = wrapper.find('.search-status-bar__selected-text');
const filteredText = wrapper.find('.search-status-bar__filtered-text');
assert.isTrue(button.exists());
assert.isFalse(selectedText.exists());
assert.isTrue(filteredText.exists());
});
});
});
// });
});
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