Skip to content

Commit 16e77d2

Browse files
authored
Merge pull request #178 from eviltester/171-total-grid-rows-count
Add filtered visible row count to grid totals
2 parents 099215a + bd260b7 commit 16e77d2

20 files changed

Lines changed: 691 additions & 22 deletions

apps/web/src/stories/data-grid-editor.stories.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, userEvent, within } from 'storybook/test';
1+
import { expect, userEvent, waitFor, within } from 'storybook/test';
22
import { TabulatorFull as Tabulator } from 'tabulator-tables';
33
import { GenericDataTable } from '@anywaydata/core/data_formats/generic-data-table.js';
44
import { createDataGridComponent } from '../../../../packages/core-ui/js/gui_components/data-grid-editor/index.js';
@@ -31,6 +31,7 @@ function renderDataGridEditorStory(args) {
3131
});
3232

3333
root.__storybookCleanup = () => component.destroy();
34+
root.__whenReady = () => component.whenReady();
3435
return root;
3536
}
3637

@@ -41,7 +42,7 @@ const meta = {
4142
docs: {
4243
description: {
4344
component:
44-
'DataGridComponent is the Phase 7 app-side component boundary that composes the extracted GridToolbar with the shared late-mount-safe TabulatorGridAdapter. It preserves the current app DOM contract while moving the main grid onto the component model.',
45+
'DataGridComponent is the Phase 7 app-side component boundary that composes the extracted GridToolbar, the dedicated GridRowVisibilitySummary MVC control, and the shared late-mount-safe TabulatorGridAdapter. It preserves the current app DOM contract while moving the main grid and its live row-summary status onto the component model.',
4546
},
4647
},
4748
},
@@ -58,14 +59,18 @@ export const EmptyGrid = {
5859
docs: {
5960
description: {
6061
story:
61-
'Shows the componentized app-side grid editor with the real Tabulator renderer but without seeded rows. Try using Add Row to confirm the toolbar is mounted through the new component boundary and the real grid host is present.',
62+
'Shows the componentized app-side grid editor with the real Tabulator renderer but without seeded rows. Try using Add Row to confirm the toolbar, extracted row-summary status, and real grid host are mounted through the new component boundary.',
6263
},
6364
},
6465
},
6566
play: async ({ canvasElement }) => {
67+
await canvasElement.__whenReady?.();
6668
const canvas = within(canvasElement);
6769
await userEvent.click(await canvas.findByRole('button', { name: 'Add Row' }));
6870
await expect(await canvas.findByText('~rename-me', { exact: true })).toBeVisible();
71+
await waitFor(() => {
72+
expect(canvasElement.querySelector('[data-role="grid-total-rows"]')?.textContent).toMatch(/^Total rows: \d+/);
73+
});
6974
},
7075
};
7176

