Skip to content

Commit bd260b7

Browse files
committed
Add grid row visibility summary component
1 parent ac84af3 commit bd260b7

12 files changed

Lines changed: 417 additions & 17 deletions

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const meta = {
4242
docs: {
4343
description: {
4444
component:
45-
'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, including the live total-row status underneath the grid.',
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.',
4646
},
4747
},
4848
},
@@ -59,7 +59,7 @@ export const EmptyGrid = {
5959
docs: {
6060
description: {
6161
story:
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 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.',
6363
},
6464
},
6565
},
@@ -82,7 +82,7 @@ export const WithSampleData = {
8282
docs: {
8383
description: {
8484
story:
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.',
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.',
8686
},
8787
},
8888
},
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/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.

packages/core-ui/js/gui_components/data-grid-editor/data-grid-component-view.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createTextInputDialogService } from '../shared/dialog-services/text-input-dialog-service.js';
22
import { escapeHtml } from '../shared/html-escape.js';
33
import { createGridToolbarComponent } from './grid-toolbar/index.js';
4+
import { createGridRowVisibilitySummaryComponent } from './grid-row-visibility-summary/index.js';
45
import { showGridError } from './grid-error-surface.js';
56
import { GuardedColumnEdits } from './shared/guarded-column-edits.js';
67
import { renderColumnHeaderActionButtonsHtml } from './shared/column-header-action-buttons.js';
@@ -142,6 +143,7 @@ class DataGridComponentView {
142143
this.gridReadyPromise = Promise.resolve(null);
143144
this.textInputDialogService = null;
144145
this.unsubscribeGridChanged = null;
146+
this.gridRowVisibilitySummary = null;
145147
this.totalRowsFrame = null;
146148
this.contextMenuState = { open: false, x: 0, y: 0 };
147149
this.handleGridContextMenu = (event) => this.onGridContextMenu(event);
@@ -173,12 +175,7 @@ class DataGridComponentView {
173175
role="status"
174176
></div>
175177
<div id="myGrid" style="height: 500px; width:100%;" class="ag-theme-alpine" data-role="data-grid-root"></div>
176-
<div
177-
data-role="grid-total-rows"
178-
class="data-grid-total-rows"
179-
aria-live="polite"
180-
role="status"
181-
>Total rows: 0</div>
178+
<div data-role="grid-row-visibility-summary-root"></div>
182179
<div
183180
class="data-grid-context-menu"
184181
data-role="data-grid-context-menu"
@@ -222,6 +219,16 @@ class DataGridComponentView {
222219
onUniqueColumnNamesChange: (uniqueColumnNames) => this.controller.setUniqueColumnNames(uniqueColumnNames),
223220
},
224221
});
222+
const gridRowVisibilitySummaryRoot = this.root.querySelector('[data-role="grid-row-visibility-summary-root"]');
223+
this.gridRowVisibilitySummary = createGridRowVisibilitySummaryComponent({
224+
root: gridRowVisibilitySummaryRoot,
225+
documentObj: this.documentObj,
226+
props: {
227+
totalRowCount: 0,
228+
visibleRowCount: 0,
229+
hasActiveFilters: false,
230+
},
231+
});
225232

