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 && }
+
+
+ );
+ })}
+
-
+ )}
)}