Skip to content

Commit f1c8542

Browse files
rsbhclaude
andcommitted
fix: DataTable reset and zero state handling (#666)
* fix: reset display settings to default sort and group in DataTable Memoize defaultTableQuery and explicitly reset sort/group_by in onDisplaySettingsReset so reset always restores defaultSort and no grouping, regardless of initial query prop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: show empty state instead of zero state on sort/group change Add hasActiveQuery util to check for active filters, search, or non-default sort/grouping. Use it in content and virtualized-content to correctly distinguish zero state from empty state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove duplicate comment in hasActiveQuery Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c93a5bb commit f1c8542

5 files changed

Lines changed: 255 additions & 59 deletions

File tree

packages/raystack/components/data-table/__tests__/data-table.test.tsx

Lines changed: 174 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { render, screen } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
3-
import { describe, expect, it, vi } from 'vitest';
3+
import { beforeAll, describe, expect, it, vi } from 'vitest';
44
import { DataTable } from '../data-table';
55
import styles from '../data-table.module.css';
66
import { DataTableColumnDef } from '../data-table.types';
77

8+
beforeAll(() => {
9+
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
10+
observe: vi.fn(),
11+
unobserve: vi.fn(),
12+
disconnect: vi.fn()
13+
}));
14+
});
15+
816
interface TestData {
917
id: number;
1018
name: string;
@@ -43,7 +51,11 @@ describe('DataTable', () => {
4351
describe('Basic Rendering', () => {
4452
it('renders data table with content', () => {
4553
render(
46-
<DataTable data={mockData} columns={mockColumns}>
54+
<DataTable
55+
data={mockData}
56+
columns={mockColumns}
57+
defaultSort={{ name: 'name', order: 'asc' }}
58+
>
4759
<DataTable.Content />
4860
</DataTable>
4961
);
@@ -58,7 +70,11 @@ describe('DataTable', () => {
5870
};
5971

6072
render(
61-
<DataTable data={mockData} columns={mockColumns}>
73+
<DataTable
74+
data={mockData}
75+
columns={mockColumns}
76+
defaultSort={{ name: 'name', order: 'asc' }}
77+
>
6278
<TestComponent />
6379
</DataTable>
6480
);
@@ -68,7 +84,11 @@ describe('DataTable', () => {
6884

6985
it('renders with empty data', () => {
7086
render(
71-
<DataTable data={[]} columns={mockColumns}>
87+
<DataTable
88+
data={[]}
89+
columns={mockColumns}
90+
defaultSort={{ name: 'name', order: 'asc' }}
91+
>
7292
<DataTable.Content />
7393
</DataTable>
7494
);
@@ -80,7 +100,11 @@ describe('DataTable', () => {
80100
describe('Data Display', () => {
81101
it('displays table data in content', () => {
82102
render(
83-
<DataTable data={mockData} columns={mockColumns}>
103+
<DataTable
104+
data={mockData}
105+
columns={mockColumns}
106+
defaultSort={{ name: 'name', order: 'asc' }}
107+
>
84108
<DataTable.Content />
85109
</DataTable>
86110
);
@@ -94,7 +118,11 @@ describe('DataTable', () => {
94118

95119
it('displays column headers', () => {
96120
render(
97-
<DataTable data={mockData} columns={mockColumns}>
121+
<DataTable
122+
data={mockData}
123+
columns={mockColumns}
124+
defaultSort={{ name: 'name', order: 'asc' }}
125+
>
98126
<DataTable.Content />
99127
</DataTable>
100128
);
@@ -114,6 +142,7 @@ describe('DataTable', () => {
114142
<DataTable
115143
data={mockData}
116144
columns={mockColumns}
145+
defaultSort={{ name: 'name', order: 'asc' }}
117146
onRowClick={onRowClick}
118147
>
119148
<DataTable.Content />
@@ -129,7 +158,11 @@ describe('DataTable', () => {
129158
describe('Component Composition', () => {
130159
it('renders with toolbar', () => {
131160
const { container } = render(
132-
<DataTable data={mockData} columns={mockColumns}>
161+
<DataTable
162+
data={mockData}
163+
columns={mockColumns}
164+
defaultSort={{ name: 'name', order: 'asc' }}
165+
>
133166
<DataTable.Toolbar />
134167
<DataTable.Content />
135168
</DataTable>
@@ -142,7 +175,11 @@ describe('DataTable', () => {
142175

143176
it('renders with search', () => {
144177
render(
145-
<DataTable data={mockData} columns={mockColumns}>
178+
<DataTable
179+
data={mockData}
180+
columns={mockColumns}
181+
defaultSort={{ name: 'name', order: 'asc' }}
182+
>
146183
<DataTable.Search />
147184
<DataTable.Content />
148185
</DataTable>
@@ -409,4 +446,133 @@ describe('DataTable', () => {
409446
expect(screen.getByText('John Doe')).toBeInTheDocument();
410447
});
411448
});
449+
450+
describe('Display Settings Reset', () => {
451+
const columnsWithSortAndGroup: DataTableColumnDef<TestData, unknown>[] = [
452+
{
453+
id: 'name',
454+
accessorKey: 'name',
455+
header: 'Name',
456+
cell: ({ getValue }) => getValue(),
457+
enableSorting: true,
458+
enableGrouping: true
459+
},
460+
{
461+
id: 'email',
462+
accessorKey: 'email',
463+
header: 'Email',
464+
cell: ({ getValue }) => getValue(),
465+
enableSorting: true
466+
},
467+
{
468+
id: 'status',
469+
accessorKey: 'status',
470+
header: 'Status',
471+
cell: ({ getValue }) => getValue(),
472+
enableSorting: true,
473+
enableGrouping: true
474+
}
475+
];
476+
477+
it('resets sort and group to defaults on reset click', async () => {
478+
const onTableQueryChange = vi.fn();
479+
const user = userEvent.setup();
480+
481+
render(
482+
<DataTable
483+
data={mockData}
484+
columns={columnsWithSortAndGroup}
485+
defaultSort={{ name: 'name', order: 'asc' }}
486+
mode='server'
487+
onTableQueryChange={onTableQueryChange}
488+
query={{
489+
sort: [{ name: 'email', order: 'desc' }],
490+
group_by: ['status']
491+
}}
492+
>
493+
<DataTable.Toolbar />
494+
<DataTable.Content />
495+
</DataTable>
496+
);
497+
498+
// Open Display popover and click reset
499+
await user.click(screen.getByText('Display'));
500+
await user.click(screen.getByText('Reset to default'));
501+
502+
// Verify onTableQueryChange was called with default sort and no group
503+
expect(onTableQueryChange).toHaveBeenLastCalledWith(
504+
expect.objectContaining({
505+
sort: [{ name: 'name', order: 'asc' }],
506+
group_by: []
507+
})
508+
);
509+
});
510+
511+
it('does not show zero state when sort or group changes in client mode', () => {
512+
render(
513+
<DataTable
514+
data={mockData}
515+
columns={columnsWithSortAndGroup}
516+
defaultSort={{ name: 'name', order: 'asc' }}
517+
mode='client'
518+
query={{
519+
sort: [{ name: 'email', order: 'desc' }],
520+
group_by: ['status']
521+
}}
522+
>
523+
<DataTable.Content
524+
zeroState={<div data-testid='zero-state'>No data</div>}
525+
emptyState={<div data-testid='empty-state'>No results</div>}
526+
/>
527+
</DataTable>
528+
);
529+
530+
// Data should still be visible, not zero/empty state
531+
expect(screen.getByText('John Doe')).toBeInTheDocument();
532+
expect(screen.queryByTestId('zero-state')).not.toBeInTheDocument();
533+
expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
534+
});
535+
536+
it('shows empty state when sort is changed and no data', () => {
537+
render(
538+
<DataTable
539+
data={[]}
540+
columns={columnsWithSortAndGroup}
541+
defaultSort={{ name: 'name', order: 'asc' }}
542+
query={{
543+
sort: [{ name: 'email', order: 'desc' }]
544+
}}
545+
>
546+
<DataTable.Content
547+
zeroState={<div data-testid='zero-state'>No data</div>}
548+
emptyState={<div data-testid='empty-state'>No results</div>}
549+
/>
550+
</DataTable>
551+
);
552+
553+
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
554+
expect(screen.queryByTestId('zero-state')).not.toBeInTheDocument();
555+
});
556+
557+
it('shows empty state when group is changed and no data', () => {
558+
render(
559+
<DataTable
560+
data={[]}
561+
columns={columnsWithSortAndGroup}
562+
defaultSort={{ name: 'name', order: 'asc' }}
563+
query={{
564+
group_by: ['status']
565+
}}
566+
>
567+
<DataTable.Content
568+
zeroState={<div data-testid='zero-state'>No data</div>}
569+
emptyState={<div data-testid='empty-state'>No results</div>}
570+
/>
571+
</DataTable>
572+
);
573+
574+
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
575+
expect(screen.queryByTestId('zero-state')).not.toBeInTheDocument();
576+
});
577+
});
412578
});

packages/raystack/components/data-table/components/content.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
GroupedData
1919
} from '../data-table.types';
2020
import { useDataTable } from '../hooks/useDataTable';
21+
import { hasActiveQuery } from '../utils';
2122

2223
function Headers<TData>({
2324
headerGroups = [],
@@ -172,7 +173,8 @@ export function Content({
172173
isLoading,
173174
loadMoreData,
174175
loadingRowCount = 3,
175-
tableQuery
176+
tableQuery,
177+
defaultSort
176178
} = useDataTable();
177179

178180
const headerGroups = table?.getHeaderGroups();
@@ -219,12 +221,10 @@ export function Content({
219221

220222
const hasData = rows?.length > 0 || isLoading;
221223

222-
const hasFiltersOrSearch =
223-
(tableQuery?.filters && tableQuery.filters.length > 0) ||
224-
Boolean(tableQuery?.search && tableQuery.search.trim() !== '');
224+
const hasChanges = hasActiveQuery(tableQuery || {}, defaultSort);
225225

226-
const isZeroState = !hasData && !hasFiltersOrSearch;
227-
const isEmptyState = !hasData && hasFiltersOrSearch;
226+
const isZeroState = !hasData && !hasChanges;
227+
const isEmptyState = !hasData && hasChanges;
228228

229229
const stateToShow: React.ReactNode = isZeroState
230230
? (zeroState ?? emptyState ?? <DefaultEmptyComponent />)

packages/raystack/components/data-table/components/virtualized-content.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
VirtualizedContentProps
1919
} from '../data-table.types';
2020
import { useDataTable } from '../hooks/useDataTable';
21+
import { hasActiveQuery } from '../utils';
2122

2223
function VirtualHeaders<TData>({
2324
headerGroups = [],
@@ -220,6 +221,7 @@ export function VirtualizedContent({
220221
isLoading,
221222
loadMoreData,
222223
tableQuery,
224+
defaultSort,
223225
loadingRowCount = 3
224226
} = useDataTable();
225227

@@ -255,12 +257,10 @@ export function VirtualizedContent({
255257

256258
const hasData = rows?.length > 0 || isLoading;
257259

258-
const hasFiltersOrSearch =
259-
(tableQuery?.filters && tableQuery.filters.length > 0) ||
260-
Boolean(tableQuery?.search && tableQuery.search.trim() !== '');
260+
const hasChanges = hasActiveQuery(tableQuery || {}, defaultSort);
261261

262-
const isZeroState = !hasData && !hasFiltersOrSearch;
263-
const isEmptyState = !hasData && hasFiltersOrSearch;
262+
const isZeroState = !hasData && !hasChanges;
263+
const isEmptyState = !hasData && hasChanges;
264264

265265
const stateToShow: React.ReactNode = isZeroState
266266
? (zeroState ?? emptyState ?? <DefaultEmptyComponent />)

0 commit comments

Comments
 (0)