Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions apps/web/src/stories/data-grid-editor.stories.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, userEvent, within } from 'storybook/test';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import { GenericDataTable } from '@anywaydata/core/data_formats/generic-data-table.js';
import { createDataGridComponent } from '../../../../packages/core-ui/js/gui_components/data-grid-editor/index.js';
Expand Down Expand Up @@ -31,6 +31,7 @@ function renderDataGridEditorStory(args) {
});

root.__storybookCleanup = () => component.destroy();
root.__whenReady = () => component.whenReady();
return root;
}

Expand All @@ -41,7 +42,7 @@ const meta = {
docs: {
description: {
component:
'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.',
'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.',
},
},
},
Expand All @@ -58,14 +59,18 @@ export const EmptyGrid = {
docs: {
description: {
story:
'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.',
'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.',
},
},
},
play: async ({ canvasElement }) => {
await canvasElement.__whenReady?.();
const canvas = within(canvasElement);
await userEvent.click(await canvas.findByRole('button', { name: 'Add Row' }));
await expect(await canvas.findByText('~rename-me', { exact: true })).toBeVisible();
await waitFor(() => {
expect(canvasElement.querySelector('[data-role="grid-total-rows"]')?.textContent).toMatch(/^Total rows: \d+/);
});
},
};

Expand All @@ -77,14 +82,18 @@ export const WithSampleData = {
docs: {
description: {
story:
'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.',
'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.',
},
},
},
play: async ({ canvasElement }) => {
await canvasElement.__whenReady?.();
const canvas = within(canvasElement);
await expect(await canvas.findByText('First Name', { exact: true })).toBeVisible();
await expect(await canvas.findByText('Ava', { exact: true })).toBeVisible();
await expect(await canvas.findByText('Paused', { exact: true })).toBeVisible();
await waitFor(() => {
expect(canvasElement.querySelector('[data-role="grid-total-rows"]')?.textContent).toBe('Total rows: 2');
});
},
};
126 changes: 126 additions & 0 deletions apps/web/src/stories/grid-row-visibility-summary.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { expect, userEvent, within } from 'storybook/test';
import { createGridRowVisibilitySummaryComponent } from '../../../../packages/core-ui/js/gui_components/data-grid-editor/grid-row-visibility-summary/index.js';

function renderGridRowVisibilitySummaryStory(args) {
const root = document.createElement('section');
root.style.display = 'flex';
root.style.flexDirection = 'column';
root.style.gap = '0.75rem';
root.style.padding = '0.75rem';
root.style.background = 'var(--panel-bg)';
root.style.color = 'var(--page-text)';
const statusRoot = document.createElement('div');
const controlsRoot = document.createElement('div');
controlsRoot.style.display = 'flex';
controlsRoot.style.flexWrap = 'wrap';
controlsRoot.style.gap = '0.5rem';
controlsRoot.innerHTML = `
<button type="button" data-action="show-filtered">Show Filtered Summary</button>
<button type="button" data-action="clear-filtered">Clear Filtered Summary</button>
`;

root.appendChild(statusRoot);
root.appendChild(controlsRoot);

const component = createGridRowVisibilitySummaryComponent({
root: statusRoot,
documentObj: document,
props: {
totalRowCount: args.totalRowCount,
visibleRowCount: args.visibleRowCount,
hasActiveFilters: args.hasActiveFilters,
},
});

controlsRoot.querySelector('[data-action="show-filtered"]')?.addEventListener('click', () => {
component.update({
totalRowCount: 8,
visibleRowCount: 3,
hasActiveFilters: true,
});
});
controlsRoot.querySelector('[data-action="clear-filtered"]')?.addEventListener('click', () => {
component.update({
totalRowCount: args.totalRowCount,
visibleRowCount: args.totalRowCount,
hasActiveFilters: false,
});
});

root.__storybookCleanup = () => component.destroy();
return root;
}

const meta = {
title: 'Data Grid Editor/Grid Row Visibility Summary',
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'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.',
},
},
},
args: {
totalRowCount: 8,
visibleRowCount: 8,
hasActiveFilters: false,
},
render: renderGridRowVisibilitySummaryStory,
};

export default meta;

