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 && }
+ {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
+ }
+ }}
+ >
+ }
+ data-testid={`${testId}-trigger`}
+ >
+ Filter
+
+
{!isMobile ? (