@qoretechnologies/reqraft 0.10.36 → 0.10.38-pr.95.gf1f6403
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +121 -2
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +20 -0
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +91 -22
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +27 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +259 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -0
- package/dist/components/form/engine/compactRowStyles.d.ts +11 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +14 -3
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +16 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +72 -5
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +13 -1
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +4 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +8 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +112 -11
- package/src/components/form/engine/FormEngine.stories.tsx +313 -13
- package/src/components/form/engine/FormEngine.tsx +98 -7
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +462 -0
- package/src/components/form/engine/compactRowStyles.ts +14 -2
- package/src/components/form/engine/readFirst.ts +81 -4
- package/src/components/form/fields/array/ArrayAuto.tsx +12 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +11 -0
|
@@ -1164,7 +1164,7 @@ export const NestedOptionInheritsRenderPropFromAncestorCompact: Story = {
|
|
|
1164
1164
|
docs: {
|
|
1165
1165
|
description: {
|
|
1166
1166
|
story:
|
|
1167
|
-
"Renders the NestedOptionInheritsRenderPropFromAncestor schema with compact=true — the compact renderer
|
|
1167
|
+
"Renders the NestedOptionInheritsRenderPropFromAncestor schema with compact=true — the compact renderer previews the list-of-hash rows through their arg_schema, naming each method, rather than printing [object Object].",
|
|
1168
1168
|
},
|
|
1169
1169
|
},
|
|
1170
1170
|
},
|
|
@@ -1229,11 +1229,13 @@ export const NestedOptionInheritsRenderPropFromAncestorCompact: Story = {
|
|
|
1229
1229
|
await waitFor(() => expect(canvas.getAllByText('Methods').length).toBeGreaterThan(0), {
|
|
1230
1230
|
timeout: 5000,
|
|
1231
1231
|
});
|
|
1232
|
-
// The list-of-hashes value
|
|
1233
|
-
//
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1232
|
+
// The list-of-hashes value names its items — never a raw "[object Object]"
|
|
1233
|
+
// (regression: it used to stringify each hash envelope). The names now come
|
|
1234
|
+
// from the schema preview rather than a joined summary line above it, so the
|
|
1235
|
+
// assertion moved with them; what must never come back is the stringified
|
|
1236
|
+
// envelope.
|
|
1237
|
+
await expect(await canvas.findByText('init', undefined, { timeout: 5000 })).toBeInTheDocument();
|
|
1238
|
+
await expect(await canvas.findByText('run')).toBeInTheDocument();
|
|
1237
1239
|
await expect(canvasElement.textContent ?? '').not.toContain('[object Object]');
|
|
1238
1240
|
},
|
|
1239
1241
|
};
|
|
@@ -1290,12 +1292,15 @@ export const CompactNestedListRowsStayCompact: Story = {
|
|
|
1290
1292
|
} as unknown as IOptionsSchema,
|
|
1291
1293
|
},
|
|
1292
1294
|
play: async ({ canvasElement }) => {
|
|
1293
|
-
// Collapsed, the row
|
|
1294
|
-
|
|
1295
|
+
// Collapsed, the row previews its items through their schema — one numbered
|
|
1296
|
+
// entry per method, each naming itself.
|
|
1297
|
+
await _testsWaitForText('Method Name');
|
|
1298
|
+
await _testsWaitForText('init');
|
|
1295
1299
|
|
|
1296
1300
|
// Open the Methods row — this mounts ArrayAuto and, with it, one
|
|
1297
|
-
// arg_schema sub-form per row.
|
|
1298
|
-
|
|
1301
|
+
// arg_schema sub-form per row. Clicked by the field's NAME: the joined
|
|
1302
|
+
// summary that used to sit on this row is gone, replaced by the preview.
|
|
1303
|
+
await _testsClickText('Methods');
|
|
1299
1304
|
|
|
1300
1305
|
await waitFor(
|
|
1301
1306
|
() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(2),
|
|
@@ -1347,8 +1352,8 @@ export const CompactNestedListRowsStayCompactMobile: Story = {
|
|
|
1347
1352
|
),
|
|
1348
1353
|
],
|
|
1349
1354
|
play: async ({ canvasElement }) => {
|
|
1350
|
-
await _testsWaitForText('init
|
|
1351
|
-
await _testsClickText('
|
|
1355
|
+
await _testsWaitForText('init');
|
|
1356
|
+
await _testsClickText('Methods');
|
|
1352
1357
|
|
|
1353
1358
|
await waitFor(
|
|
1354
1359
|
() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(2),
|
|
@@ -4122,6 +4127,29 @@ export const CompactFieldTypes: Story = {
|
|
|
4122
4127
|
secure: { type: 'bool', display_name: 'Secure' },
|
|
4123
4128
|
},
|
|
4124
4129
|
},
|
|
4130
|
+
// The list counterpart of `connectionInfo`: a list of hashes whose fields
|
|
4131
|
+
// are declared. This is the shape the schema preview was built for, and the
|
|
4132
|
+
// catalogue was missing it — it covered the described HASH but not the
|
|
4133
|
+
// described LIST, which is the case that was actually reported.
|
|
4134
|
+
routes: {
|
|
4135
|
+
type: 'list',
|
|
4136
|
+
ui_type: 'list',
|
|
4137
|
+
element_type: 'hash',
|
|
4138
|
+
display_name: 'List of hashes (arg_schema)',
|
|
4139
|
+
group: 'structured',
|
|
4140
|
+
arg_schema: {
|
|
4141
|
+
method: {
|
|
4142
|
+
type: 'string',
|
|
4143
|
+
display_name: 'Method',
|
|
4144
|
+
allowed_values: [
|
|
4145
|
+
{ value: 'get', display_name: 'GET' },
|
|
4146
|
+
{ value: 'post', display_name: 'POST' },
|
|
4147
|
+
],
|
|
4148
|
+
},
|
|
4149
|
+
path: { type: 'string', display_name: 'Path' },
|
|
4150
|
+
handler: { type: 'string', ui_type: 'code-editor', display_name: 'Handler' },
|
|
4151
|
+
},
|
|
4152
|
+
},
|
|
4125
4153
|
freeConfig: {
|
|
4126
4154
|
type: 'free-hash',
|
|
4127
4155
|
ui_type: 'free-hash',
|
|
@@ -4330,6 +4358,22 @@ export const CompactFieldTypes: Story = {
|
|
|
4330
4358
|
secure: { type: 'bool', value: true },
|
|
4331
4359
|
},
|
|
4332
4360
|
},
|
|
4361
|
+
routes: {
|
|
4362
|
+
type: 'list',
|
|
4363
|
+
value: [
|
|
4364
|
+
// One item complete, one with the optional field unset — the empty
|
|
4365
|
+
// state stays exercised rather than being filled in everywhere.
|
|
4366
|
+
{
|
|
4367
|
+
type: 'hash',
|
|
4368
|
+
value: {
|
|
4369
|
+
method: 'get',
|
|
4370
|
+
path: '/orders',
|
|
4371
|
+
handler: 'sub get_orders() {\n return orders.all();\n}',
|
|
4372
|
+
},
|
|
4373
|
+
},
|
|
4374
|
+
{ type: 'hash', value: { method: 'post', path: '/orders' } },
|
|
4375
|
+
],
|
|
4376
|
+
},
|
|
4333
4377
|
freeConfig: { type: 'free-hash', value: '%YAML 1.2\n---\nretries: 3\ntimeout: 30\n' },
|
|
4334
4378
|
bigConfig: {
|
|
4335
4379
|
type: 'hash',
|
|
@@ -4412,7 +4456,23 @@ export const CompactFieldTypes: Story = {
|
|
|
4412
4456
|
await _testsWaitForText('#0000FF'); // colour → uppercase hex
|
|
4413
4457
|
await _testsWaitForText('rgba(255, 0, 0, 0.5)'); // colour with alpha → rgba()
|
|
4414
4458
|
await _testsWaitForText('config.txt'); // file → filename
|
|
4415
|
-
|
|
4459
|
+
// A hash the schema DESCRIBES needs no count: its preview names every field,
|
|
4460
|
+
// so "3 fields" above "Host / Port / Secure" would be the same fact twice.
|
|
4461
|
+
// A hash it does not describe keeps the count — the untyped tree beneath it
|
|
4462
|
+
// says nothing about what the value means, so the count is the only line
|
|
4463
|
+
// that does. Both halves asserted here: the distinction IS the behaviour.
|
|
4464
|
+
await _testsWaitForText('2 fields'); // undescribed hash → field count summary
|
|
4465
|
+
await _testsWaitForText('Host'); // described hash → its fields, by name
|
|
4466
|
+
await _testsWaitForTextToNotExist('3 fields');
|
|
4467
|
+
|
|
4468
|
+
// Described LIST of hashes: numbered items, labels by display_name, and the
|
|
4469
|
+
// allowed value read back as its label rather than the stored `get`.
|
|
4470
|
+
await _testsWaitForText('Path');
|
|
4471
|
+
await _testsWaitForText('/orders');
|
|
4472
|
+
await _testsWaitForText('GET');
|
|
4473
|
+
// A code sub-field renders AS code, not as a flattened mono line.
|
|
4474
|
+
await _testsWaitForText('Handler');
|
|
4475
|
+
await _testsWaitForText(/sub get_orders/);
|
|
4416
4476
|
await _testsWaitForText('order-to-invoice'); // interface reference → raw value
|
|
4417
4477
|
|
|
4418
4478
|
// Field stack (merged from dpql): byte-size shows its value string; the
|
|
@@ -5915,3 +5975,243 @@ export const CompactRowMarkdownWithoutRenderer: Story = {
|
|
|
5915
5975
|
expect(descText).not.toContain('**');
|
|
5916
5976
|
},
|
|
5917
5977
|
};
|
|
5978
|
+
|
|
5979
|
+
/**
|
|
5980
|
+
* The auth-profile scheme sub-schema, which is where all three of the list-row
|
|
5981
|
+
* affordances below were reported. One required choice with named allowed
|
|
5982
|
+
* values, one field that belongs to a single choice.
|
|
5983
|
+
*/
|
|
5984
|
+
const AuthSchemeArgSchema = {
|
|
5985
|
+
type: {
|
|
5986
|
+
type: 'string',
|
|
5987
|
+
ui_type: 'string',
|
|
5988
|
+
display_name: 'Scheme Type',
|
|
5989
|
+
short_desc: 'Authentication scheme type',
|
|
5990
|
+
required: true,
|
|
5991
|
+
allowed_values: [
|
|
5992
|
+
{ value: 'default', display_name: 'Default RBAC' },
|
|
5993
|
+
{ value: 'cookie', display_name: 'Cookie' },
|
|
5994
|
+
{ value: 'oauth2', display_name: 'OAuth2' },
|
|
5995
|
+
],
|
|
5996
|
+
},
|
|
5997
|
+
cookie_name: {
|
|
5998
|
+
type: 'string',
|
|
5999
|
+
ui_type: 'string',
|
|
6000
|
+
display_name: 'Session Cookie Name',
|
|
6001
|
+
short_desc: 'Applies to the Cookie scheme alone',
|
|
6002
|
+
depends_on: ['type=cookie'],
|
|
6003
|
+
},
|
|
6004
|
+
} as unknown as IOptionsSchema;
|
|
6005
|
+
|
|
6006
|
+
const AuthSchemeOptions = {
|
|
6007
|
+
schemes: {
|
|
6008
|
+
type: 'list',
|
|
6009
|
+
ui_type: 'list',
|
|
6010
|
+
element_type: 'hash',
|
|
6011
|
+
display_name: 'Authentication Schemes',
|
|
6012
|
+
short_desc: 'Schemes tried in order, first match wins',
|
|
6013
|
+
required: true,
|
|
6014
|
+
arg_schema: AuthSchemeArgSchema,
|
|
6015
|
+
},
|
|
6016
|
+
} as unknown as IOptionsSchema;
|
|
6017
|
+
|
|
6018
|
+
/**
|
|
6019
|
+
* A list-of-hash row reads back in the words the form asked for the value.
|
|
6020
|
+
*
|
|
6021
|
+
* Reported on an auth profile: the row summarised as "2 items" and its preview
|
|
6022
|
+
* printed a data tree — "Object · 1 field" over `type: default`. Both are
|
|
6023
|
+
* exactly what is stored, and neither is a string the author has ever seen: the
|
|
6024
|
+
* form calls that key "Scheme Type" and that value "Default RBAC". A row meant
|
|
6025
|
+
* to confirm a choice showed a value nobody had chosen, in a shape nobody had
|
|
6026
|
+
* asked about.
|
|
6027
|
+
*
|
|
6028
|
+
* A generic data view has to announce what it found, because inference is all it
|
|
6029
|
+
* has. A field with an `arg_schema` needs none of that — the shape, the names and
|
|
6030
|
+
* the choices are known before the value arrives — so the preview renders
|
|
6031
|
+
* THROUGH the schema (`SchemaDataView`) and the summary resolves through the
|
|
6032
|
+
* same one. The row, the preview and the editor cannot describe a value three
|
|
6033
|
+
* ways.
|
|
6034
|
+
*/
|
|
6035
|
+
export const CompactListOfHashReadsInSchemaWords: Story = {
|
|
6036
|
+
parameters: {
|
|
6037
|
+
docs: {
|
|
6038
|
+
description: {
|
|
6039
|
+
story:
|
|
6040
|
+
'A list-of-hash option whose sub-schema names its fields and values. The collapsed row summarises by those names ("Default RBAC, Cookie") and the preview renders as labelled schema rows — "Scheme Type · Default RBAC" per numbered item — instead of an untyped data tree over the stored `type: default`.',
|
|
6041
|
+
},
|
|
6042
|
+
},
|
|
6043
|
+
},
|
|
6044
|
+
args: {
|
|
6045
|
+
compact: true,
|
|
6046
|
+
minColumnWidth: '300px',
|
|
6047
|
+
options: AuthSchemeOptions,
|
|
6048
|
+
value: {
|
|
6049
|
+
schemes: {
|
|
6050
|
+
type: 'list',
|
|
6051
|
+
value: [
|
|
6052
|
+
{ type: 'hash', value: { type: 'default' } },
|
|
6053
|
+
{ type: 'hash', value: { type: 'cookie', cookie_name: 'qorus-session' } },
|
|
6054
|
+
],
|
|
6055
|
+
},
|
|
6056
|
+
},
|
|
6057
|
+
},
|
|
6058
|
+
play: async ({ canvasElement }) => {
|
|
6059
|
+
// The preview IS the value here — the row prints no summary line above it.
|
|
6060
|
+
// A joined "Default RBAC, Cookie" directly over a preview that names both
|
|
6061
|
+
// items is the same facts twice, the lossy version first.
|
|
6062
|
+
const preview = await waitFor(() => {
|
|
6063
|
+
const element = canvasElement.querySelector('.schema-data-view');
|
|
6064
|
+
expect(element).toBeTruthy();
|
|
6065
|
+
return element!;
|
|
6066
|
+
});
|
|
6067
|
+
const text = preview.textContent ?? '';
|
|
6068
|
+
expect(text).toContain('Scheme Type');
|
|
6069
|
+
expect(text).toContain('Default RBAC');
|
|
6070
|
+
expect(text).toContain('Session Cookie Name');
|
|
6071
|
+
|
|
6072
|
+
// Nothing announces the container's shape — that is the tell of a renderer
|
|
6073
|
+
// guessing at data it has not been told about.
|
|
6074
|
+
expect(text).not.toMatch(/Object\b/);
|
|
6075
|
+
expect(text).not.toMatch(/\d+ fields?\b/);
|
|
6076
|
+
|
|
6077
|
+
// The joined summary is gone from the row, and it is the ROW that has to be
|
|
6078
|
+
// checked: the same words still exist inside the preview, so asserting on
|
|
6079
|
+
// the whole canvas would pass whether or not the line was removed.
|
|
6080
|
+
const valueLine = canvasElement.querySelector(
|
|
6081
|
+
'[data-field="schemes"] .options-readfirst-valuetext'
|
|
6082
|
+
);
|
|
6083
|
+
expect(valueLine?.textContent ?? '').not.toBe('Default RBAC, Cookie');
|
|
6084
|
+
|
|
6085
|
+
// A literal the author typed is set in mono; a chosen label is not.
|
|
6086
|
+
const mono = [...preview.querySelectorAll('.schema-view-data')].map((el) =>
|
|
6087
|
+
(el.textContent ?? '').trim()
|
|
6088
|
+
);
|
|
6089
|
+
expect(mono).toContain('qorus-session');
|
|
6090
|
+
expect(mono).not.toContain('Default RBAC');
|
|
6091
|
+
|
|
6092
|
+
// The stored spellings are what the author never chose, so they must not be
|
|
6093
|
+
// in the preview at all. Substring checks cannot say this — "Scheme Type"
|
|
6094
|
+
// contains "type" and "Default RBAC" contains "default" — so the assertion
|
|
6095
|
+
// is on whole leaf elements: no chip or cell reads exactly `type`,
|
|
6096
|
+
// `cookie_name` or `default`. A substring assertion here passed against a
|
|
6097
|
+
// preview that was still rendering the raw pair.
|
|
6098
|
+
const leafTexts = [...preview.querySelectorAll('*')]
|
|
6099
|
+
.filter((element) => element.children.length === 0)
|
|
6100
|
+
.map((element) => (element.textContent ?? '').trim());
|
|
6101
|
+
expect(leafTexts).toContain('Scheme Type');
|
|
6102
|
+
for (const stored of ['type', 'cookie_name', 'default']) {
|
|
6103
|
+
expect(leafTexts).not.toContain(stored);
|
|
6104
|
+
}
|
|
6105
|
+
expect(canvasElement.textContent ?? '').not.toContain('[object Object]');
|
|
6106
|
+
},
|
|
6107
|
+
};
|
|
6108
|
+
|
|
6109
|
+
/**
|
|
6110
|
+
* Adding a list item opens the field the item cannot be saved without.
|
|
6111
|
+
*
|
|
6112
|
+
* `+ Add new item for "Authentication Scheme"` added a row whose one required
|
|
6113
|
+
* field sat collapsed, so the author had to find it and click it before the
|
|
6114
|
+
* form could be completed — a second click to reach the only thing the first
|
|
6115
|
+
* click could have meant.
|
|
6116
|
+
*
|
|
6117
|
+
* The engine already had `autoFocusFirstRequired`, and it could not fire here:
|
|
6118
|
+
* it waits for focus to be free so it never steals a caret, and a form mounted
|
|
6119
|
+
* BY a click never sees free focus — the button that mounted it still holds it.
|
|
6120
|
+
* Opening a row and moving the caret are separate decisions, so they are now
|
|
6121
|
+
* separate flags; this one only opens.
|
|
6122
|
+
*/
|
|
6123
|
+
export const CompactAddedListRowOpensItsRequiredField: Story = {
|
|
6124
|
+
parameters: {
|
|
6125
|
+
docs: {
|
|
6126
|
+
description: {
|
|
6127
|
+
story:
|
|
6128
|
+
'Clicks "Add new item" on a list-of-hash option and shows the new row with its required "Scheme Type" field already open. Rows that were already in the value stay collapsed — only the row just added is opened.',
|
|
6129
|
+
},
|
|
6130
|
+
},
|
|
6131
|
+
},
|
|
6132
|
+
args: {
|
|
6133
|
+
compact: true,
|
|
6134
|
+
minColumnWidth: '300px',
|
|
6135
|
+
options: AuthSchemeOptions,
|
|
6136
|
+
value: {
|
|
6137
|
+
schemes: {
|
|
6138
|
+
type: 'list',
|
|
6139
|
+
value: [{ type: 'hash', value: { type: 'default' } }],
|
|
6140
|
+
},
|
|
6141
|
+
},
|
|
6142
|
+
},
|
|
6143
|
+
play: async ({ canvasElement }) => {
|
|
6144
|
+
// Open the list itself, which mounts the rows and the Add button. Clicked by
|
|
6145
|
+
// FIELD, not by its text: the row no longer prints a summary line, and the
|
|
6146
|
+
// words that remain live inside the preview.
|
|
6147
|
+
await _testsClickText('Authentication Schemes');
|
|
6148
|
+
await waitFor(() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(1));
|
|
6149
|
+
|
|
6150
|
+
// The row that was already there is collapsed — this is the state the new
|
|
6151
|
+
// row must NOT be confused with.
|
|
6152
|
+
const isOpen = (element: Element | null | undefined) =>
|
|
6153
|
+
!!element &&
|
|
6154
|
+
(element.classList.contains('readfirst-row-editing') ||
|
|
6155
|
+
element.classList.contains('options-readfirst-card'));
|
|
6156
|
+
const typeRows = () => canvasElement.querySelectorAll('[data-field="type"]');
|
|
6157
|
+
expect([...typeRows()].some(isOpen)).toBe(false);
|
|
6158
|
+
|
|
6159
|
+
await _testsClickButton({ label: 'Add new item for "Authentication Schemes"' });
|
|
6160
|
+
|
|
6161
|
+
await waitFor(() => expect(canvasElement.querySelectorAll('.array-auto-item')).toHaveLength(2));
|
|
6162
|
+
// Exactly one open required field: the one belonging to the row just added.
|
|
6163
|
+
await waitFor(() => expect([...typeRows()].filter(isOpen)).toHaveLength(1));
|
|
6164
|
+
},
|
|
6165
|
+
};
|
|
6166
|
+
|
|
6167
|
+
/**
|
|
6168
|
+
* The same schema-worded row at a phone-class width.
|
|
6169
|
+
*
|
|
6170
|
+
* The narrow branch is what makes this worth its own story: below 480px the row
|
|
6171
|
+
* stacks the value under the label, so a summary and a preview that were both
|
|
6172
|
+
* rewritten to be READ (rather than decoded) have to survive the stacking
|
|
6173
|
+
* without wrapping into an unreadable column. `compactNarrow` comes from
|
|
6174
|
+
* `useMeasure` on the form's own wrapper, not from a media query, so a narrow
|
|
6175
|
+
* container is the honest way to reach the branch here — the viewport-parameter
|
|
6176
|
+
* rule applies to media-query components, which this is not.
|
|
6177
|
+
*/
|
|
6178
|
+
export const CompactListOfHashReadsInSchemaWordsMobile: Story = {
|
|
6179
|
+
parameters: {
|
|
6180
|
+
docs: {
|
|
6181
|
+
description: {
|
|
6182
|
+
story:
|
|
6183
|
+
'The schema-worded list-of-hash row at a ~360px width: the summary and the renamed preview stack under the field name and stay legible in one column.',
|
|
6184
|
+
},
|
|
6185
|
+
},
|
|
6186
|
+
},
|
|
6187
|
+
args: CompactListOfHashReadsInSchemaWords.args,
|
|
6188
|
+
decorators: [
|
|
6189
|
+
(StoryComponent: React.ComponentType) => (
|
|
6190
|
+
<div style={{ maxWidth: 360, margin: '0 auto', border: '1px dashed #ffffff22' }}>
|
|
6191
|
+
<StoryComponent />
|
|
6192
|
+
</div>
|
|
6193
|
+
),
|
|
6194
|
+
],
|
|
6195
|
+
play: async ({ canvasElement }) => {
|
|
6196
|
+
await _testsWaitForText('Scheme Type');
|
|
6197
|
+
|
|
6198
|
+
// The narrow branch is actually engaged — otherwise this is the desktop
|
|
6199
|
+
// story with a border round it, and it would pass while proving nothing.
|
|
6200
|
+
await waitFor(() =>
|
|
6201
|
+
expect(canvasElement.querySelector('.readfirst-narrow')).toBeTruthy()
|
|
6202
|
+
);
|
|
6203
|
+
|
|
6204
|
+
const preview = await waitFor(() => {
|
|
6205
|
+
const element = canvasElement.querySelector('.schema-data-view');
|
|
6206
|
+
expect(element).toBeTruthy();
|
|
6207
|
+
return element!;
|
|
6208
|
+
});
|
|
6209
|
+
const leafTexts = [...preview.querySelectorAll('*')]
|
|
6210
|
+
.filter((element) => element.children.length === 0)
|
|
6211
|
+
.map((element) => (element.textContent ?? '').trim());
|
|
6212
|
+
expect(leafTexts).toContain('Scheme Type');
|
|
6213
|
+
for (const stored of ['type', 'cookie_name', 'default']) {
|
|
6214
|
+
expect(leafTexts).not.toContain(stored);
|
|
6215
|
+
}
|
|
6216
|
+
},
|
|
6217
|
+
};
|
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
108
108
|
import {
|
|
109
109
|
TReadFirstStatus,
|
|
110
|
+
findAllowedValueOption,
|
|
110
111
|
getFirstAttentionOptionName,
|
|
111
112
|
getOptionGroup,
|
|
112
113
|
getOptionGroupLabel,
|
|
@@ -429,13 +430,18 @@ export const fixOptions = (
|
|
|
429
430
|
};
|
|
430
431
|
}
|
|
431
432
|
|
|
433
|
+
// A value that is not one of the declared choices is dropped. What counts
|
|
434
|
+
// as "one of the choices" is `findAllowedValueOption` — the same predicate
|
|
435
|
+
// the read-first row uses to LABEL a value — because a value the row can
|
|
436
|
+
// name is by definition a value the form must keep. Inlining a narrower
|
|
437
|
+
// test here (envelope and `name`, but not a bare `value`) silently erased
|
|
438
|
+
// every value declared the bare way: the collapsed row still showed its
|
|
439
|
+
// display name while the editor showed "—" and the value never reached
|
|
440
|
+
// the submitted data.
|
|
432
441
|
if (
|
|
433
442
|
newOption.value !== undefined &&
|
|
434
443
|
options?.[optionName]?.allowed_values &&
|
|
435
|
-
!options?.[optionName]
|
|
436
|
-
(allowedValue: any) =>
|
|
437
|
-
allowedValue.value?.value === newOption.value || allowedValue.name === newOption.value
|
|
438
|
-
) &&
|
|
444
|
+
!findAllowedValueOption(newOption.value, options?.[optionName]) &&
|
|
439
445
|
!isValueTemplate(newOption.value) &&
|
|
440
446
|
!options?.[optionName]?.multiselect &&
|
|
441
447
|
!options?.[optionName]?.allowed_values_creatable
|
|
@@ -715,6 +721,27 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
715
721
|
*/
|
|
716
722
|
autoFocusFirstRequired?: boolean;
|
|
717
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Opt-in: on mount, OPEN the first row that needs attention — without taking
|
|
726
|
+
* focus.
|
|
727
|
+
*
|
|
728
|
+
* `autoFocusFirstRequired` fuses two decisions that are not the same: which
|
|
729
|
+
* row is open (layout) and where the caret is (focus). Focus is the half that
|
|
730
|
+
* can be stolen, so that flag waits for focus to be free — and a form mounted
|
|
731
|
+
* BY a click never sees free focus, because the button that mounted it still
|
|
732
|
+
* has it. Adding a list item is exactly that case: the row the author must
|
|
733
|
+
* fill stayed shut behind a second click, and the flag that exists to prevent
|
|
734
|
+
* it could not fire.
|
|
735
|
+
*
|
|
736
|
+
* Opening a row takes nothing from the user, so this half needs no guard. Use
|
|
737
|
+
* it where the mount is already the answer to a deliberate action; use
|
|
738
|
+
* `autoFocusFirstRequired` where the form is the destination and the caret
|
|
739
|
+
* should land in it. Setting both keeps the focusing behaviour. Same one-shot
|
|
740
|
+
* contract: it fires on the first render with focusable content and then never
|
|
741
|
+
* again for the life of the instance. No-op in classic mode. Default: off.
|
|
742
|
+
*/
|
|
743
|
+
expandFirstRequired?: boolean;
|
|
744
|
+
|
|
718
745
|
/**
|
|
719
746
|
* Names of read-first rows to open on mount, in addition to whatever the
|
|
720
747
|
* user opens afterwards.
|
|
@@ -783,6 +810,7 @@ const FormEngineImpl = ({
|
|
|
783
810
|
rendererOnlyUiTypes,
|
|
784
811
|
inheritedFromParent,
|
|
785
812
|
autoFocusFirstRequired,
|
|
813
|
+
expandFirstRequired,
|
|
786
814
|
initialExpandedOptions,
|
|
787
815
|
...rest
|
|
788
816
|
}: IFormEngineProps) => {
|
|
@@ -831,6 +859,47 @@ const FormEngineImpl = ({
|
|
|
831
859
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
832
860
|
// Which options are expanded into their editor (several can be open at once).
|
|
833
861
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
862
|
+
|
|
863
|
+
// --- Reveal a status box's content when it is opened ----------------------
|
|
864
|
+
//
|
|
865
|
+
// The status boxes stack, so the last one ("Optional", which holds every
|
|
866
|
+
// not-yet-added field) sits at the bottom of the form — frequently at the
|
|
867
|
+
// bottom of the scroll container too. Opening it mounts its rows BELOW the
|
|
868
|
+
// fold: the box's own header is all that stays on screen, the click appears to
|
|
869
|
+
// have done nothing, and the fields it just revealed have to be hunted for by
|
|
870
|
+
// scrolling.
|
|
871
|
+
//
|
|
872
|
+
// The box that was just opened, read by the layout effect below. Held as state
|
|
873
|
+
// rather than acted on inside the collapse handler because the handler runs
|
|
874
|
+
// BEFORE React re-renders — at that moment the rows do not exist yet, so the
|
|
875
|
+
// box has not grown and there is nothing to scroll to. A layout effect runs
|
|
876
|
+
// after the DOM is updated and before paint, which makes the reveal
|
|
877
|
+
// deterministic instead of a guessed delay.
|
|
878
|
+
const [openedStatusBox, setOpenedStatusBox] = useState<string | undefined>(undefined);
|
|
879
|
+
const statusBoxRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
880
|
+
useLayoutEffect(() => {
|
|
881
|
+
if (!openedStatusBox) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const element = statusBoxRefs.current[openedStatusBox];
|
|
885
|
+
// Consume it either way: a box that has gone (a search narrowed it out of
|
|
886
|
+
// existence) must not leave a pending reveal armed for a later render.
|
|
887
|
+
setOpenedStatusBox(undefined);
|
|
888
|
+
// `scrollIntoView` is not implemented in jsdom, and a missing reveal must
|
|
889
|
+
// never break a form.
|
|
890
|
+
if (!element?.scrollIntoView) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
// 'nearest' scrolls the MINIMUM needed: nothing at all when the box is
|
|
894
|
+
// already fully visible, and a top-alignment when it is taller than the
|
|
895
|
+
// viewport (header plus the first rows — the right answer for a long
|
|
896
|
+
// Optional list). Anything else moves the page when it did not need to.
|
|
897
|
+
element.scrollIntoView({
|
|
898
|
+
block: 'nearest',
|
|
899
|
+
behavior:
|
|
900
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth',
|
|
901
|
+
});
|
|
902
|
+
}, [openedStatusBox]);
|
|
834
903
|
// Remembers each row's last settled status box, so an actively-edited field
|
|
835
904
|
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
836
905
|
// another box mid-edit and stealing focus. Keyed by option name.
|
|
@@ -1863,7 +1932,11 @@ const FormEngineImpl = ({
|
|
|
1863
1932
|
// (or below-the-fold) form is never scrolled into view on mount.
|
|
1864
1933
|
const autoFocusNameRef = useRef<string | undefined>(undefined);
|
|
1865
1934
|
useEffect(() => {
|
|
1866
|
-
|
|
1935
|
+
// Two decisions, one scan. `autoFocusFirstRequired` opens the row AND puts
|
|
1936
|
+
// the caret in it; `expandFirstRequired` only opens it. Setting both keeps
|
|
1937
|
+
// the focusing behaviour, since focusing implies opening.
|
|
1938
|
+
const wantsFocus = !!autoFocusFirstRequired;
|
|
1939
|
+
if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
|
|
1867
1940
|
return;
|
|
1868
1941
|
}
|
|
1869
1942
|
if (hasAutoFocusedRef.current) {
|
|
@@ -1881,8 +1954,14 @@ const FormEngineImpl = ({
|
|
|
1881
1954
|
// clean mount focus rests on the body, so the intended "drop into the first
|
|
1882
1955
|
// field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
|
|
1883
1956
|
// retries once focus is free.
|
|
1957
|
+
//
|
|
1958
|
+
// Only the FOCUSING variant waits: opening a row moves no caret, so there is
|
|
1959
|
+
// nothing to steal and nothing to wait for. Making the expand-only variant
|
|
1960
|
+
// wait here would make it a no-op in its main use — a sub-form mounted by a
|
|
1961
|
+
// click, where the button that mounted it still holds focus and never gives
|
|
1962
|
+
// it up.
|
|
1884
1963
|
const active = document.activeElement as HTMLElement | null;
|
|
1885
|
-
if (active && active !== document.body) {
|
|
1964
|
+
if (wantsFocus && active && active !== document.body) {
|
|
1886
1965
|
return;
|
|
1887
1966
|
}
|
|
1888
1967
|
|
|
@@ -1910,7 +1989,10 @@ const FormEngineImpl = ({
|
|
|
1910
1989
|
|
|
1911
1990
|
if (target) {
|
|
1912
1991
|
// Set the ref before the state update so CompactRow's focus timer sees it.
|
|
1913
|
-
|
|
1992
|
+
// Expand-only leaves it unset, which is what keeps the caret where it is.
|
|
1993
|
+
if (wantsFocus) {
|
|
1994
|
+
autoFocusNameRef.current = target;
|
|
1995
|
+
}
|
|
1914
1996
|
setExpandedOptions((prev) =>
|
|
1915
1997
|
prev.includes(target) ? prev
|
|
1916
1998
|
: expandMode === 'multi' ? [...prev, target]
|
|
@@ -1919,6 +2001,7 @@ const FormEngineImpl = ({
|
|
|
1919
2001
|
}
|
|
1920
2002
|
}, [
|
|
1921
2003
|
autoFocusFirstRequired,
|
|
2004
|
+
expandFirstRequired,
|
|
1922
2005
|
compact,
|
|
1923
2006
|
options,
|
|
1924
2007
|
availableOptions,
|
|
@@ -2783,6 +2866,14 @@ const FormEngineImpl = ({
|
|
|
2783
2866
|
$accent={accent}
|
|
2784
2867
|
$bg={boxBg}
|
|
2785
2868
|
key={box.key}
|
|
2869
|
+
ref={(node: HTMLDivElement | null) => {
|
|
2870
|
+
statusBoxRefs.current[box.key] = node;
|
|
2871
|
+
}}
|
|
2872
|
+
// Opening arms the reveal; collapsing disarms it, so a
|
|
2873
|
+
// close never scrolls.
|
|
2874
|
+
onCollapseChange={(collapsed) =>
|
|
2875
|
+
setOpenedStatusBox(collapsed ? undefined : box.key)
|
|
2876
|
+
}
|
|
2786
2877
|
flat
|
|
2787
2878
|
minimal
|
|
2788
2879
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|