export const TotalOnly = {
parameters: {
docs: {
description: {
story:
'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.',
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8');
},
};

export const FilteredVisible = {
args: {
totalRowCount: 8,
visibleRowCount: 3,
hasActiveFilters: true,
},
parameters: {
docs: {
description: {
story:
'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.',
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8 | Filtered Visible: 3');
},
};

export const InteractiveStateChange = {
parameters: {
docs: {
description: {
story:
'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.',
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button', { name: 'Show Filtered Summary' }));
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8 | Filtered Visible: 3');
await userEvent.click(canvas.getByRole('button', { name: 'Clear Filtered Summary' }));
await expect(canvas.getByRole('status')).toHaveTextContent('Total rows: 8');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class GridEditorComponent {
this.toolbar = this.container.locator('[data-role="grid-toolbar-root"]');
this.errorStatus = this.container.locator('[data-role="grid-error-status"]').first();
this.grid = this.container.locator('[data-role="data-grid-root"]');
this.totalRows = this.container.locator('[data-role="grid-total-rows"]').first();
this.contextMenu = this.page.locator('[data-role="data-grid-context-menu"]');
this.renderer = new GridRendererComponent(page, this.grid);
this.header = new GridHeaderComponent(page, this.grid, this.renderer);
Expand All @@ -36,6 +37,15 @@ class GridEditorComponent {
await expect(this.clearFiltersButton).toBeVisible();
await expect(this.clearSortButton).toBeVisible();
await expect(this.resetTableButton).toBeVisible();
await expect(this.totalRows).toBeVisible();
}

async expectTotalRows(count) {
await expect(this.totalRows).toHaveText(`Total rows: ${count}`);
}

async expectFilteredVisibleRows({ totalRows, visibleRows }) {
await expect(this.totalRows).toHaveText(`Total rows: ${totalRows} | Filtered Visible: ${visibleRows}`);
}

async expectReady() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ test.describe('3. Filtering and Sorting', () => {
test('Column Specific Filter', async ({ page }) => {
const { appPage, pageErrors } = await openApp(page);
const col = await seedRows(appPage, ['Open', 'Closed', 'Open']);
await appPage.gridEditor.expectTotalRows(3);

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

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

expectNoPageErrors(pageErrors);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ test.describe('3. Filtering and Sorting', () => {
test('Global Filter', async ({ page }) => {
const { appPage, pageErrors } = await openApp(page);
await seedRows(appPage, ['Apple', 'Banana', 'Cherry']);
await appPage.gridEditor.expectTotalRows(3);

await appPage.gridEditor.setQuickFilter('App');
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(1);
await appPage.gridEditor.expectFilteredVisibleRows({ totalRows: 3, visibleRows: 1 });
await appPage.gridEditor.setQuickFilter('app');
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(1);
await appPage.gridEditor.expectFilteredVisibleRows({ totalRows: 3, visibleRows: 1 });

await appPage.gridEditor.setQuickFilter('');
await expect.poll(async () => appPage.gridEditor.renderer.countVisibleRows()).toBe(3);
await appPage.gridEditor.expectTotalRows(3);

expectNoPageErrors(pageErrors);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ test.describe('1. Grid Basic Operations', () => {

await appPage.gridEditor.expectVisible();
const before = await appPage.gridEditor.renderer.countRows();
await appPage.gridEditor.expectTotalRows(before);
await appPage.gridEditor.addRow();
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(before + 1);
await appPage.gridEditor.expectTotalRows(before + 1);

const [columnName] = await appPage.gridEditor.header.getColumnNames();
await appPage.gridEditor.renderer.setCellTextByColumnName(columnName, before, 'Test Data');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ test.describe('4. Import Export Basic', () => {

await appPage.gridEditor.resetTable();
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(0);
await appPage.gridEditor.expectTotalRows(0);

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

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

expectNoPageErrors(pageErrors);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ test.describe('7. Test Data Generation', () => {
await appPage.testDataPanel.clickGenerate();
await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBe(5);
await expect.poll(async () => appPage.gridEditor.header.getColumnNames()).toContain('First Name');
await appPage.gridEditor.expectTotalRows(5);

const values = await appPage.gridEditor.renderer.getColumnTextsByName('First Name');
expect(values).toHaveLength(5);
Expand Down
7 changes: 7 additions & 0 deletions apps/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ body.theme-dark a:hover {
gap: 1rem;
}

.data-grid-total-rows {
color: var(--page-text);
background: transparent;
font-size: 1.05rem;
line-height: 1.4;
}

/* was #3D5249 - then docusaurus uses #2e8555*/
.header {
background-color: var(--header-bg);
Expand Down
1 change: 1 addition & 0 deletions docs/frontend-component-migration-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ Current status:
- 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.
- 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`.
- `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.
- `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.
- 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.
- `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.
- 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.
Expand Down
Loading