@@ -77,14 +82,18 @@ export const WithSampleData = {
7782
docs: {
7883
description: {
7984
story:
80-
'Shows the same component after seeding real grid data through the shared grid extension API. This is the high-fidelity story for reviewing the real Tabulator-backed app grid surface.',
85+
'Shows the same component after seeding real grid data through the shared grid extension API. This is the high-fidelity story for reviewing the real Tabulator-backed app grid surface together with its extracted row-summary control.',
8186
},
8287
},
8388
},
8489
play: async ({ canvasElement }) => {
90+
await canvasElement.__whenReady?.();
8591
const canvas = within(canvasElement);
8692
await expect(await canvas.findByText('First Name', { exact: true })).toBeVisible();
8793
await expect(await canvas.findByText('Ava', { exact: true })).toBeVisible();
8894
await expect(await canvas.findByText('Paused', { exact: true })).toBeVisible();
95+
await waitFor(() => {
96+
expect(canvasElement.querySelector('[data-role="grid-total-rows"]')?.textContent).toBe('Total rows: 2');
97+
});
8998
},
9099
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { expect, userEvent, within } from 'storybook/test';
2+
import { createGridRowVisibilitySummaryComponent } from '../../../../packages/core-ui/js/gui_components/data-grid-editor/grid-row-visibility-summary/index.js';
3+
4+
function renderGridRowVisibilitySummaryStory(args) {
5+
const root = document.createElement('section');
6+
root.style.display = 'flex';
7+
root.style.flexDirection = 'column';
8+
root.style.gap = '0.75rem';
9+
root.style.padding = '0.75rem';
10+
root.style.background = 'var(--panel-bg)';
11+
root.style.color = 'var(--page-text)';
12+
const statusRoot = document.createElement('div');
13+
const controlsRoot = document.createElement('div');
14+
controlsRoot.style.display = 'flex';
15+
controlsRoot.style.flexWrap = 'wrap';
16+
controlsRoot.style.gap = '0.5rem';
17+
controlsRoot.innerHTML = `
18+
<button type="button" data-action="show-filtered">Show Filtered Summary</button>
19+
<button type="button" data-action="clear-filtered">Clear Filtered Summary</button>
20+
`;
21+
22+
root.appendChild(statusRoot);
23+
root.appendChild(controlsRoot);
24+
25+
const component = createGridRowVisibilitySummaryComponent({
26+
root: statusRoot,
27+
documentObj: document,
28+
props: {
29+
totalRowCount: args.totalRowCount,
30+
visibleRowCount: args.visibleRowCount,
31+
hasActiveFilters: args.hasActiveFilters,
32+
},
33+
});
34+
35+
controlsRoot.querySelector('[data-action="show-filtered"]')?.addEventListener('click', () => {
36+
component.update({
37+
totalRowCount: 8,
38+
visibleRowCount: 3,
39+
hasActiveFilters: true,
40+
});
41+
});
42+
controlsRoot.querySelector('[data-action="clear-filtered"]')?.addEventListener('click', () => {
43+
component.update({
44+
totalRowCount: args.totalRowCount,
45+
visibleRowCount: args.totalRowCount,
46+
hasActiveFilters: false,
47+
});
48+
});
49+
50+
root.__storybookCleanup = () => component.destroy();
51+
return root;
52+
}
53+
54+
const meta = {
55+
title: 'Data Grid Editor/Grid Row Visibility Summary',
56+
tags: ['autodocs'],
57+
parameters: {
58+
docs: {
59+
description: {
60+
component:
61+
'GridRowVisibilitySummary is the extracted MVC status control for the Data Grid Editor row totals. It shows the always-visible total row count and conditionally adds the filtered visible count when the grid is actively filtered.',
62+
},
63+
},
64+
},
65+
args: {
66+
totalRowCount: 8,
67+
visibleRowCount: 8,
68+
hasActiveFilters: false,
69+
},
70+
render: renderGridRowVisibilitySummaryStory,
71+
};
72+
73+
export default meta;
74+
75+
export const TotalOnly = {
76+
parameters: {
77+
docs: {
78+
description: {
79+
story:
80+
'Shows the default summary state with only the total row count visible. Reviewers should see the same status text that appears beneath the full data-grid component when no filters are active.',
81+
},
82+
},
83+
},
84+
play: async ({ canvasElement }) => {
85+
const canvas = within(canvasElement);
86+
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8');
87+
},
88+
};
89+
90+
export const FilteredVisible = {
91+
args: {
92+
totalRowCount: 8,
93+
visibleRowCount: 3,
94+
hasActiveFilters: true,
95+
},
96+
parameters: {
97+
docs: {
98+
description: {
99+
story:
100+
'Shows the filtered state. Reviewers should see both the total row count and the filtered visible count, matching the app behavior after a global or column filter is applied.',
101+
},
102+
},
103+
},
104+
play: async ({ canvasElement }) => {
105+
const canvas = within(canvasElement);
106+
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8 | Filtered Visible: 3');
107+
},
108+
};
109+
110+
export const InteractiveStateChange = {
111+
parameters: {
112+
docs: {
113+
description: {
114+
story:
115+
'Demonstrates the component state transition. Try the buttons to switch between the filtered and unfiltered summaries and confirm the status text updates without the full grid story around it.',
116+
},
117+
},
118+
},
119+
play: async ({ canvasElement }) => {
120+
const canvas = within(canvasElement);
121+
await userEvent.click(canvas.getByRole('button', { name: 'Show Filtered Summary' }));
122+
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8 | Filtered Visible: 3');
123+
await userEvent.click(canvas.getByRole('button', { name: 'Clear Filtered Summary' }));
124+
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8');
125+
},
126+
};

