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'; ...@@ -13,6 +13,7 @@ import SidebarContentError from './SidebarContentError';
import ThreadList from './ThreadList'; import ThreadList from './ThreadList';
import { useRootThread } from './hooks/use-root-thread'; import { useRootThread } from './hooks/use-root-thread';
import FilterStatus from './old-search/FilterStatus'; import FilterStatus from './old-search/FilterStatus';
import FilterControls from './search/FilterControls';
export type SidebarViewProps = { export type SidebarViewProps = {
onLogin: () => void; onLogin: () => void;
...@@ -39,8 +40,8 @@ function SidebarView({ ...@@ -39,8 +40,8 @@ function SidebarView({
// Store state values // Store state values
const store = useSidebarStore(); const store = useSidebarStore();
const focusedGroupId = store.focusedGroupId(); const focusedGroupId = store.focusedGroupId();
const hasAppliedFilter = const hasSelection = store.hasSelectedAnnotations();
store.hasAppliedFilter() || store.hasSelectedAnnotations(); const hasAppliedFilter = store.hasAppliedFilter() || hasSelection;
const isLoading = store.isLoading(); const isLoading = store.isLoading();
const isLoggedIn = store.isLoggedIn(); const isLoggedIn = store.isLoggedIn();
...@@ -67,10 +68,19 @@ function SidebarView({ ...@@ -67,10 +68,19 @@ function SidebarView({
hasDirectLinkedAnnotationError || hasDirectLinkedGroupError; hasDirectLinkedAnnotationError || hasDirectLinkedGroupError;
const searchPanelEnabled = store.isFeatureEnabled('search_panel'); const searchPanelEnabled = store.isFeatureEnabled('search_panel');
const showFilterStatus = !hasContentError && !searchPanelEnabled;
const showTabs = const showTabs =
!hasContentError && (searchPanelEnabled || !hasAppliedFilter); !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 // Show a CTA to log in if successfully viewing a direct-linked annotation
// and not logged in // and not logged in
const showLoggedOutMessage = const showLoggedOutMessage =
...@@ -134,6 +144,7 @@ function SidebarView({ ...@@ -134,6 +144,7 @@ function SidebarView({
<div> <div>
<h2 className="sr-only">Annotations</h2> <h2 className="sr-only">Annotations</h2>
{showFilterStatus && <FilterStatus />} {showFilterStatus && <FilterStatus />}
{showFilterControls && <FilterControls withCardContainer />}
<LoginPromptPanel onLogin={onLogin} onSignUp={onSignUp} /> <LoginPromptPanel onLogin={onLogin} onSignUp={onSignUp} />
{hasDirectLinkedAnnotationError && ( {hasDirectLinkedAnnotationError && (
<SidebarContentError <SidebarContentError
......
import { import {
Card,
CardContent,
Button, Button,
FileGenericIcon, FileGenericIcon,
MinusIcon, MinusIcon,
...@@ -6,6 +8,8 @@ import { ...@@ -6,6 +8,8 @@ import {
ProfileIcon, ProfileIcon,
} from '@hypothesis/frontend-shared'; } from '@hypothesis/frontend-shared';
import classnames from 'classnames'; import classnames from 'classnames';
import { Fragment } from 'preact';
import type { ComponentChildren } from 'preact';
import { useSidebarStore } from '../../store'; import { useSidebarStore } from '../../store';
...@@ -82,6 +86,31 @@ function FilterToggle({ ...@@ -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. * Displays the state of various filters and allows the user to toggle them.
* *
...@@ -94,7 +123,9 @@ function FilterToggle({ ...@@ -94,7 +123,9 @@ function FilterToggle({
* This doesn't include the state of other layers of filters which have their * 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. * 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 store = useSidebarStore();
const selectedCount = store.selectedAnnotations().length; const selectedCount = store.selectedAnnotations().length;
...@@ -114,7 +145,10 @@ export default function FilterControls() { ...@@ -114,7 +145,10 @@ export default function FilterControls() {
return null; return null;
} }
const Container = withCardContainer ? CardContainer : Fragment;
return ( return (
<Container>
<div <div
className="flex flex-row gap-x-2 items-center" className="flex flex-row gap-x-2 items-center"
data-testid="filter-controls" data-testid="filter-controls"
...@@ -160,5 +194,6 @@ export default function FilterControls() { ...@@ -160,5 +194,6 @@ export default function FilterControls() {
/> />
)} )}
</div> </div>
</Container>
); );
} }
...@@ -5,8 +5,8 @@ import FilterControls, { $imports } from '../FilterControls'; ...@@ -5,8 +5,8 @@ import FilterControls, { $imports } from '../FilterControls';
describe('FilterControls', () => { describe('FilterControls', () => {
let fakeStore; let fakeStore;
const createComponent = () => { const createComponent = (props = {}) => {
return mount(<FilterControls />); return mount(<FilterControls {...props} />);
}; };
beforeEach(() => { beforeEach(() => {
...@@ -59,10 +59,12 @@ describe('FilterControls', () => { ...@@ -59,10 +59,12 @@ describe('FilterControls', () => {
} }
} }
[true, false].forEach(withCardContainer => {
it('returns nothing if there are no filters', () => { it('returns nothing if there are no filters', () => {
const wrapper = createComponent(); const wrapper = createComponent({ withCardContainer });
assert.equal(wrapper.html(), ''); assert.equal(wrapper.html(), '');
}); });
});
it('displays selection toggle if there is a selection', () => { it('displays selection toggle if there is a selection', () => {
fakeStore.selectedAnnotations.returns([{ id: 'abc' }]); fakeStore.selectedAnnotations.returns([{ id: 'abc' }]);
...@@ -76,6 +78,17 @@ describe('FilterControls', () => { ...@@ -76,6 +78,17 @@ describe('FilterControls', () => {
assert.calledOnce(fakeStore.clearSelection); 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', filterType: 'user',
......
...@@ -66,6 +66,7 @@ describe('SidebarView', () => { ...@@ -66,6 +66,7 @@ describe('SidebarView', () => {
hasSidebarOpened: sinon.stub(), hasSidebarOpened: sinon.stub(),
isLoading: sinon.stub().returns(false), isLoading: sinon.stub().returns(false),
isLoggedIn: sinon.stub(), isLoggedIn: sinon.stub(),
isSidebarPanelOpen: sinon.stub().returns(false),
profile: sinon.stub().returns({ userid: null }), profile: sinon.stub().returns({ userid: null }),
searchUris: sinon.stub().returns([]), searchUris: sinon.stub().returns([]),
toggleFocusMode: sinon.stub(), toggleFocusMode: sinon.stub(),
...@@ -247,10 +248,36 @@ describe('SidebarView', () => { ...@@ -247,10 +248,36 @@ describe('SidebarView', () => {
}); });
}); });
context('user-focus mode', () => { describe('filter controls', () => {
it('shows old FilterStatus component when `search_panel` feature is disabled', () => { it('renders old filter controls when `search_panel` feature is disabled', () => {
fakeStore.isFeatureEnabled.withArgs('search_panel').returns(false);
const wrapper = createComponent(); 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