Commit 0b1d00e0 authored by Robert Knight's avatar Robert Knight

Show standalone filter controls when search panel is hidden

When the search panel is open, filter controls are displayed inside it. To make
filter controls accessible when the search panel is closed, render standalone
controls from `SidebarView`, wrapper in a container card.

Rendering of the card is handled by `FilterControls`, so that the logic for
conditionally rendering depending on whether filters are configured, does not
need to be duplicated.
parent 0d047312
......@@ -13,6 +13,7 @@ import SidebarContentError from './SidebarContentError';
import ThreadList from './ThreadList';
import { useRootThread } from './hooks/use-root-thread';
import FilterStatus from './old-search/FilterStatus';
import FilterControls from './search/FilterControls';
export type SidebarViewProps = {
onLogin: () => void;
......@@ -39,8 +40,8 @@ function SidebarView({
// Store state values
const store = useSidebarStore();
const focusedGroupId = store.focusedGroupId();
const hasAppliedFilter =
store.hasAppliedFilter() || store.hasSelectedAnnotations();
const hasSelection = store.hasSelectedAnnotations();
const hasAppliedFilter = store.hasAppliedFilter() || hasSelection;
const isLoading = store.isLoading();
const isLoggedIn = store.isLoggedIn();
......@@ -67,10 +68,19 @@ function SidebarView({
hasDirectLinkedAnnotationError || hasDirectLinkedGroupError;
const searchPanelEnabled = store.isFeatureEnabled('search_panel');
const showFilterStatus = !hasContentError && !searchPanelEnabled;
const showTabs =
!hasContentError && (searchPanelEnabled || !hasAppliedFilter);
// Whether to render the old filter status UI. If no filter is active, this
// will render nothing.
const showFilterStatus = !hasContentError && !searchPanelEnabled;
// Whether to render the new filter UI. Note that when the search panel is
// open, filter controls are integrated into it. The UI may render nothing
// if no filters are configured or selection is active.
const isSearchPanelOpen = store.isSidebarPanelOpen('searchAnnotations');
const showFilterControls = searchPanelEnabled && !isSearchPanelOpen;
// Show a CTA to log in if successfully viewing a direct-linked annotation
// and not logged in
const showLoggedOutMessage =
......@@ -134,6 +144,7 @@ function SidebarView({
<div>
<h2 className="sr-only">Annotations</h2>
{showFilterStatus && <FilterStatus />}
{showFilterControls && <FilterControls withCardContainer />}
<LoginPromptPanel onLogin={onLogin} onSignUp={onSignUp} />
{hasDirectLinkedAnnotationError && (
<SidebarContentError
......
import {
Card,
CardContent,
Button,
FileGenericIcon,
MinusIcon,
......@@ -6,6 +8,8 @@ import {
ProfileIcon,
} from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import { Fragment } from 'preact';
import type { ComponentChildren } from 'preact';
import { useSidebarStore } from '../../store';
......@@ -82,6 +86,31 @@ function FilterToggle({
);
}
/**
* Container for the filter controls when it is rendered standalone, outside
* the search panel.
*/
function CardContainer({ children }: { children: ComponentChildren }) {
return (
<div className="mb-3">
<Card>
<CardContent>{children}</CardContent>
</Card>
</div>
);
}
export type FilterControlsProps = {
/**
* Whether to render the controls in a card container.
*
* The container is rendered by `FilterControls` rather than the parent,
* because `FilterControls` can conditionally render nothing if no filter is
* configured.
*/
withCardContainer?: boolean;
};
/**
* Displays the state of various filters and allows the user to toggle them.
*
......@@ -94,7 +123,9 @@ function FilterToggle({
* This doesn't include the state of other layers of filters which have their
* own UI controls such as search or the annotation type tabs.
*/
export default function FilterControls() {
export default function FilterControls({
withCardContainer = false,
}: FilterControlsProps) {
const store = useSidebarStore();
const selectedCount = store.selectedAnnotations().length;
......@@ -114,51 +145,55 @@ export default function FilterControls() {
return null;
}
const Container = withCardContainer ? CardContainer : Fragment;
return (
<div
className="flex flex-row gap-x-2 items-center"
data-testid="filter-controls"
>
<b>Filters</b>
{hasSelection && (
<FilterToggle
label={`${selectedCount} selected`}
description={`Show ${selectedCount} selected annotations`}
active={true}
setActive={() => store.clearSelection()}
testId="selection-toggle"
/>
)}
{focusFilters.user && (
<FilterToggle
icon={ProfileIcon}
label={`By ${focusFilters.user.display}`}
description={`Show annotations by ${focusFilters.user.display}`}
active={focusActive.has('user')}
setActive={() => store.toggleFocusMode({ key: 'user' })}
testId="user-focus-toggle"
/>
)}
{focusFilters.page && (
<FilterToggle
icon={FileGenericIcon}
label={`Pages ${focusFilters.page.display}`}
description={`Show annotations on pages ${focusFilters.page.display}`}
active={focusActive.has('page')}
setActive={() => store.toggleFocusMode({ key: 'page' })}
testId="page-focus-toggle"
/>
)}
{focusFilters.cfi && (
<FilterToggle
icon={FileGenericIcon}
label="Selected chapter"
description="Show annotations on selected book chapter(s)"
active={focusActive.has('cfi')}
setActive={() => store.toggleFocusMode({ key: 'cfi' })}
testId="cfi-focus-toggle"
/>
)}
</div>
<Container>
<div
className="flex flex-row gap-x-2 items-center"
data-testid="filter-controls"
>
<b>Filters</b>
{hasSelection && (
<FilterToggle
label={`${selectedCount} selected`}
description={`Show ${selectedCount} selected annotations`}
active={true}
setActive={() => store.clearSelection()}
testId="selection-toggle"
/>
)}
{focusFilters.user && (
<FilterToggle
icon={ProfileIcon}
label={`By ${focusFilters.user.display}`}
description={`Show annotations by ${focusFilters.user.display}`}
active={focusActive.has('user')}
setActive={() => store.toggleFocusMode({ key: 'user' })}
testId="user-focus-toggle"
/>
)}
{focusFilters.page && (
<FilterToggle
icon={FileGenericIcon}
label={`Pages ${focusFilters.page.display}`}
description={`Show annotations on pages ${focusFilters.page.display}`}
active={focusActive.has('page')}
setActive={() => store.toggleFocusMode({ key: 'page' })}
testId="page-focus-toggle"
/>
)}
{focusFilters.cfi && (
<FilterToggle
icon={FileGenericIcon}
label="Selected chapter"
description="Show annotations on selected book chapter(s)"
active={focusActive.has('cfi')}
setActive={() => store.toggleFocusMode({ key: 'cfi' })}
testId="cfi-focus-toggle"
/>
)}
</div>
</Container>
);
}
......@@ -5,8 +5,8 @@ import FilterControls, { $imports } from '../FilterControls';
describe('FilterControls', () => {
let fakeStore;
const createComponent = () => {
return mount(<FilterControls />);
const createComponent = (props = {}) => {
return mount(<FilterControls {...props} />);
};
beforeEach(() => {
......@@ -59,9 +59,11 @@ describe('FilterControls', () => {
}
}
it('returns nothing if there are no filters', () => {
const wrapper = createComponent();
assert.equal(wrapper.html(), '');
[true, false].forEach(withCardContainer => {
it('returns nothing if there are no filters', () => {
const wrapper = createComponent({ withCardContainer });
assert.equal(wrapper.html(), '');
});
});
it('displays selection toggle if there is a selection', () => {
......@@ -76,6 +78,17 @@ describe('FilterControls', () => {
assert.calledOnce(fakeStore.clearSelection);
});
it('renders card container if requested', () => {
fakeStore.selectedAnnotations.returns([{ id: 'abc' }]);
const wrapper = createComponent({ withCardContainer: true });
// The main controls of the component should be present, with an additional
// container.
assert.isTrue(wrapper.exists('Card'));
const toggle = new ToggleButtonWrapper(wrapper, 'selection-toggle');
assert.isTrue(toggle.exists());
});
[
{
filterType: 'user',
......
......@@ -66,6 +66,7 @@ describe('SidebarView', () => {
hasSidebarOpened: sinon.stub(),
isLoading: sinon.stub().returns(false),
isLoggedIn: sinon.stub(),
isSidebarPanelOpen: sinon.stub().returns(false),
profile: sinon.stub().returns({ userid: null }),
searchUris: sinon.stub().returns([]),
toggleFocusMode: sinon.stub(),
......@@ -247,10 +248,36 @@ describe('SidebarView', () => {
});
});
context('user-focus mode', () => {
it('shows old FilterStatus component when `search_panel` feature is disabled', () => {
describe('filter controls', () => {
it('renders old filter controls when `search_panel` feature is disabled', () => {
fakeStore.isFeatureEnabled.withArgs('search_panel').returns(false);
const wrapper = createComponent();
assert.isTrue(wrapper.find('FilterStatus').exists());
assert.isTrue(wrapper.exists('FilterStatus'));
assert.isFalse(wrapper.exists('FilterControls'));
});
[
{
searchPanelOpen: false,
showControls: true,
},
{
searchPanelOpen: true,
showControls: false,
},
].forEach(({ searchPanelOpen, showControls }) => {
it(`renders new filter controls when "search_panel" is enabled and search panel is not open`, () => {
fakeStore.isFeatureEnabled.withArgs('search_panel').returns(true);
fakeStore.isSidebarPanelOpen
.withArgs('searchAnnotations')
.returns(searchPanelOpen);
const wrapper = createComponent();
assert.isFalse(wrapper.exists('FilterStatus'));
assert.equal(wrapper.exists('FilterControls'), showControls);
});
});
});
......
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