apps/web/src/tests/browser/app/abstractions/components/grid-editor.component.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class GridEditorComponent {
1010
this.toolbar = this.container.locator('[data-role="grid-toolbar-root"]');
1111
this.errorStatus = this.container.locator('[data-role="grid-error-status"]').first();
1212
this.grid = this.container.locator('[data-role="data-grid-root"]');
13+
this.totalRows = this.container.locator('[data-role="grid-total-rows"]').first();
1314
this.contextMenu = this.page.locator('[data-role="data-grid-context-menu"]');
1415
this.renderer = new GridRendererComponent(page, this.grid);
1516
this.header = new GridHeaderComponent(page, this.grid, this.renderer);
@@ -36,6 +37,15 @@ class GridEditorComponent {
3637
await expect(this.clearFiltersButton).toBeVisible();
3738
await expect(this.clearSortButton).toBeVisible();
3839
await expect(this.resetTableButton).toBeVisible();
40+
await expect(this.totalRows).toBeVisible();
41+
}
42+
43+
async expectTotalRows(count) {
44+
await expect(this.totalRows).toHaveText(`Total rows: ${count}`);
45+
}
46+
47+
async expectFilteredVisibleRows({ totalRows, visibleRows }) {
48+
await expect(this.totalRows).toHaveText(`Total rows: ${totalRows} | Filtered Visible: ${visibleRows}`);
3949
}
4050

4151
async expectReady() {

apps/web/src/tests/browser/app/functional/filtering-sorting/column-filter.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ test.describe('3. Filtering and Sorting', () => {
55
test('Column Specific Filter', async ({ page }) => {
66
const { appPage, pageErrors } = await openApp(page);
77
const col = await seedRows(appPage, ['Open', 'Closed', 'Open']);
8+
await appPage.gridEditor.expectTotalRows(3);
89

910
await appPage.gridEditor.header.setColumnFilter(col, 'Open');
1011
await expect.poll(async () => appPage.gridEditor.header.getColumnFilterValue(col)).toBe('Open');
1112
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(2);
13+
await appPage.gridEditor.expectFilteredVisibleRows({ totalRows: 3, visibleRows: 2 });
1214

1315
await appPage.gridEditor.setQuickFilter('Open');
1416
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(2);
17+
await appPage.gridEditor.expectFilteredVisibleRows({ totalRows: 3, visibleRows: 2 });
1518

1619
expectNoPageErrors(pageErrors);
1720
});

apps/web/src/tests/browser/app/functional/filtering-sorting/global-filter.spec.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ test.describe('3. Filtering and Sorting', () => {
55
test('Global Filter', async ({ page }) => {
66
const { appPage, pageErrors } = await openApp(page);
77
await seedRows(appPage, ['Apple', 'Banana', 'Cherry']);
8+
await appPage.gridEditor.expectTotalRows(3);
89

910
await appPage.gridEditor.setQuickFilter('App');
1011
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(1);
12+
await appPage.gridEditor.expectFilteredVisibleRows({ totalRows: 3, visibleRows: 1 });
1113
await appPage.gridEditor.setQuickFilter('app');
1214
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(1);
15+
await appPage.gridEditor.expectFilteredVisibleRows({ totalRows: 3, visibleRows: 1 });
1316

1417
await appPage.gridEditor.setQuickFilter('');
1518
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(3);
19+
await appPage.gridEditor.expectTotalRows(3);
1620

1721
expectNoPageErrors(pageErrors);
1822
});

apps/web/src/tests/browser/app/functional/grid-editor/row-single-select-operations/add-single-row.spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ test.describe('1. Grid Basic Operations', () => {
77

88
await appPage.gridEditor.expectVisible();
99
const before = await appPage.gridEditor.renderer.countRows();
10+
await appPage.gridEditor.expectTotalRows(before);
1011
await appPage.gridEditor.addRow();
1112
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(before + 1);
13+
await appPage.gridEditor.expectTotalRows(before + 1);
1214

1315
const [columnName] = await appPage.gridEditor.header.getColumnNames();
1416
await appPage.gridEditor.renderer.setCellTextByColumnName(columnName, before, 'Test Data');

apps/web/src/tests/browser/app/functional/import-export/set-grid-from-text.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ test.describe('4. Import Export Basic', () => {
1212

1313
await appPage.gridEditor.resetTable();
1414
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(0);
15+
await appPage.gridEditor.expectTotalRows(0);
1516

1617
await appPage.textPreviewEditor.selectFormat('CSV');
1718
await ensureTextEditMode(appPage);
@@ -21,12 +22,14 @@ test.describe('4. Import Export Basic', () => {
2122
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(2);
2223
await expect.poll(async () => appPage.gridEditor.header.countColumns()).toBe(2);
2324
await expect.poll(async () => appPage.gridEditor.renderer.getCellTextByColumnName('Name', 0)).toBe('A');
25+
await appPage.gridEditor.expectTotalRows(2);
2426

2527
// Product decision: malformed CSV may clear/reset grid state for faster import handling.
2628
// We assert that behavior explicitly instead of requiring state preservation.
2729
await appPage.textPreviewEditor.setOutputText('"bad csv');
2830
await appPage.importExportWorkspace.setGridFromText();
2931
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(0);
32+
await appPage.gridEditor.expectTotalRows(0);
3033

3134
expectNoPageErrors(pageErrors);
3235
});

apps/web/src/tests/browser/app/functional/test-data/basic-generation.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ test.describe('7. Test Data Generation', () => {
2121
await appPage.testDataPanel.clickGenerate();
2222
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(5);
2323
await expect.poll(async () => appPage.gridEditor.header.getColumnNames()).toContain('First Name');
24+
await appPage.gridEditor.expectTotalRows(5);
2425

2526
const values = await appPage.gridEditor.renderer.getColumnTextsByName('First Name');
2627
expect(values).toHaveLength(5);

apps/web/styles.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ body.theme-dark a:hover {
8080
gap: 1rem;
8181
}
8282

83+
.data-grid-total-rows {
84+
color: var(--page-text);
85+
background: transparent;
86+
font-size: 1.05rem;
87+
line-height: 1.4;
88+
}
89+
8390
/* was #3D5249 - then docusaurus uses #2e8555*/
8491
.header {
8592
background-color: var(--header-bg);

docs/frontend-component-migration-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ Current status:
565565
- The post-`SchemaPanel` re-audit found no urgent missing primary stories for schema, page shells, instructions, app data population, generator controls, generator preview, data grid editor, import/export toolbar, text preview editor, format selector, or format options. The next useful Storybook-driven splits are smaller visible sub-surfaces: the import/export options-preview split layout and the generator output-format selector.
566566
- The import/export options-preview shell is now a focused app-side component with its own Storybook docs and direct tests. `ImportExportWorkspaceView` no longer owns the splitter drag/keyboard/clamping behavior itself; it now composes the dedicated split-layout boundary through `TextPreviewEditor`.
567567
- `GeneratorControls` now composes a dedicated `GeneratorOutputFormatSelector` component, and Storybook documents that selector directly through `generator-output-format-selector.stories.js` instead of only through the larger controls surface.
568+
- `DataGridComponent` now composes a dedicated `GridRowVisibilitySummary` MVC control, and Storybook documents that status surface directly through `grid-row-visibility-summary.stories.js` instead of only through the full data-grid story.
568569
- The `TextPreviewEditor` re-audit showed that its Preview/Edit controls were still a meaningful visible sub-surface. That cluster is now a dedicated `TextPreviewToolbar` component with its own Storybook docs and focused tests, while `TextPreviewEditor` keeps the textarea and split-layout shell composition.
569570
- `ImportExportWorkspace` now shows a dedicated grid/preview sync row above a closed-by-default native `Import / Export` details section, leaving Auto Sync, Preview/Edit, row count, format tabs, and text preview outside the collapsible import/export toolbar area.
570571
- App download controls and generator controls now also expose reviewer-facing file export encoding settings through a small settings disclosure, with Storybook examples covering default OS-derived line endings and explicit Windows CR/LF plus BOM output.

0 commit comments

Comments
 (0)