226233
this.gridAdapter = createTabulatorGridAdapter({
227234
rootElement: gridRoot,
@@ -275,8 +282,7 @@ class DataGridComponentView {
275282
}
276283

277284
syncTotalRows() {
278-
const totalRowsElement = this.root?.querySelector?.('[data-role="grid-total-rows"]');
279-
if (!totalRowsElement) {
285+
if (!this.gridRowVisibilitySummary) {
280286
return;
281287
}
282288

@@ -287,9 +293,11 @@ class DataGridComponentView {
287293
const normalizedTotalRows = Number.isFinite(totalRows) ? totalRows : 0;
288294
const normalizedVisibleRowCount = Number.isFinite(visibleRowCount) ? visibleRowCount : 0;
289295

290-
totalRowsElement.textContent = hasActiveFilters
291-
? `Total rows: ${normalizedTotalRows} | Filtered Visible: ${normalizedVisibleRowCount}`
292-
: `Total rows: ${normalizedTotalRows}`;
296+
this.gridRowVisibilitySummary.update({
297+
totalRowCount: normalizedTotalRows,
298+
visibleRowCount: normalizedVisibleRowCount,
299+
hasActiveFilters,
300+
});
293301
}
294302

295303
getGridAdapter() {
@@ -469,6 +477,8 @@ class DataGridComponentView {
469477
}
470478
this.unsubscribeGridChanged?.();
471479
this.unsubscribeGridChanged = null;
480+
this.gridRowVisibilitySummary?.destroy?.();
481+
this.gridRowVisibilitySummary = null;
472482
this.toolbar?.destroy?.();
473483
this.toolbar = null;
474484
this.gridAdapter?.destroy?.();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
function normalizeCount(value) {
2+
const parsed = Number.parseInt(value, 10);
3+
if (!Number.isFinite(parsed)) {
4+
return 0;
5+
}
6+
return Math.max(0, parsed);
7+
}
8+
9+
function normalizeProps(props = {}) {
10+
return {
11+
totalRowCount: normalizeCount(props.totalRowCount),
12+
visibleRowCount: normalizeCount(props.visibleRowCount),
13+
hasActiveFilters: props.hasActiveFilters === true,
14+
className: props.className || 'data-grid-total-rows',
15+
totalLabel: props.totalLabel || 'Total rows',
16+
filteredLabel: props.filteredLabel || 'Filtered Visible',
17+
roleName: props.roleName || 'grid-total-rows',
18+
};
19+
}
20+
21+
class GridRowVisibilitySummaryController {
22+
constructor({ props = {} } = {}) {
23+
this.state = normalizeProps(props);
24+
}
25+
26+
updateProps(nextProps = {}) {
27+
this.state = normalizeProps({ ...this.state, ...nextProps });
28+
}
29+
30+
getState() {
31+
return { ...this.state };
32+
}
33+
34+
getDisplayText() {
35+
const state = this.getState();
36+
const totalText = `${state.totalLabel}: ${state.totalRowCount}`;
37+
if (!state.hasActiveFilters) {
38+
return totalText;
39+
}
40+
return `${totalText} | ${state.filteredLabel}: ${state.visibleRowCount}`;
41+
}
42+
}
43+
44+
export { GridRowVisibilitySummaryController };
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
function getDefaultDocumentObj() {
2+
return typeof document !== 'undefined' ? document : null;
3+
}
4+
5+
class GridRowVisibilitySummaryView {
6+
constructor({ root, controller, documentObj = getDefaultDocumentObj() } = {}) {
7+
this.root = root;
8+
this.controller = controller;
9+
this.documentObj = documentObj;
10+
}
11+
12+
mount() {
13+
if (!this.root) {
14+
throw new Error('GridRowVisibilitySummaryView requires a root element');
15+
}
16+
this.render();
17+
}
18+
19+
render() {
20+
const state = this.controller.getState();
21+
this.root.className = state.className;
22+
this.root.setAttribute('data-role', state.roleName);
23+
this.root.setAttribute('aria-live', 'polite');
24+
this.root.setAttribute('role', 'status');
25+
this.root.textContent = this.controller.getDisplayText();
26+
}
27+
28+
destroy() {
29+
this.root.className = '';
30+
this.root.removeAttribute('aria-live');
31+
this.root.removeAttribute('role');
32+
this.root.removeAttribute('data-role');
33+
this.root.textContent = '';
34+
}
35+
}
36+
37+
export { GridRowVisibilitySummaryView };
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { GridRowVisibilitySummaryController } from './grid-row-visibility-summary-controller.js';
2+
import { GridRowVisibilitySummaryView } from './grid-row-visibility-summary-view.js';
3+
4+
function createGridRowVisibilitySummaryComponent({ root, props = {}, documentObj } = {}) {
5+
const controller = new GridRowVisibilitySummaryController({ props });
6+
const view = new GridRowVisibilitySummaryView({ root, controller, documentObj });
7+
view.mount();
8+
9+
return {
10+
update(nextProps) {
11+
controller.updateProps(nextProps);
12+
view.render();
13+
},
14+
destroy() {
15+
view.destroy();
16+
},
17+
getState() {
18+
return controller.getState();
19+
},
20+
getDisplayText() {
21+
return controller.getDisplayText();
22+
},
23+
};
24+
}
25+
26+
export { createGridRowVisibilitySummaryComponent };

packages/core-ui/js/gui_components/data-grid-editor/tabulator/tabulator-helpers.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@ class TabulatorHelper {
4747

4848
addRowToBottom(rowToAdd) {
4949
// add row to bottom of table - false for bottom, true for top
50-
this.addRow(rowToAdd, false);
50+
return this.addRow(rowToAdd, false);
5151
}
5252

5353
addRowToTop(rowToAdd) {
5454
// add row to top of table - false for bottom, true for top
55-
this.addRow(rowToAdd, true);
55+
return this.addRow(rowToAdd, true);
5656
}
5757

5858
addRow(rowToAdd, addToTop) {
5959
// add row to bottom of table - false for bottom, true for top
60-
this.tabulator.addData([rowToAdd], addToTop);
60+
return this.tabulator.addData([rowToAdd], addToTop);
6161
}
6262
}
6363

0 commit comments

Comments
 (0)