From 10ef39ac1d1c1c4e93d41d08cd37c5c536c9a684 Mon Sep 17 00:00:00 2001 From: Alan Richardson Date: Wed, 10 Jun 2026 11:02:15 +0100 Subject: [PATCH 1/3] add import trim options --- README.md | 9 ++ apps/api/src/api-service.js | 4 + apps/api/src/openapi.js | 10 ++ .../tests/generate/v1-generate-amend.spec.js | 17 +++ .../src/tests/health/health-endpoints.spec.js | 6 + apps/cli/src/cli-options.js | 11 ++ apps/cli/src/run-cli.js | 2 + apps/cli/src/tests/cli-options.test.js | 5 + .../src/tests/integration.cli-params.test.js | 27 ++++ apps/mcp/src/index.js | 9 ++ apps/mcp/src/mcp.test.js | 29 ++++ .../stories/import-export-toolbar.stories.js | 53 +++++++ .../import-export-workspace.component.js | 31 ++++ .../csv-file-upload-trim-input.spec.js | 29 ++++ .../030-rest-api.md | 4 +- .../070-interfaces-and-deployment/040-mcp.md | 4 +- .../050-cli-node-and-bun.md | 4 +- docs/frontend-component-migration-plan.md | 1 + ...import-export-import-control-controller.js | 4 + .../import-export-import-control-view.js | 99 +++++++++++++ .../import-export-toolbar-controller.js | 4 + .../import-export-toolbar-view.js | 1 + .../app/import-export-trim-state.js | 21 +++ ...ate-import-export-file-transfer-service.js | 12 ++ .../create-import-export-workspace-runtime.js | 1 + ...mport-export-workspace-workflow-service.js | 6 + .../import-export-workspace-controller.js | 7 + .../import-export-workspace-view.js | 1 + .../app/import-export-import-control.test.js | 35 +++++ .../tests/app/import-export-workspace.test.js | 59 ++++++++ .../core-ui/src/tests/grid/importer.test.js | 18 +++ packages/core/js/grid/import-trim-settings.js | 56 +++++++ packages/core/js/grid/importer.js | 17 ++- packages/core/src/index.js | 5 + .../core-api/amendFromTextSpecAndData.test.js | 57 ++++++++ .../cross-surface-amend-trim-parity.test.js | 137 ++++++++++++++++++ 36 files changed, 791 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/tests/browser/app/functional/import-export/csv-file-upload-trim-input.spec.js create mode 100644 packages/core-ui/js/gui_components/app/import-export-trim-state.js create mode 100644 packages/core/js/grid/import-trim-settings.js create mode 100644 tests/integration/cross-surface-amend-trim-parity.test.js diff --git a/README.md b/README.md index d0ea8968..ce52afe3 100644 --- a/README.md +++ b/README.md @@ -298,11 +298,17 @@ Supported options: - `-o, --outputfile` write output to file instead of stdout - `-t, --testMode` enable diagnostics mode and generate one row - `--unsafe-faker-expressions` allow expression-style faker args (disabled by default) +- `--trim-input` trim whitespace from every imported field value before amend processing +- `--trim-input-fields` comma-separated imported field names to trim before amend processing Amend existing data with a schema: `anywaydata amend --schema-file schema.txt --data-file input.csv --input-format csv -f json -o amended.json` +Trim imported input values during amend: + +`anywaydata amend --schema-file schema.txt --data-file input.csv --input-format csv --trim-input-fields Name,Email -f json` + Pairwise note: - when `--pairwise` is enabled, `--numberOfLines` is accepted but ignored (the pairwise engine determines row count) @@ -370,6 +376,7 @@ Amend imported data endpoint: `POST http://localhost:3000/v1/generate/amend` - JSON body fields: `textSpec` (required), `inputData` (required raw text), `inputFormat` (required), `rowCount` (optional, defaults to input row count), `outputFormat` (optional), `responseFormat` (optional), `stream` (optional and ignored) +- amend trim controls: `trimInput` trims all imported field values; `trimInputFieldsCsv` trims only the listed imported field names - `rowCount` must be `<=` imported row count - response returns the full resulting dataset after amendment @@ -499,6 +506,8 @@ Inputs: - `outputFormat` (required string e.g. `csv`, `json`, `jsonl`, `xml`, `sql`) - `options` (optional object) - `seed` (optional number) +- `trimInput` (optional boolean for `amend_data_from_spec`) +- `trimInputFieldsCsv` (optional string for `amend_data_from_spec`) Discoverability support: diff --git a/apps/api/src/api-service.js b/apps/api/src/api-service.js index e1659ef9..b23dbe5e 100644 --- a/apps/api/src/api-service.js +++ b/apps/api/src/api-service.js @@ -211,6 +211,8 @@ export function createApiService({ outputFormat = 'csv', options, seed, + trimInput, + trimInputFieldsCsv, unsafeFakerExpressions, stream, } = payload; @@ -242,6 +244,8 @@ export function createApiService({ outputFormat: concreteOutputFormat, options: effectiveOptions, seed: parsedSeed.seed, + trimInput: parseBooleanFlag(trimInput), + trimInputFieldsCsv, unsafeFakerExpressions: unsafeFakerExpressions || false, stream, }); diff --git a/apps/api/src/openapi.js b/apps/api/src/openapi.js index 13d71a13..40c6119e 100644 --- a/apps/api/src/openapi.js +++ b/apps/api/src/openapi.js @@ -418,6 +418,16 @@ const openApiDocument = { outputFormat: { type: 'string', default: 'csv' }, options: { type: 'object' }, seed: { type: 'number' }, + trimInput: { + type: 'boolean', + default: false, + description: 'Trim whitespace from every imported input field value before amend processing.', + }, + trimInputFieldsCsv: { + type: 'string', + description: + 'Comma-separated imported field names whose values should be trimmed before amend processing.', + }, unsafeFakerExpressions: { type: 'boolean', default: false }, responseFormat: { type: 'string', enum: ['rows', 'rendered', 'all', 'raw'], default: 'rows' }, stream: { type: 'boolean', description: 'Accepted for compatibility and ignored for amend mode.' }, diff --git a/apps/api/src/tests/generate/v1-generate-amend.spec.js b/apps/api/src/tests/generate/v1-generate-amend.spec.js index 502abfbe..b6117b43 100644 --- a/apps/api/src/tests/generate/v1-generate-amend.spec.js +++ b/apps/api/src/tests/generate/v1-generate-amend.spec.js @@ -41,4 +41,21 @@ test.describe('POST /v1/generate/amend', () => { expect(Array.isArray(body.diagnostics)).toBe(false); expect(typeof body.diagnostics).toBe('object'); }); + + test('amend trimInputFieldsCsv trims only listed imported columns', async ({ request }) => { + const response = await request.post(apiUrl('/v1/generate/amend'), { + data: { + textSpec: 'Status\nActive', + inputData: '"Name","Role"\n" Alice "," Engineer "', + inputFormat: 'csv', + rowCount: 0, + outputFormat: 'json', + trimInputFieldsCsv: 'Name', + }, + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body.rows).toEqual([['Alice', ' Engineer ', '']]); + }); }); diff --git a/apps/api/src/tests/health/health-endpoints.spec.js b/apps/api/src/tests/health/health-endpoints.spec.js index aec7299e..585c8468 100644 --- a/apps/api/src/tests/health/health-endpoints.spec.js +++ b/apps/api/src/tests/health/health-endpoints.spec.js @@ -24,6 +24,12 @@ test.describe('Health and docs endpoints', () => { expect(body.openapi).toBe('3.0.3'); expect(body.paths['/v1/generate']).toBeTruthy(); expect(body.paths['/v1/generate/amend']).toBeTruthy(); + expect( + body.paths['/v1/generate/amend'].post.requestBody.content['application/json'].schema.properties.trimInput + ).toBeTruthy(); + expect( + body.paths['/v1/generate/amend'].post.requestBody.content['application/json'].schema.properties.trimInputFieldsCsv + ).toBeTruthy(); }); test('GET /v1/docs serves Swagger UI html', async ({ request }) => { diff --git a/apps/cli/src/cli-options.js b/apps/cli/src/cli-options.js index 2e0e09a7..a4b6253a 100644 --- a/apps/cli/src/cli-options.js +++ b/apps/cli/src/cli-options.js @@ -11,6 +11,15 @@ export function parseCliOptions(argvInput = process.argv) { .option('schema-file', { type: 'string', describe: 'Schema file path for amend command' }) .option('data-file', { type: 'string', describe: 'Input data file path for amend command' }) .option('input-format', { type: 'string', describe: 'Input data format for amend command (e.g. csv, json)' }) + .option('trim-input', { + type: 'boolean', + default: false, + describe: 'Trim whitespace from every imported input field value before amend processing', + }) + .option('trim-input-fields', { + type: 'string', + describe: 'Comma-separated imported field names whose values should be trimmed before amend processing', + }) .option('n', { alias: 'numberOfLines', type: 'number', @@ -93,5 +102,7 @@ export function parseCliOptions(argvInput = process.argv) { bom: parsed.bom === true, unsafeFakerExpressions: parsed['unsafe-faker-expressions'] === true, pairwise: parsed.pairwise === true, + trimInput: command === 'amend' && parsed['trim-input'] === true, + trimInputFieldsCsv: command === 'amend' ? parsed['trim-input-fields'] || '' : '', }; } diff --git a/apps/cli/src/run-cli.js b/apps/cli/src/run-cli.js index bbac2ef4..77086029 100644 --- a/apps/cli/src/run-cli.js +++ b/apps/cli/src/run-cli.js @@ -95,6 +95,8 @@ export async function runCliCommand({ options, platform }) { rowCount: options.rowCount, outputFormat, options: formatterOptions, + trimInput: options.trimInput === true, + trimInputFieldsCsv: options.trimInputFieldsCsv || '', unsafeFakerExpressions: options.unsafeFakerExpressions, stream: options.shouldStream, }); diff --git a/apps/cli/src/tests/cli-options.test.js b/apps/cli/src/tests/cli-options.test.js index bbe15114..0827eb83 100644 --- a/apps/cli/src/tests/cli-options.test.js +++ b/apps/cli/src/tests/cli-options.test.js @@ -52,6 +52,9 @@ test('amend command options are parsed', () => { 'data.csv', '--input-format', 'csv', + '--trim-input', + '--trim-input-fields', + 'Name,Role', '-n', '3', '-f', @@ -63,6 +66,8 @@ test('amend command options are parsed', () => { expect(opts.inputFormat).toBe('csv'); expect(opts.rowCount).toBe(3); expect(opts.shouldStream).toBe(false); + expect(opts.trimInput).toBe(true); + expect(opts.trimInputFieldsCsv).toBe('Name,Role'); }); test('export encoding flags are parsed', () => { diff --git a/apps/cli/src/tests/integration.cli-params.test.js b/apps/cli/src/tests/integration.cli-params.test.js index 24a81bf7..0cff88fe 100644 --- a/apps/cli/src/tests/integration.cli-params.test.js +++ b/apps/cli/src/tests/integration.cli-params.test.js @@ -205,6 +205,33 @@ test('amend command applies schema to existing data', async () => { expect(JSON.parse(result.stdout.trim())).toEqual([{ Name: 'Bob' }, { Name: 'Eve' }]); }); +test('amend command trim flags affect imported input values before amend processing', async () => { + const schemaPath = tempFile('trim-schema'); + const dataPath = tempFile('trim-data'); + await fs.writeFile(schemaPath, 'Status\nActive', 'utf8'); + await fs.writeFile(dataPath, '"Name","Role"\n" Alice "," Engineer "', 'utf8'); + + const result = runCli([ + 'amend', + '--schema-file', + schemaPath, + '--data-file', + dataPath, + '--input-format', + 'csv', + '--trim-input-fields', + 'Name', + '-n', + '0', + '-f', + 'json', + '--show-progress', + 'false', + ]); + expect(result.status).toBe(0); + expect(JSON.parse(result.stdout.trim())).toEqual([{ Name: 'Alice', Role: ' Engineer ', Status: '' }]); +}); + test('amend command fixture flow: CSV input to DSV output on stdout', async () => { const schemaPath = path.join(amendFixturesDir, 'schema.txt'); const dataPath = path.join(amendFixturesDir, 'input.csv'); diff --git a/apps/mcp/src/index.js b/apps/mcp/src/index.js index ea6a04e2..fc88cdab 100644 --- a/apps/mcp/src/index.js +++ b/apps/mcp/src/index.js @@ -328,6 +328,15 @@ function handleRequest(request) { outputFormat: { type: 'string', enum: SUPPORTED_FORMATS }, options: GENERATE_OPTIONS_SCHEMA, seed: { type: 'number' }, + trimInput: { + type: 'boolean', + description: 'Trim whitespace from every imported input field value before amend processing.', + }, + trimInputFieldsCsv: { + type: 'string', + description: + 'Comma-separated imported field names whose values should be trimmed before amend processing.', + }, stream: { type: 'boolean', description: 'Accepted for compatibility and ignored for amend operation (always buffered).', diff --git a/apps/mcp/src/mcp.test.js b/apps/mcp/src/mcp.test.js index 8bffcd49..fef4c895 100644 --- a/apps/mcp/src/mcp.test.js +++ b/apps/mcp/src/mcp.test.js @@ -374,6 +374,35 @@ test('MCP server handles amend_data_from_spec tool call', () => { expect((payload.diagnostics?.warnings || []).join(' ')).toContain('stream is ignored'); }); +test('MCP amend tool schema includes import trim options', () => { + const response = requestServer({ jsonrpc: '2.0', id: 1500, method: 'tools/list' }); + const amendTool = response?.result?.tools?.find((tool) => tool.name === 'amend_data_from_spec'); + expect(amendTool?.inputSchema?.properties?.trimInput?.type).toBe('boolean'); + expect(amendTool?.inputSchema?.properties?.trimInputFieldsCsv?.type).toBe('string'); +}); + +test('MCP amend tool applies trimInputFieldsCsv to imported values', () => { + const response = requestServer({ + jsonrpc: '2.0', + id: 1501, + method: 'tools/call', + params: { + name: 'amend_data_from_spec', + arguments: { + textSpec: 'Status\nActive', + inputData: '"Name","Role"\n" Alice "," Engineer "', + inputFormat: 'csv', + rowCount: 0, + outputFormat: 'json', + trimInputFieldsCsv: 'Name', + }, + }, + }); + const payload = JSON.parse(response?.result?.content?.[0]?.text || '{}'); + expect(payload.ok).toBe(true); + expect(payload.rows).toEqual([['Alice', ' Engineer ', '']]); +}); + test('MCP amend parity matches core', () => { const coreResult = amendFromTextSpecAndData({ textSpec: 'Name\nBob', diff --git a/apps/web/src/stories/import-export-toolbar.stories.js b/apps/web/src/stories/import-export-toolbar.stories.js index c83b2df0..7d7a2ce2 100644 --- a/apps/web/src/stories/import-export-toolbar.stories.js +++ b/apps/web/src/stories/import-export-toolbar.stories.js @@ -33,6 +33,9 @@ function renderImportExportToolbarStory(args) { exportStatusMessage: args.exportStatusMessage, exportStatusLoading: args.exportStatusLoading, errorStatusMessage: args.errorStatusMessage, + trimInput: args.trimInput, + trimInputFieldsEnabled: args.trimInputFieldsEnabled, + trimInputFieldsCsv: args.trimInputFieldsCsv, }, callbacks: { onDownload: () => { @@ -47,6 +50,10 @@ function renderImportExportToolbarStory(args) { args.onImportFromClipboard?.(); result.textContent = 'action:import-from-clipboard'; }, + onImportTrimSettingsChange: (settings) => { + args.onImportTrimSettingsChange?.(settings); + result.textContent = `trim:${JSON.stringify(settings)}`; + }, }, }); @@ -77,6 +84,7 @@ const meta = { React.createElement(Controls), React.createElement(Canvas, { of: Default }), React.createElement(Canvas, { of: FileImportSurface }), + React.createElement(Canvas, { of: ImportTrimSettings }), React.createElement(Canvas, { of: BusyAndStatus }) ), description: { @@ -98,9 +106,13 @@ const meta = { exportStatusMessage: '', exportStatusLoading: false, errorStatusMessage: '', + trimInput: false, + trimInputFieldsEnabled: false, + trimInputFieldsCsv: '', onDownload: fn(), onFileSelected: fn(), onImportFromClipboard: fn(), + onImportTrimSettingsChange: fn(), }, argTypes: { fileExtension: { @@ -143,6 +155,18 @@ const meta = { control: 'text', description: 'Persistent error/status message shown at the end of the toolbar.', }, + trimInput: { + control: 'boolean', + description: 'When true, imported input values are trimmed before import/amend processing.', + }, + trimInputFieldsEnabled: { + control: 'boolean', + description: 'Enables field-list trimming in the import settings disclosure.', + }, + trimInputFieldsCsv: { + control: 'text', + description: 'Comma-separated imported field names trimmed when selected-fields mode is enabled.', + }, onDownload: { description: 'Storybook action fired when Download is clicked.', table: { category: 'Events' }, @@ -155,6 +179,10 @@ const meta = { description: 'Storybook action fired when From Clipboard is clicked.', table: { category: 'Events' }, }, + onImportTrimSettingsChange: { + description: 'Storybook action fired when the import trim settings change.', + table: { category: 'Events' }, + }, }, render: renderImportExportToolbarStory, }; @@ -236,3 +264,28 @@ export const BusyAndStatus = { await expect(canvas.getByText('Generating export text...')).toBeVisible(); }, }; + +export const ImportTrimSettings = { + args: { + trimInput: true, + trimInputFieldsEnabled: true, + trimInputFieldsCsv: 'Name, Email', + }, + render: renderImportExportToolbarStory, + parameters: { + docs: { + description: { + story: + 'Documents the import-only trim settings behind the import settings disclosure. Toggle the radio groups and edit the CSV textbox to confirm the emitted settings payload shown in the story log.', + }, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByLabelText('Import settings')); + const textInput = canvas.getByRole('textbox', { name: /trim input fields csv/i }); + await userEvent.clear(textInput); + await userEvent.type(textInput, 'Name, Role'); + await expect(canvas.getByText(/"trimInputFieldsCsv":"Name, Role"/)).toBeVisible(); + }, +}; diff --git a/apps/web/src/tests/browser/app/abstractions/components/import-export-workspace.component.js b/apps/web/src/tests/browser/app/abstractions/components/import-export-workspace.component.js index 745caf5f..da51492a 100644 --- a/apps/web/src/tests/browser/app/abstractions/components/import-export-workspace.component.js +++ b/apps/web/src/tests/browser/app/abstractions/components/import-export-workspace.component.js @@ -10,6 +10,8 @@ class ImportExportWorkspaceComponent { this.setGridFromTextButton = this.container.getByRole('button', { name: /set grid from text/i }); this.clipboardImportButton = this.container.getByRole('button', { name: /import from clipboard/i }); this.downloadButton = this.container.getByRole('button', { name: /download/i }); + this.importSettingsDetails = this.container.locator('[data-role="import-settings-details"]').first(); + this.importSettingsSummary = this.importSettingsDetails.locator('summary').first(); this.importLabel = this.container .locator('label') .filter({ hasText: /import:/i }) @@ -23,6 +25,10 @@ class ImportExportWorkspaceComponent { this.importProgressStatus = this.container.locator('[data-role="import-progress-status"]').first(); this.exportProgressStatus = this.container.locator('[data-role="export-progress-status"]').first(); this.errorStatus = this.container.locator('[data-role="error-status"]').first(); + this.trimInputOnRadio = this.container.getByRole('radio', { name: 'On' }).first(); + this.trimInputOffRadio = this.container.getByRole('radio', { name: 'Off' }).first(); + this.trimInputFieldsSelectedRadio = this.container.getByRole('radio', { name: 'Selected Fields' }).first(); + this.trimInputFieldsCsvInput = this.container.getByRole('textbox', { name: /trim input fields csv/i }).first(); } async expectVisible() { @@ -53,6 +59,23 @@ class ImportExportWorkspaceComponent { await this.fileInput.setInputFiles(filePath); } + async setTrimInput(enabled = true) { + await this.openImportExportDetails(); + await this.openImportSettings(); + if (enabled) { + await this.trimInputOnRadio.check(); + return; + } + await this.trimInputOffRadio.check(); + } + + async setTrimInputFieldsCsv(value) { + await this.openImportExportDetails(); + await this.openImportSettings(); + await this.trimInputFieldsSelectedRadio.check(); + await this.trimInputFieldsCsvInput.fill(value); + } + async clickDownloadAndWaitForEvent() { await this.openImportExportDetails(); const downloadPromise = this.page.waitForEvent('download'); @@ -121,6 +144,14 @@ class ImportExportWorkspaceComponent { await this.disclosureSummary.click(); await expect(this.disclosure).toHaveJSProperty('open', true); } + + async openImportSettings() { + if (await this.importSettingsDetails.evaluate((element) => element.open)) { + return; + } + await this.importSettingsSummary.click(); + await expect(this.importSettingsDetails).toHaveJSProperty('open', true); + } } module.exports = { ImportExportWorkspaceComponent }; diff --git a/apps/web/src/tests/browser/app/functional/import-export/csv-file-upload-trim-input.spec.js b/apps/web/src/tests/browser/app/functional/import-export/csv-file-upload-trim-input.spec.js new file mode 100644 index 00000000..c4600135 --- /dev/null +++ b/apps/web/src/tests/browser/app/functional/import-export/csv-file-upload-trim-input.spec.js @@ -0,0 +1,29 @@ +const { test } = require('@playwright/test'); +const { openApp, expectNoPageErrors, expect } = require('../../abstractions/helpers/scenario-helpers'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +test.describe('4. Import Export Basic', () => { + test('CSV file upload can trim selected imported fields', async ({ page }) => { + const { appPage, pageErrors } = await openApp(page); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gtev-upload-trim-')); + const valid = path.join(tempDir, 'trimmed.csv'); + fs.writeFileSync(valid, 'Name,Role\n Alice , Engineer \n'); + + try { + await appPage.importExportWorkspace.setTrimInputFieldsCsv('Name'); + await appPage.importExportWorkspace.uploadFile(valid); + await expect.poll(async () => appPage.gridEditor.renderer.countRows()).toBeGreaterThanOrEqual(1); + await appPage.importExportWorkspace.expectProgressStatusContains('Import complete'); + + await appPage.importExportWorkspace.setTextFromGrid(); + await expect.poll(async () => appPage.textPreviewEditor.getOutputText()).toContain('"Alice"'); + await expect.poll(async () => appPage.textPreviewEditor.getOutputText()).toContain('" Engineer "'); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + + expectNoPageErrors(pageErrors); + }); +}); diff --git a/docs-src/docs/070-interfaces-and-deployment/030-rest-api.md b/docs-src/docs/070-interfaces-and-deployment/030-rest-api.md index 6a69e3e2..c0cd67d7 100644 --- a/docs-src/docs/070-interfaces-and-deployment/030-rest-api.md +++ b/docs-src/docs/070-interfaces-and-deployment/030-rest-api.md @@ -142,6 +142,8 @@ Both endpoints generate data from the same schema language and output formats. T `POST /v1/generate/amend` behavior: - accepts `textSpec`, `inputData`, `inputFormat`, optional `rowCount`, `outputFormat`, `responseFormat` +- optional `trimInput: true` trims all imported field values before amend processing +- optional `trimInputFieldsCsv: "Name,Email"` trims only the listed imported field names - defaults `rowCount` to imported row count - if `rowCount` is provided and smaller, only first `N` rows are amended - output always returns the full resulting dataset @@ -218,6 +220,7 @@ curl -X POST http://localhost:3000/v1/generate/amend \ "inputFormat": "csv", "rowCount": 2, "outputFormat": "dsv", + "trimInputFieldsCsv": "Name", "responseFormat": "all", "stream": true }' @@ -268,4 +271,3 @@ curl http://localhost:3000/openapi.json - If you run on a non-default port, replace `3000` in all examples. - For MCP tool integrations, see [MCP](/docs/interfaces-and-deployment/mcp). - diff --git a/docs-src/docs/070-interfaces-and-deployment/040-mcp.md b/docs-src/docs/070-interfaces-and-deployment/040-mcp.md index e0397112..39c679a4 100644 --- a/docs-src/docs/070-interfaces-and-deployment/040-mcp.md +++ b/docs-src/docs/070-interfaces-and-deployment/040-mcp.md @@ -110,6 +110,8 @@ Important: `amend_data_from_spec` behavior: - requires `textSpec`, `inputData`, and `inputFormat` +- optional `trimInput` trims all imported field values before amend processing +- optional `trimInputFieldsCsv` trims only the listed imported field names before amend processing - optional `rowCount` defaults to imported row count - if `rowCount` is smaller than imported rows, only first `N` rows are amended - output always includes the full resulting dataset @@ -129,10 +131,10 @@ Example `tools/call` payload: "inputFormat": "csv", "rowCount": 2, "outputFormat": "json", + "trimInputFieldsCsv": "Name", "stream": true } } } ``` - diff --git a/docs-src/docs/070-interfaces-and-deployment/050-cli-node-and-bun.md b/docs-src/docs/070-interfaces-and-deployment/050-cli-node-and-bun.md index f96249c4..b2463e13 100644 --- a/docs-src/docs/070-interfaces-and-deployment/050-cli-node-and-bun.md +++ b/docs-src/docs/070-interfaces-and-deployment/050-cli-node-and-bun.md @@ -47,6 +47,8 @@ Parameter guide for the examples: - `--stream`: enable streaming generation when supported (`csv`, `jsonl`, `dsv`, `json`, `xml`). - `--stream-threshold`: auto-enable streaming when `rowCount >= threshold` and `--outputfile` is set (default `5000`). - `--unsafe-faker-expressions`: opt-in to expression-style faker arguments (unsafe for untrusted input). +- `--trim-input`: trim whitespace from every imported field value before amend processing. +- `--trim-input-fields`: comma-separated imported field names to trim before amend processing. - `--help`: show CLI usage and options. - `amend --schema-file --data-file --input-format`: import input data and amend it with schema rules. @@ -82,6 +84,7 @@ anywaydata amend \ --schema-file schema.txt \ --data-file input.csv \ --input-format csv \ + --trim-input-fields Name,Email \ -f dsv ``` @@ -154,4 +157,3 @@ anywaydata generate -i input.txt -n 10 -f csv --unsafe-faker-expressions - Choose **CLI** for local scripts and shell pipelines. - Choose [REST API](/docs/interfaces-and-deployment/rest-api) for HTTP integrations and OpenAPI. - Choose [MCP](/docs/interfaces-and-deployment/mcp) for stdio tool integrations with MCP hosts. - diff --git a/docs/frontend-component-migration-plan.md b/docs/frontend-component-migration-plan.md index 626ccca4..e88be565 100644 --- a/docs/frontend-component-migration-plan.md +++ b/docs/frontend-component-migration-plan.md @@ -560,6 +560,7 @@ Current status: - `PopulationActions` now has dedicated reviewer-facing Storybook coverage in `apps/web/src/stories/population-actions.stories.js`, and the action cluster is now reused by generator controls as a shared icon+tippy action component with host-specific HTML help content for app-to-grid versus generator-to-file flows. - The embedded app test-data panel no longer exposes a separate `Refresh Text Preview` button; successful generate/amend flows now refresh the preview automatically so the shared action cluster stays aligned with the generator surface. - The import/export toolbar Storybook docs now expose the real file-input and drag/drop surface directly, with dedicated reviewer-facing stories for the default toolbar, file-import boundary, and busy/status state instead of leaving drag/drop behavior implicit inside the full workspace story. +- The import/export toolbar Storybook docs now also cover the import-only trim settings disclosure, so reviewers can inspect the new import trim radios and field-list textbox without booting the full app page. - App and generator page shell composition now also have standalone reviewer-facing Storybook coverage in `app-page-shell.stories.js` and `generator-page-shell.stories.js`, using explicit placeholder mount-root cards so reviewers can inspect shell layout separately from full bootstrap/runtime behavior. - The re-audit found that both app and generator still owned visible schema wrapper markup around the shared schema definition. That wrapper is now one shared `SchemaPanel` component with standalone Storybook host coverage in `generator-schema-panel.stories.js` and `test-data-schema-panel.stories.js`, so `DataPopulationPanel` and `GeneratorPage` both compose a shared schema wrapper instead of carrying host-local copies. - 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. diff --git a/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-controller.js b/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-controller.js index e92616b5..22eacc63 100644 --- a/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-controller.js +++ b/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-controller.js @@ -1,3 +1,5 @@ +import { applyImportTrimProps, createImportTrimState } from '../import-export-trim-state.js'; + class ImportExportImportControlController { constructor({ props = {} } = {}) { this.state = { @@ -8,6 +10,7 @@ class ImportExportImportControlController { fileExtension: props.fileExtension || '.csv', importStatusMessage: props.importStatusMessage || '', importStatusLoading: props.importStatusLoading === true, + ...createImportTrimState(props), }; } @@ -37,6 +40,7 @@ class ImportExportImportControlController { if (Object.prototype.hasOwnProperty.call(nextProps, 'importStatusLoading')) { this.state.importStatusLoading = nextProps.importStatusLoading === true; } + applyImportTrimProps(this.state, nextProps); } } diff --git a/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-view.js b/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-view.js index 711a6212..feb2c518 100644 --- a/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-view.js +++ b/packages/core-ui/js/gui_components/app/import-export-import-control/import-export-import-control-view.js @@ -8,6 +8,9 @@ class ImportExportImportControlView { this.services = services; this.fileImportBindings = null; this.handleImportFromClipboardClick = () => this.callbacks.onImportFromClipboard?.(); + this.handleTrimInputChange = () => this.emitImportTrimSettingsChange(); + this.handleTrimInputFieldsModeChange = () => this.emitImportTrimSettingsChange(); + this.handleTrimInputFieldsTextInput = () => this.emitImportTrimSettingsChange(); } mount() { @@ -26,6 +29,48 @@ class ImportExportImportControlView { return `
+
+ + ${renderIconHtml('settings', { className: 'app-icon import-export-import-control__settings-icon' })} + Settings + +
+
+ Trim Input + + +
+
+ Trim Input Fields + + + +
+
+