diff --git a/src/__testing__/DataTableToolbar.test.tsx b/src/__testing__/DataTableToolbar.test.tsx new file mode 100644 index 000000000..7893344cc --- /dev/null +++ b/src/__testing__/DataTableToolbar.test.tsx @@ -0,0 +1,86 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { DataTableToolbar } from '../custom/DataTableToolbar'; +import { SistentThemeProvider } from '../theme'; + +const renderWithTheme = (ui: React.ReactElement) => + render({ui}); + +describe('DataTableToolbar', () => { + it('renders primaryActions content', () => { + renderWithTheme(Add} />); + expect(screen.getByRole('button', { name: 'Add' })).toBeTruthy(); + }); + + it('renders secondaryActions content', () => { + renderWithTheme(Export} />); + expect(screen.getByRole('button', { name: 'Export' })).toBeTruthy(); + }); + + it('renders search slot', () => { + renderWithTheme(} />); + expect(screen.getByPlaceholderText('Search')).toBeTruthy(); + }); + + it('renders filter slot', () => { + renderWithTheme(Filter} />); + expect(screen.getByText('Filter')).toBeTruthy(); + }); + + it('renders columnVisibility slot', () => { + renderWithTheme(Columns} />); + expect(screen.getByText('Columns')).toBeTruthy(); + }); + + it('renders viewSwitch slot', () => { + renderWithTheme(Grid/Table} />); + expect(screen.getByText('Grid/Table')).toBeTruthy(); + }); + + it('renders bulkOperations content in right section', () => { + renderWithTheme(Select All} />); + expect(screen.getByRole('button', { name: 'Select All' })).toBeTruthy(); + }); + + it('renders all slots simultaneously with bulkOperations', () => { + renderWithTheme( + Add} + secondaryActions={} + bulkOperations={} + search={} + filter={
Filter
} + /> + ); + expect(screen.getByRole('button', { name: 'Add' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Export' })).toBeTruthy(); + expect(screen.getByRole('button', { name: 'Select All' })).toBeTruthy(); + expect(screen.getByPlaceholderText('Search')).toBeTruthy(); + expect(screen.getByText('Filter')).toBeTruthy(); + }); + + it('renders search before filter in DOM order', () => { + const { container } = renderWithTheme( + Search} + filter={Filter} + /> + ); + const children = container.querySelectorAll('[data-testid="search"], [data-testid="filter"]'); + expect(children.length).toBe(2); + expect(children[0].getAttribute('data-testid')).toBe('search'); + expect(children[1].getAttribute('data-testid')).toBe('filter'); + }); + + it('renders without any props (empty state)', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeTruthy(); + }); + + it('applies custom sx styles', () => { + const { container } = renderWithTheme(); + const root = container.firstChild as HTMLElement; + expect(root).toHaveStyle('margin-top: 32px'); + }); +}); diff --git a/src/custom/DataTableToolbar/DataTableToolbar.tsx b/src/custom/DataTableToolbar/DataTableToolbar.tsx new file mode 100644 index 000000000..4a73b3558 --- /dev/null +++ b/src/custom/DataTableToolbar/DataTableToolbar.tsx @@ -0,0 +1,64 @@ +import { Box } from '../../base'; +import { styled } from '../../theme'; +import type { DataTableToolbarProps } from './DataTableToolbar.types'; + +const ToolbarRoot = styled(Box)(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing(4), + minHeight: theme.spacing(8), + padding: theme.spacing(1.5), + backgroundColor: theme.palette.background.card, + borderRadius: theme.spacing(1), + boxShadow: theme.shadows[2], + + [theme.breakpoints.down('sm')]: { + height: 'auto', + flexWrap: 'wrap', + padding: theme.spacing(1), + gap: theme.spacing(1) + } +})); + +const Section = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1) +})); + +export function DataTableToolbar({ + primaryActions, + secondaryActions, + bulkOperations, + search, + filter, + columnVisibility, + viewSwitch, + sx +}: DataTableToolbarProps): JSX.Element { + const hasLeftContent = Boolean(primaryActions); + const hasRightContent = + Boolean(bulkOperations) || + Boolean(secondaryActions) || + Boolean(filter) || + Boolean(search) || + Boolean(columnVisibility) || + Boolean(viewSwitch); + + return ( + + {hasLeftContent &&
{primaryActions}
} + {hasRightContent && ( +
+ {bulkOperations} + {secondaryActions} + {search} + {filter} + {columnVisibility} + {viewSwitch} +
+ )} +
+ ); +} diff --git a/src/custom/DataTableToolbar/DataTableToolbar.types.ts b/src/custom/DataTableToolbar/DataTableToolbar.types.ts new file mode 100644 index 000000000..54d6b0ed6 --- /dev/null +++ b/src/custom/DataTableToolbar/DataTableToolbar.types.ts @@ -0,0 +1,27 @@ +import type { SxProps, Theme } from '@mui/material'; + +export interface DataTableToolbarProps { + /** Left side: primary action buttons (Add, Create, Import) */ + primaryActions?: React.ReactNode; + + /** Left side next to primary: secondary actions (Export, bulk delete) */ + secondaryActions?: React.ReactNode; + + /** Right side: bulk action controls (select all, batch delete) */ + bulkOperations?: React.ReactNode; + + /** Right side: SearchBar component */ + search?: React.ReactNode; + + /** Right side: UniversalFilter component */ + filter?: React.ReactNode; + + /** Right side: Column visibility control */ + columnVisibility?: React.ReactNode; + + /** Right side: Grid/table view toggle */ + viewSwitch?: React.ReactNode; + + /** Custom styles for migration compatibility */ + sx?: SxProps; +} diff --git a/src/custom/DataTableToolbar/index.tsx b/src/custom/DataTableToolbar/index.tsx new file mode 100644 index 000000000..47a241fc8 --- /dev/null +++ b/src/custom/DataTableToolbar/index.tsx @@ -0,0 +1,2 @@ +export { DataTableToolbar } from './DataTableToolbar'; +export type { DataTableToolbarProps } from './DataTableToolbar.types'; diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index 9cfb6f0f7..9678cf7d4 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -12,7 +12,7 @@ import { TooltipIcon } from './TooltipIconButton'; export const IconWrapper = styled('div', { shouldForwardProp: (prop) => prop !== 'disabled' })<{ disabled?: boolean }>(({ disabled = false }) => ({ - width: '100%', + width: 'auto', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? '0.5' : '1', display: 'flex', diff --git a/src/custom/SearchBar.tsx b/src/custom/SearchBar.tsx index 8558b2725..099a0d49c 100644 --- a/src/custom/SearchBar.tsx +++ b/src/custom/SearchBar.tsx @@ -153,7 +153,7 @@ function SearchBar({ placeholder={placeholder} data-testid="searchbar-input" style={{ - width: expanded ? '150px' : '0', + width: expanded ? '15rem' : '0', opacity: expanded ? 1 : 0, transition: 'width 0.3s ease, opacity 0.3s ease' }} diff --git a/src/custom/UniversalFilter.tsx b/src/custom/UniversalFilter.tsx index a34f70a11..a7a1bcb54 100644 --- a/src/custom/UniversalFilter.tsx +++ b/src/custom/UniversalFilter.tsx @@ -2,7 +2,7 @@ import { Drawer, styled, useMediaQuery } from '@mui/material'; import { SelectChangeEvent } from '@mui/material/Select'; import { subDays, subMonths, subYears } from 'date-fns'; import React from 'react'; -import { Button } from '../base/Button'; +import { Badge, Button } from '../base'; import { ClickAwayListener } from '../base/ClickAwayListener'; import { DateTimePicker } from '../base/DateTimePicker'; import { InputLabel } from '../base/InputLabel'; @@ -12,7 +12,6 @@ import { Select } from '../base/Select'; import { FilterIcon } from '../icons'; import { useTheme } from '../theme'; import PopperListener from './PopperListener'; -import { TooltipIcon } from './TooltipIconButton'; export interface FilterColumn { name: string; @@ -260,15 +259,35 @@ function UniversalFilter({ ); + const activeFilterCount = Object.values(selectedFilters).filter( + (value) => value && value !== 'All' + ).length; + return ( <>
- } - arrow - /> + t.typography.caption.fontSize, + height: 20, + minWidth: 20 + } + }} + > + + {!isMobile ? (