diff --git a/apps/www/src/content/docs/dataview/props.ts b/apps/www/src/content/docs/dataview/props.ts index ffbed5a8b..1d216b646 100644 --- a/apps/www/src/content/docs/dataview/props.ts +++ b/apps/www/src/content/docs/dataview/props.ts @@ -92,7 +92,7 @@ export interface DataViewField { /** Filter input type. */ filterType?: 'string' | 'number' | 'date' | 'select' | 'multiselect'; - /** Options when filterType is select/multiselect. */ + /** Options when filterType is select/multiselect. The add-filter menu leaves out the field while this is empty, and the trigger is disabled if no other field is available. */ filterOptions?: Array<{ label: string; value: string }>; /** Allow sorting. */ diff --git a/packages/react/components/data-view/__tests__/data-view.test.tsx b/packages/react/components/data-view/__tests__/data-view.test.tsx index 09ab62807..577555369 100644 --- a/packages/react/components/data-view/__tests__/data-view.test.tsx +++ b/packages/react/components/data-view/__tests__/data-view.test.tsx @@ -372,6 +372,72 @@ describe('DataView', () => { ).toBeInTheDocument(); }); + it('does not offer select or multiselect fields without filterOptions', async () => { + const user = userEvent.setup(); + const fields: DataViewField[] = [ + { accessorKey: 'name', label: 'Name', filterable: true }, + { + accessorKey: 'status', + label: 'Status', + filterable: true, + filterType: 'select', + filterOptions: [{ label: 'Active', value: 'string' }] + }, + { + accessorKey: 'email', + label: 'Email', + filterable: true, + filterType: 'select', + filterOptions: [] + }, + { + accessorKey: 'id', + label: 'Id', + filterable: true, + filterType: 'multiselect' + } + ]; + render( + + ( + + )} + /> + + ); + const trigger = screen.getByRole('button'); + expect(trigger).toHaveTextContent(/^Name,Status$/); + await user.click(trigger); + await screen.findByRole('menuitem', { name: 'Status' }); + expect(screen.getAllByRole('menuitem').map(i => i.textContent)).toEqual([ + 'Name', + 'Status' + ]); + }); + + it('disables the filter trigger while select options are loading', () => { + render( + + + + ); + expect(screen.getByRole('button', { name: /filter/i })).toBeDisabled(); + }); + it('search input updates the query', async () => { const user = userEvent.setup(); render( diff --git a/packages/react/components/data-view/components/filters.tsx b/packages/react/components/data-view/components/filters.tsx index fa6a3002a..4a2aa75a7 100644 --- a/packages/react/components/data-view/components/filters.tsx +++ b/packages/react/components/data-view/components/filters.tsx @@ -41,9 +41,15 @@ function AddFilter({ className, ...positionerProps }: AddFilterProps) { - const availableFilters = fieldList?.filter( + const unappliedFields = fieldList.filter( f => !appliedFiltersSet.has(f.accessorKey) ); + const availableFilters = unappliedFields.filter( + f => + f.filterOptions?.length || + (f.filterType !== FilterType.select && + f.filterType !== FilterType.multiselect) + ); const trigger = useMemo(() => { if (typeof children === 'function') @@ -75,9 +81,10 @@ function AddFilter({ ); }, [children, appliedFiltersSet, availableFilters, className]); - return availableFilters.length > 0 ? ( + return unappliedFields.length > 0 ? ( {trigger}} />