diff --git a/.changeset/three-taxis-fix.md b/.changeset/three-taxis-fix.md new file mode 100644 index 0000000..547414d --- /dev/null +++ b/.changeset/three-taxis-fix.md @@ -0,0 +1,5 @@ +--- +'@envyjs/webui': patch +--- + +Collapse source and systems filter into button diff --git a/packages/webui/src/components/ui/FiltersAndActions.test.tsx b/packages/webui/src/components/ui/FiltersAndActions.test.tsx index c8aaf07..f1b1b08 100644 --- a/packages/webui/src/components/ui/FiltersAndActions.test.tsx +++ b/packages/webui/src/components/ui/FiltersAndActions.test.tsx @@ -7,9 +7,6 @@ import { setUseApplicationData } from '@/testing/mockUseApplication'; import FiltersAndActions from './FiltersAndActions'; jest.mock('@/components', () => ({ - SourceAndSystemFilter: function (props: any) { - return
Mock SourceAndSystemFilter component
; - }, SearchInput: function ({ onChange }: any) { return onChange(e.target.value)} />; }, @@ -55,16 +52,6 @@ describe('FiltersAndActions', () => { render(); }); - describe('sources and systems', () => { - it('should render SourceAndSystemFilter component', () => { - const { getByTestId } = render(); - - const sourcesAndSystems = getByTestId('sources-and-systems'); - expect(sourcesAndSystems).toBeVisible(); - expect(sourcesAndSystems).toHaveTextContent('Mock SourceAndSystemFilter component'); - }); - }); - describe('search term', () => { it('should render SearchInput component', () => { const { getByTestId } = render(); diff --git a/packages/webui/src/components/ui/FiltersAndActions.tsx b/packages/webui/src/components/ui/FiltersAndActions.tsx index 979ac62..5ce3e9b 100644 --- a/packages/webui/src/components/ui/FiltersAndActions.tsx +++ b/packages/webui/src/components/ui/FiltersAndActions.tsx @@ -1,8 +1,6 @@ import { SearchInput } from '@/components'; import useApplication from '@/hooks/useApplication'; -import SourceAndSystemFilter from './SourceAndSystemFilter'; - export default function FiltersAndActions() { const { setFilters } = useApplication(); @@ -13,10 +11,5 @@ export default function FiltersAndActions() { })); } - return ( - - - - - ); + return ; } diff --git a/packages/webui/src/components/ui/Header.test.tsx b/packages/webui/src/components/ui/Header.test.tsx index 6abd443..6e82a64 100644 --- a/packages/webui/src/components/ui/Header.test.tsx +++ b/packages/webui/src/components/ui/Header.test.tsx @@ -16,6 +16,13 @@ jest.mock( return
Mock FiltersAndActions component
; }, ); +jest.mock( + '@/components/ui/SourceAndSystemFilter', + () => + function SourceAndSystemFilter() { + return
Mock Source and Systems component
; + }, +); describe('Header', () => { const originalProcessEnv = process.env; diff --git a/packages/webui/src/components/ui/Header.tsx b/packages/webui/src/components/ui/Header.tsx index af50f36..b93e10b 100644 --- a/packages/webui/src/components/ui/Header.tsx +++ b/packages/webui/src/components/ui/Header.tsx @@ -5,6 +5,7 @@ import DarkModeToggle from '../DarkModeToggle'; import DebugToolbar from './DebugToolbar'; import FiltersAndActions from './FiltersAndActions'; import Logo from './Logo'; +import SourceAndSystemFilter from './SourceAndSystemFilter'; export default function Header() { const { enableThemeSwitcher } = useFeatureFlags(); @@ -21,6 +22,7 @@ export default function Header() {
+ {enableThemeSwitcher && } {isDebugMode && }
diff --git a/packages/webui/src/components/ui/SourceAndSystemFilter.test.tsx b/packages/webui/src/components/ui/SourceAndSystemFilter.test.tsx index 91750bb..6b66fe1 100644 --- a/packages/webui/src/components/ui/SourceAndSystemFilter.test.tsx +++ b/packages/webui/src/components/ui/SourceAndSystemFilter.test.tsx @@ -79,112 +79,6 @@ describe('SourceAndSystemFilter', () => { expect(component).toBeVisible(); }); - describe('placeholder', () => { - it('should display expected placeholder in the listbox when there are no registered systems', () => { - // mock that there are no registered systems - setupMockSystems([]); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('Sources...'); - }); - - it('should display expected placeholder in the listbox when there is one or more registered systems', () => { - setupMockSystems(mockSystems); - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('Sources & Systems'); - }); - }); - - describe('selection summary', () => { - it('should display expected selection summary when one source is selected', () => { - const filters: Filters = { - sources: ['source1'], - systems: [], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('1 source'); - }); - - it('should display expected selection summary when two sources are selected', () => { - const filters: Filters = { - sources: ['source1', 'source2'], - systems: [], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('2 sources'); - }); - - it('should display expected selection summary when one system is selected', () => { - const filters: Filters = { - sources: [], - systems: [mockSystems[0].name], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('1 system'); - }); - - it('should display expected selection summary when two systems are selected', () => { - const filters: Filters = { - sources: [], - systems: [mockSystems[0].name, mockSystems[1].name], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('2 systems'); - }); - - it('should display expected selection summary when a combination of sources and systems are selected', () => { - const filters: Filters = { - sources: ['source1'], - systems: [mockSystems[0].name, mockSystems[1].name], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('1 source, 2 systems'); - }); - - it('should display the placeholder when no sources or systems are selected', () => { - const filters: Filters = { - sources: [], - systems: [], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByRole } = render(); - - const component = getByRole('listbox'); - expect(component).toHaveTextContent('Sources & Systems'); - }); - }); - describe('selection options', () => { describe('with no registered systems', () => { beforeEach(() => { @@ -220,7 +114,6 @@ describe('SourceAndSystemFilter', () => { const noSourcesMessage = getByTestId('no-sources'); expect(noSourcesMessage).toBeVisible(); - expect(noSourcesMessage).toHaveTextContent('No sources connected...'); }); it('should display source options', async () => { @@ -239,22 +132,6 @@ describe('SourceAndSystemFilter', () => { expect(systemItems).not.toBeInTheDocument(); }); - it('should not display source options heading', async () => { - const { queryByTestId } = await openDropDown(); - - const sourceItemsHeading = queryByTestId('source-items-heading'); - - expect(sourceItemsHeading).not.toBeInTheDocument(); - }); - - it('should not display source / system divider', async () => { - const { queryByTestId } = await openDropDown(); - - const itemsDivider = queryByTestId('items-divider'); - - expect(itemsDivider).not.toBeInTheDocument(); - }); - it('should display each source as an option', async () => { const { getAllByTestId } = await openDropDown(); @@ -271,8 +148,8 @@ describe('SourceAndSystemFilter', () => { const sourceItems = getAllByTestId('source-item'); - expect(within(sourceItems.at(0)!).getByTestId('status')).toHaveClass('bg-green-300'); - expect(within(sourceItems.at(1)!).getByTestId('status')).toHaveClass('bg-green-300'); + expect(within(sourceItems.at(0)!).getByTestId('status')).toHaveClass('bg-green-400'); + expect(within(sourceItems.at(1)!).getByTestId('status')).toHaveClass('bg-green-400'); expect(within(sourceItems.at(2)!).getByTestId('status')).toHaveClass('bg-red-300'); }); }); @@ -337,8 +214,8 @@ describe('SourceAndSystemFilter', () => { const sourceItems = getAllByTestId('source-item'); - expect(within(sourceItems.at(0)!).getByTestId('status')).toHaveClass('bg-green-300'); - expect(within(sourceItems.at(1)!).getByTestId('status')).toHaveClass('bg-green-300'); + expect(within(sourceItems.at(0)!).getByTestId('status')).toHaveClass('bg-green-400'); + expect(within(sourceItems.at(1)!).getByTestId('status')).toHaveClass('bg-green-400'); expect(within(sourceItems.at(2)!).getByTestId('status')).toHaveClass('bg-red-300'); }); @@ -553,80 +430,6 @@ describe('SourceAndSystemFilter', () => { }); }); - describe('clearing selections', () => { - it('should not display clear button when no selection is made', () => { - const filters: Filters = { - sources: [], - systems: [], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { queryByTestId } = render(); - - const clearButton = queryByTestId('input-clear'); - expect(clearButton).not.toBeInTheDocument(); - }); - - it('should display clear button when a selection is made', async () => { - const filters: Filters = { - sources: ['source1'], - systems: [mockSystems[0].name], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByTestId } = render(); - - const clearButton = getByTestId('input-clear'); - expect(clearButton).toHaveTextContent('Mock X component'); - }); - - it('should call `setFilters` with empty source and systems when clicking the clear button', async () => { - const filters: Filters = { - sources: ['source1'], - systems: [mockSystems[0].name], - searchTerm: '', - }; - setUseApplicationData({ filters }); - - const { getByTestId } = render(); - - await act(async () => { - const clearButton = getByTestId('input-clear'); - await userEvent.click(clearButton); - }); - - assertSetFilterUpdate(filters, { - sources: [], - systems: [], - searchTerm: '', - }); - }); - - it('should not affect search term when clicking the clear button', async () => { - const filters: Filters = { - sources: ['source1'], - systems: [mockSystems[0].name], - searchTerm: 'search term', - }; - setUseApplicationData({ filters }); - - const { getByTestId } = render(); - - await act(async () => { - const clearButton = getByTestId('input-clear'); - await userEvent.click(clearButton); - }); - - assertSetFilterUpdate(filters, { - sources: [], - systems: [], - searchTerm: 'search term', - }); - }); - }); - describe('clicking away', () => { it('should hide options when clicking away somewhere else in the document', async () => { const { container, getByRole, queryByTestId } = render(); diff --git a/packages/webui/src/components/ui/SourceAndSystemFilter.tsx b/packages/webui/src/components/ui/SourceAndSystemFilter.tsx index 2c63d88..95d1b32 100644 --- a/packages/webui/src/components/ui/SourceAndSystemFilter.tsx +++ b/packages/webui/src/components/ui/SourceAndSystemFilter.tsx @@ -1,4 +1,4 @@ -import { Check, Filter, X } from 'lucide-react'; +import { Check, Filter } from 'lucide-react'; import { Ref, RefObject, forwardRef, useRef, useState } from 'react'; import useApplication from '@/hooks/useApplication'; @@ -38,14 +38,6 @@ function SourceAndSystemFilter({ className, ...props }: SourceAndSystemFilterPro }); } - function clearSelection() { - setFilters(curr => ({ - ...curr, - sources: [], - systems: [], - })); - } - const defaultIcon = getDefaultSystem().getIconUri(); const systems = getRegisteredSystems(); const hasFilters = filters.sources.length + filters.systems.length > 0; @@ -53,114 +45,86 @@ function SourceAndSystemFilter({ className, ...props }: SourceAndSystemFilterPro const hasSources = connections.length > 0; const hasSystems = systems.length > 0; - let placeholder = hasSystems ? 'Sources & Systems' : 'Sources...'; - - const dropDownOptionsClasses = hasSystems - ? 'rounded-tr-none w-[32rem] grid-cols-[1fr_1px_1fr] gap-4 py-2 px-2' - : 'rounded-t-none w-full grid-cols-1'; - - const currentSelections = []; - if (filters.sources.length > 0) { - currentSelections.push(`${filters.sources.length} source${filters.sources.length > 1 ? 's' : ''}`); - } - if (filters.systems.length > 0) { - currentSelections.push(`${filters.systems.length} system${filters.systems.length > 1 ? 's' : ''}`); - } - - placeholder = hasFilters ? currentSelections.join(', ') : placeholder; - return ( -
+
setIsOpen(curr => !curr)} - > -
{placeholder}
- {hasFilters && ( -
- -
- )} -
+ /> {isOpen && ( -
-
-
-
- {hasSystems && ( -
- Sources -
- )} - {hasSources ? ( -
    - {connections.map(([name, isActive]) => { - const isSelected = filters.sources.includes(name); - const statusColor = isActive ? 'bg-green-300' : 'bg-red-300'; - return ( -
  • handleSourceSelection(name)} - > - - - {name} - {isSelected && } - -
  • - ); - })} -
- ) : ( - - No sources connected... - - )} +
+
+ Sources +
+ {hasSources ? ( +
    + {connections.map(([name, isActive]) => { + const isSelected = filters.sources.includes(name); + const statusColor = isActive ? 'bg-green-400' : 'bg-red-300'; + return ( +
  • handleSourceSelection(name)} + > + + + {name} + {isSelected && } + +
  • + ); + })} +
+ ) : ( + + No sources connected + + )} + {hasSystems &&
} + {hasSystems && ( +
+
+ Systems
- {hasSystems &&
} - {hasSystems && ( -
-
- Systems -
-
    - {systems.map(system => { - const isSelected = filters.systems.includes(system.name); - const icon = system.getIconUri?.() ?? defaultIcon; - return ( -
  • handleSystemSelection(system.name)} - > - - {icon && } - {system.name} - {isSelected && } - -
  • - ); - })} -
-
- )} +
    + {systems.map(system => { + const isSelected = filters.systems.includes(system.name); + const icon = system.getIconUri?.() ?? defaultIcon; + return ( +
  • handleSystemSelection(system.name)} + > + + {icon && } + {system.name} + {isSelected && } + +
  • + ); + })} +
-
+ )}
)}