Skip to content

Commit 1e7c567

Browse files
authored
Add guided params editor to schema UI (#206)
* Add guided schema params editor * Fix enum params dialog for variadic values * Improve Storybook params dialog framing * Prevent command picker style flash * Use edit icon for params dialog * Refine guided params editor defaults * Fix params dialog value field overflow * Add param metadata tooltips * Fix named params mapping in params dialog * Refine params editor dialog behavior
1 parent ec7867a commit 1e7c567

19 files changed

Lines changed: 2162 additions & 15 deletions

apps/web/src/stories/shared-schema-definition.stories.js

Lines changed: 58 additions & 1 deletion
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 RandExp from 'randexp';
33
import { faker } from '@faker-js/faker';
44
import {
@@ -87,6 +87,7 @@ function renderSharedSchemaDefinitionStory(args) {
8787
const fakerCommands = getFakerCommands().filter((command) => command !== 'RegEx' && command.startsWith('helpers.'));
8888
const domainCommands = getDomainCommands();
8989
const root = document.createElement('section');
90+
root.style.minHeight = args.storyMinHeight || 'auto';
9091
const ids = createIds(args.idPrefix || 'shared-schema');
9192
const createBlankRow = createBlankRowFactory(args.idPrefix || 'story-schema-row');
9293
const component = createSharedSchemaDefinitionComponent({
@@ -201,6 +202,7 @@ const meta = {
201202
initialText: '',
202203
showErrors: false,
203204
startInTextMode: false,
205+
storyMinHeight: 'auto',
204206
},
205207
};
206208

@@ -423,3 +425,58 @@ export const CommandPicker = {
423425
await expect(commandInput?.value).toBe('helpers.fake');
424426
},
425427
};
428+
429+
export const ParamsDialog = {
430+
render: renderSharedSchemaDefinitionStory,
431+
args: {
432+
storyMinHeight: '820px',
433+
initialRows: [
434+
{
435+
id: 'sequence-row',
436+
name: 'SequenceId',
437+
sourceType: 'domain',
438+
command: 'autoIncrement.sequence',
439+
params: '',
440+
value: '',
441+
comments: '',
442+
leadingTextLines: [],
443+
},
444+
],
445+
},
446+
parameters: {
447+
layout: 'fullscreen',
448+
docs: {
449+
description: {
450+
story:
451+
'Guided params editing flow for documented command params. This story demonstrates the value-only editor: required state appears as read-only checkboxes, documented defaults are prefilled into the value inputs, each param row exposes a help tippy with descriptions/examples/rules, and string values are auto-quoted when the generated schema params text is built. Hover a row help icon to review the metadata before editing and applying the params back into the shared row editor.',
452+
},
453+
},
454+
},
455+
play: async ({ canvasElement }) => {
456+
expectSchemaModeVisible(canvasElement);
457+
const initialRow = canvasElement.querySelector('.shared-schema-row');
458+
const paramsButton = initialRow.querySelector('[data-action="edit-params"]');
459+
expect(paramsButton).not.toBeNull();
460+
await userEvent.click(paramsButton);
461+
462+
const dialog = within(document.body).getByRole('dialog', { name: /edit params for .*autoincrement\.sequence/i });
463+
const dialogScope = within(dialog);
464+
const firstHelpIcon = dialog.querySelector('[data-role="params-editor-param-help"]');
465+
expect(dialog.querySelector('[data-role="params-editor-mode"]')).toBeNull();
466+
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('<strong>Rules:</strong>'));
467+
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('Optional.'));
468+
await expect(firstHelpIcon).toHaveAttribute('data-help-text', expect.stringContaining('Default: 1'));
469+
await expect(dialogScope.getByRole('textbox', { name: /start value/i }).value).toBe('1');
470+
await expect(dialogScope.getByRole('textbox', { name: /step value/i }).value).toBe('1');
471+
const prefixInput = dialogScope.getByRole('textbox', { name: /prefix value/i });
472+
await userEvent.type(prefixInput, 'filename');
473+
await expect(dialogScope.getByText('(start=1,step=1,prefix="filename",zeropadding=0)')).toBeTruthy();
474+
await userEvent.click(dialogScope.getByRole('button', { name: /^apply$/i }));
475+
476+
await waitFor(() =>
477+
expect(canvasElement.querySelector('.shared-schema-row [data-field="params"]').value).toBe(
478+
'(start=1,step=1,prefix="filename",zeropadding=0)'
479+
)
480+
);
481+
},
482+
};

apps/web/src/tests/browser/generator/functional/schema-edit.spec.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,25 @@ test.describe('Generator Schema Editing', () => {
212212
expectNoPageErrors(pageErrors);
213213
});
214214

215+
test('documented command params can be edited through the guided params dialog', async ({ page }) => {
216+
const { generatorPage, pageErrors } = await openGenerator(page);
217+
218+
await generatorPage.schema.setTextMode(false);
219+
await generatorPage.schema.setRowName(0, 'Status');
220+
await generatorPage.schema.editor.setRowTypeValue(0, 'datatype.enum');
221+
await generatorPage.schema.editor.editRowParamsWithDialog(0, {
222+
values: 'active,inactive,pending',
223+
});
224+
225+
await expect(generatorPage.schema.row(0).locator('[data-action="pick-command"]')).toHaveText('datatype.enum');
226+
await expect(generatorPage.schema.row(0).locator('input[data-field="params"]')).toHaveValue(
227+
'(active,inactive,pending)'
228+
);
229+
await expect.poll(async () => generatorPage.schema.getSchemaText()).toContain('enum(active,inactive,pending)');
230+
231+
expectNoPageErrors(pageErrors);
232+
});
233+
215234
test('schema edit buttons states are correct across top middle and bottom rows', async ({ page }) => {
216235
const { generatorPage, pageErrors } = await openGenerator(page);
217236

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const { expect } = require('@playwright/test');
2+
3+
function escapeRegExp(value) {
4+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5+
}
6+
7+
class ParamsEditorDialogComponent {
8+
constructor(page) {
9+
this.page = page;
10+
this.overlay = page.locator('[data-role="params-editor-overlay"]');
11+
this.dialog = page.getByRole('dialog', { name: /^edit params for /i });
12+
this.applyButton = this.dialog.getByRole('button', { name: /^apply$/i });
13+
}
14+
15+
async expectOpen() {
16+
await expect(this.dialog).toBeVisible();
17+
}
18+
19+
valueInput(name) {
20+
return this.dialog.getByRole('textbox', { name: new RegExp(`^${escapeRegExp(name)} value$`, 'i') });
21+
}
22+
23+
async setValue(name, value) {
24+
await this.expectOpen();
25+
await this.valueInput(name).fill(String(value));
26+
}
27+
28+
async apply() {
29+
await this.expectOpen();
30+
await this.applyButton.click();
31+
await expect(this.overlay).toHaveCount(0);
32+
}
33+
}
34+
35+
module.exports = { ParamsEditorDialogComponent };

apps/web/src/tests/browser/shared/abstractions/components/schema-editor.component.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { expect } = require('@playwright/test');
22
const { MethodPickerDialogComponent } = require('./method-picker-dialog.component');
33
const { OverlaySafeActivationComponent } = require('./overlay-safe-activation.component');
4+
const { ParamsEditorDialogComponent } = require('./params-editor-dialog.component');
45

56
class SchemaEditorComponent {
67
constructor(page, config) {
@@ -31,6 +32,7 @@ class SchemaEditorComponent {
3132
? page.locator(this.config.addFieldSelector)
3233
: this.root.locator('[data-role="schema-add-field"]');
3334
this.methodPicker = new MethodPickerDialogComponent(page);
35+
this.paramsEditor = new ParamsEditorDialogComponent(page);
3436
}
3537

3638
row(index) {
@@ -171,6 +173,16 @@ class SchemaEditorComponent {
171173
await this.row(index).locator(`button[data-action="${action}"]`).click();
172174
}
173175

176+
async editRowParamsWithDialog(index, valuesByName) {
177+
await this.ensureSchemaMode();
178+
await this.dismissOpenHelpTooltips();
179+
await this.row(index).locator('[data-action="edit-params"]').click();
180+
for (const [name, value] of Object.entries(valuesByName || {})) {
181+
await this.paramsEditor.setValue(name, value);
182+
}
183+
await this.paramsEditor.apply();
184+
}
185+
174186
async dragRowToIndex(fromIndex, toIndex, { placement = 'before' } = {}) {
175187
await this.ensureSchemaMode();
176188
const source = this.row(fromIndex).locator('[data-action="drag"]');

apps/web/styles.css

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2063,7 +2063,7 @@ body.theme-dark .shared-schema-row-validation {
20632063
align-self: center;
20642064
margin: 0 0.2rem;
20652065
}
2066-
.shared-schema-row > input[data-field='params'] {
2066+
.shared-schema-row > .shared-schema-params-control {
20672067
grid-column: 1 / span 3;
20682068
grid-row: 4;
20692069
min-width: 0;
@@ -2097,6 +2097,23 @@ body.theme-dark .shared-schema-row-validation {
20972097
position: relative;
20982098
}
20992099

2100+
.shared-schema-params-control {
2101+
display: grid;
2102+
grid-template-columns: minmax(0, 1fr) max-content;
2103+
gap: 0.35rem;
2104+
align-items: center;
2105+
}
2106+
2107+
.shared-schema-params-control > input[data-field='params'] {
2108+
min-width: 0;
2109+
width: 100%;
2110+
}
2111+
2112+
.shared-schema-params-button[disabled] {
2113+
opacity: 0.55;
2114+
cursor: not-allowed;
2115+
}
2116+
21002117
.shared-schema-command-picker-shadow-select {
21012118
position: absolute;
21022119
left: -9999px;

docs/frontend-component-migration-plan.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ Current status:
305305
306306
- `SharedSchemaDefinition` now lives under `shared/schema-definition/` with a controller, view, and create-component factory.
307307
- The new component reuses the existing shared schema parsing, validation, row-editing, command-picker, drag/drop, and text-mode logic from `shared/test-data/schema/` instead of duplicating those rules.
308+
- The shared schema row editor now also exposes a documented params-edit dialog for commands with parameter metadata, so app and generator hosts share the same guided param-entry flow and semantic validation path instead of leaving params as raw punctuation-only text entry.
309+
- The shared params-edit dialog now normalizes command metadata so optional/defaulted params stay visibly optional, shows reviewer-facing `Req` checkboxes instead of text labels, and auto-quotes string values while keeping the dialog focused on value entry rather than quote-format selection.
308310
- The app test-data panel now mounts its schema editor through `SharedSchemaDefinition`, keeping the same DOM IDs so the rest of the app flow and browser tests continue to treat the schema surface as a black box.
309311
- The generator page now also mounts its live schema editor through `SharedSchemaDefinition`, while preserving the page-level `#generatorSchemaSection` host contract, row-level browser interactions, and text-mode generate/pairwise flows.
310312
- Generator runtime adoption also moved the shared text-mode syncing, method-picker command selection, semantic-validation caret preservation, and pairwise-button visibility behavior onto the shared component path.

packages/core-ui/js/gui_components/shared/domain-command-help-metadata.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ const SYNTHETIC_DOMAIN_HELP = Object.freeze({
1212
examples: ['datatype.enum(active,inactive,pending)'],
1313
exampleReturnValues: ['active', 'inactive', 'pending'],
1414
returnType: 'string',
15-
args: [],
15+
args: [
16+
{
17+
name: 'values',
18+
type: 'comma-separated list',
19+
variadic: true,
20+
optional: false,
21+
description: 'List of allowed enum values chosen at random during generation.',
22+
example: 'active,inactive,pending',
23+
},
24+
],
1625
},
1726
});
1827

packages/core-ui/js/gui_components/shared/test-data/help/help-model-builder.js

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ const HELP_URLS = Object.freeze({
2727
enum: 'https://anywaydata.com/docs/category/generating-data',
2828
});
2929

30+
const ENUM_VALUE_PARAM = Object.freeze({
31+
name: 'values',
32+
type: 'comma-separated list',
33+
variadic: true,
34+
optional: false,
35+
description: 'List of allowed values chosen at random during generation.',
36+
example: 'active,inactive,pending',
37+
});
38+
3039
function resolveFakerDocsUrl(command, docsUrl) {
3140
const normalizedCommand = String(command || '').trim();
3241
if (normalizedCommand.startsWith('helpers.')) {
@@ -42,6 +51,54 @@ function cleanParamText(text) {
4251
.trim();
4352
}
4453

54+
function extractSimpleDefaultValue(param = {}) {
55+
if (Object.prototype.hasOwnProperty.call(param, 'defaultValue')) {
56+
return param.defaultValue;
57+
}
58+
if (Object.prototype.hasOwnProperty.call(param, 'default')) {
59+
return param.default;
60+
}
61+
62+
const description = cleanParamText(param.description);
63+
const match = description.match(/defaults?\s+to\s+("[^"]*"|'[^']*'|-?\d+(?:\.\d+)?|true|false|null)\.?$/iu);
64+
if (!match) {
65+
return '';
66+
}
67+
68+
const value = match[1];
69+
if (value.startsWith('"') || value.startsWith("'")) {
70+
return value.slice(1, -1);
71+
}
72+
return value;
73+
}
74+
75+
function normalizeHelpParam(param = {}) {
76+
return {
77+
...param,
78+
optional: param.optional === true || param.required === false,
79+
defaultValue: extractSimpleDefaultValue(param),
80+
};
81+
}
82+
83+
function normalizeHelpParams(params = []) {
84+
return (Array.isArray(params) ? params : []).map((param) => normalizeHelpParam(param));
85+
}
86+
87+
function resolveDomainHelpParams(command, commandHelp) {
88+
const normalized = normalizeHelpParams(commandHelp?.args || []);
89+
if (normalized.length > 0) {
90+
return normalized;
91+
}
92+
if (
93+
String(command || '')
94+
.trim()
95+
.toLowerCase() === 'datatype.enum'
96+
) {
97+
return normalizeHelpParams([ENUM_VALUE_PARAM]);
98+
}
99+
return [];
100+
}
101+
45102
function buildCallSignature(heading, params) {
46103
if (!Array.isArray(params) || params.length === 0) {
47104
return `${heading}()`;
@@ -169,11 +226,8 @@ function buildSchemaHelpModel(sourceType, commandValue) {
169226
{
170227
params: [
171228
{
172-
name: 'values',
173-
type: 'comma-separated list',
174-
optional: false,
229+
...ENUM_VALUE_PARAM,
175230
description: 'List of allowed values randomly selected during generation.',
176-
example: 'active,inactive,pending',
177231
},
178232
],
179233
example: 'enum active,inactive,pending',
@@ -198,7 +252,7 @@ function buildSchemaHelpModel(sourceType, commandValue) {
198252
heading: `faker.${command}`,
199253
summary: commandHelp?.summary || `Generates data using faker.${command}.`,
200254
docsUrl: resolveFakerDocsUrl(command, commandHelp?.docsUrl),
201-
params: commandHelp?.params || [],
255+
params: normalizeHelpParams(commandHelp?.params || []),
202256
example: commandHelp?.example || '',
203257
examples: Array.isArray(commandHelp?.examples) ? commandHelp.examples : [],
204258
exampleReturnValues: Array.isArray(commandHelp?.exampleReturnValues)
@@ -226,7 +280,7 @@ function buildSchemaHelpModel(sourceType, commandValue) {
226280
heading: commandHelp?.canonical || command,
227281
summary: commandHelp?.summary || `Generates data using ${commandHelp?.canonical || command}.`,
228282
docsUrl: commandHelp?.docsUrl || HELP_URLS.domain,
229-
params: commandHelp?.args || [],
283+
params: resolveDomainHelpParams(command, commandHelp),
230284
example: commandHelp?.example || '',
231285
examples: Array.isArray(commandHelp?.examples) ? commandHelp.examples : [],
232286
exampleReturnValues: Array.isArray(commandHelp?.exampleReturnValues)

0 commit comments

Comments
 (0)