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..379fc4f5 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
@@ -6,8 +6,12 @@ class ImportExportImportControlView {
this.controller = controller;
this.callbacks = callbacks;
this.services = services;
+ this.instanceId = ImportExportImportControlView.nextInstanceId++;
this.fileImportBindings = null;
this.handleImportFromClipboardClick = () => this.callbacks.onImportFromClipboard?.();
+ this.handleTrimInputChange = () => this.emitImportTrimSettingsChange();
+ this.handleTrimInputFieldsModeChange = () => this.emitImportTrimSettingsChange();
+ this.handleTrimInputFieldsTextInput = () => this.emitImportTrimSettingsChange();
}
mount() {
@@ -23,9 +27,53 @@ class ImportExportImportControlView {
template() {
const state = this.controller.getState();
+ const trimInputGroupName = this.getRadioGroupName('trim-input');
+ const trimInputFieldsGroupName = this.getRadioGroupName('trim-input-fields');
return `
+
+
+ ${renderIconHtml('settings', { className: 'app-icon import-export-import-control__settings-icon' })}
+ Settings
+
+
+
+
+
+