@qoretechnologies/reqraft 0.10.41-pr.95.g294e5aa → 0.10.41
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/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/codeSize/index.d.ts +37 -0
- package/dist/components/codeSize/index.d.ts.map +1 -0
- package/dist/components/codeSize/index.js +87 -0
- package/dist/components/codeSize/index.js.map +1 -0
- package/dist/components/form/engine/CompactRow.d.ts +20 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +284 -42
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +72 -72
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +163 -75
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +102 -87
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +22 -5
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +57 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +143 -4
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +1 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +44 -26
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +32 -4
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +25 -4
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
- package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
- package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
- package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +35 -6
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/helpers/common.d.ts +8 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +9 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +148 -0
- package/dist/helpers/optionUiTypes.js.map +1 -0
- package/dist/helpers/options.d.ts +19 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +25 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/providerValue.d.ts +68 -0
- package/dist/helpers/providerValue.d.ts.map +1 -0
- package/dist/helpers/providerValue.js +50 -0
- package/dist/helpers/providerValue.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts +37 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -0
- package/dist/helpers/singleLineString.js +66 -0
- package/dist/helpers/singleLineString.js.map +1 -0
- package/dist/helpers/templates.d.ts +100 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +237 -4
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +103 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +858 -91
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/codeSize/index.tsx +79 -0
- package/src/components/form/engine/CompactRow.tsx +407 -34
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +863 -12
- package/src/components/form/engine/FormEngine.tsx +183 -33
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +110 -103
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +132 -80
- package/src/components/form/engine/compactRowStyles.ts +44 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +163 -4
- package/src/components/form/fields/Field.tsx +3 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
- package/src/components/form/fields/array/ArrayAuto.tsx +23 -4
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
- package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
- package/src/components/form/fields/auto/AutoFormField.tsx +31 -1
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
- package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
- package/src/components/form/fields/template/TemplateField.tsx +47 -8
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
- package/src/components/form/index.tsx +1 -0
- package/src/helpers/common.ts +11 -1
- package/src/helpers/optionUiTypes.ts +153 -0
- package/src/helpers/options.ts +23 -0
- package/src/helpers/providerValue.ts +125 -0
- package/src/helpers/singleLineString.ts +61 -0
- package/src/helpers/templates.ts +272 -3
- package/src/helpers/validations.ts +1177 -109
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
- package/src/index.tsx +2 -0
|
@@ -43,15 +43,28 @@ export const FieldAllowedValuesCheckGroup = memo(
|
|
|
43
43
|
onChange,
|
|
44
44
|
name,
|
|
45
45
|
value,
|
|
46
|
+
label,
|
|
46
47
|
...rest
|
|
47
48
|
}: Partial<Omit<IFieldAllowedValuesProps, 'items'>> & {
|
|
48
49
|
multiSelect?: boolean;
|
|
49
50
|
items: ISelectFormFieldItem[];
|
|
51
|
+
/**
|
|
52
|
+
* The field's own name, used as the group's heading.
|
|
53
|
+
*
|
|
54
|
+
* A picker headed by what it IS beats one headed by a generic instruction:
|
|
55
|
+
* "Select one:" says nothing the shape of a radio group has not already
|
|
56
|
+
* said, and in a container that absorbs a sibling it forced the field's name
|
|
57
|
+
* to be printed a second time beside the group. Falls back to the
|
|
58
|
+
* instruction when a field declares no display name.
|
|
59
|
+
*/
|
|
60
|
+
label?: string;
|
|
50
61
|
}) => {
|
|
51
62
|
return (
|
|
52
63
|
<ReqoreControlGroup vertical size={rest.size || 'small'} gapSize='tiny'>
|
|
53
64
|
<ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
|
|
54
|
-
{
|
|
65
|
+
{label ? `${label}:`
|
|
66
|
+
: multiSelect ? 'Select one or more:'
|
|
67
|
+
: 'Select one:'}
|
|
55
68
|
</ReqoreSpan>
|
|
56
69
|
{items?.map((item) => (
|
|
57
70
|
<ReqoreCheckbox
|
|
@@ -62,10 +75,17 @@ export const FieldAllowedValuesCheckGroup = memo(
|
|
|
62
75
|
disabled={rest.disabled}
|
|
63
76
|
readOnly={rest.readOnly}
|
|
64
77
|
intent={item.value === value ? 'info' : undefined}
|
|
78
|
+
// `isEqual`, and only once there is a value to match. The single
|
|
79
|
+
// -select branch compared JSON.stringify(value) with
|
|
80
|
+
// JSON.stringify(item.value), and JSON.stringify(undefined) is
|
|
81
|
+
// undefined — so an unset field whose items carry no resolved value
|
|
82
|
+
// compared undefined with undefined and reported EVERY option as
|
|
83
|
+
// checked, on a field that was simultaneously "This field is
|
|
84
|
+
// required". Nothing selected must read as nothing selected.
|
|
65
85
|
checked={
|
|
66
86
|
multiSelect
|
|
67
87
|
? Array.isArray(value) && value.some((v) => isEqual(v, item.value))
|
|
68
|
-
:
|
|
88
|
+
: value !== undefined && value !== null && isEqual(value, item.value)
|
|
69
89
|
}
|
|
70
90
|
onClick={() => {
|
|
71
91
|
if (multiSelect) {
|
|
@@ -103,6 +123,7 @@ export const FieldAllowedValues = memo(
|
|
|
103
123
|
disabled,
|
|
104
124
|
name,
|
|
105
125
|
readOnly,
|
|
126
|
+
label,
|
|
106
127
|
}: IFieldAllowedValuesProps) => {
|
|
107
128
|
const fullItems = useMemo(() => {
|
|
108
129
|
const result = [
|
|
@@ -131,6 +152,7 @@ export const FieldAllowedValues = memo(
|
|
|
131
152
|
onChange={onChange}
|
|
132
153
|
value={value}
|
|
133
154
|
name={name}
|
|
155
|
+
label={label}
|
|
134
156
|
/>
|
|
135
157
|
);
|
|
136
158
|
}
|
|
@@ -24,6 +24,7 @@ import { validateFieldWithResult } from '../../../../helpers/validations';
|
|
|
24
24
|
import { useWhyDidYouUpdate } from '../../../../hooks/useWhyDidYouUpdate';
|
|
25
25
|
// Direct imports — the cycle (ArrayAuto → TemplateField/auto → ArrayAuto) is
|
|
26
26
|
// safe the same way Field → AutoFormField → Field is.
|
|
27
|
+
import { recordIdentity } from '../../engine/readFirst';
|
|
27
28
|
import { AutoFormField } from '../auto/AutoFormField';
|
|
28
29
|
import { TemplateField } from '../template/TemplateField';
|
|
29
30
|
|
|
@@ -133,12 +134,29 @@ export const ArrayAuto = ({
|
|
|
133
134
|
// Render list of auto fields
|
|
134
135
|
return (
|
|
135
136
|
<ReqoreControlGroup vertical fluid>
|
|
136
|
-
{map(localValue, (val: string | number, idx: string) =>
|
|
137
|
+
{map(localValue, (val: string | number, idx: string) => {
|
|
138
|
+
// The same field the collapsed preview promotes, resolved by the same
|
|
139
|
+
// definition — see `recordIdentity`. A list of seven methods headed
|
|
140
|
+
// `#1 … #7` makes the reader open each one to find out which is which,
|
|
141
|
+
// and heading them differently in the preview and the editor would make
|
|
142
|
+
// one item answer to two names.
|
|
143
|
+
const identity =
|
|
144
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
145
|
+
? recordIdentity(val as Record<string, unknown>, rest.arg_schema)
|
|
146
|
+
: undefined;
|
|
147
|
+
|
|
148
|
+
return (
|
|
137
149
|
<ReqorePanel
|
|
138
150
|
key={idx}
|
|
139
|
-
|
|
151
|
+
// `#N` stays as the fallback: a row whose identifying field is still
|
|
152
|
+
// empty (one just added) has nothing to be called yet, and a blank
|
|
153
|
+
// heading would be worse than a number.
|
|
154
|
+
label={identity?.text || `#${idx + 1}`}
|
|
155
|
+
tooltip={identity?.label}
|
|
140
156
|
unMountContentOnCollapse={false}
|
|
141
|
-
badge
|
|
157
|
+
// The position moves to the badge once the name owns the heading, so
|
|
158
|
+
// "which of these is third" stays answerable.
|
|
159
|
+
badge={identity ? `#${idx + 1}` : display_name || name}
|
|
142
160
|
collapsible
|
|
143
161
|
responsiveActions={false}
|
|
144
162
|
responsiveTitle={false}
|
|
@@ -188,7 +206,8 @@ export const ArrayAuto = ({
|
|
|
188
206
|
/>
|
|
189
207
|
{showValidationMessage(val)}
|
|
190
208
|
</ReqorePanel>
|
|
191
|
-
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
192
211
|
<ReqoreControlGroup fluid>
|
|
193
212
|
<ReqoreButton
|
|
194
213
|
onClick={addValue}
|
|
@@ -46,6 +46,58 @@ const numberRenderItem: IArrayAutoFieldProps['renderItem'] = ({
|
|
|
46
46
|
/>
|
|
47
47
|
);
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Hash items are records, so a renderer that casts the item to a string prints
|
|
51
|
+
* `[object Object]` and shows nothing at all. This one reads the record through
|
|
52
|
+
* the schema that describes it: one input per declared field, labelled by that
|
|
53
|
+
* field's `display_name`, carrying the value actually stored under its key.
|
|
54
|
+
*
|
|
55
|
+
* With no `arg_schema` there is nothing to name the parts with, so the record is
|
|
56
|
+
* shown as JSON — unhelpful, but at least it is the data.
|
|
57
|
+
*/
|
|
58
|
+
const hashRenderItem: IArrayAutoFieldProps['renderItem'] = ({
|
|
59
|
+
value,
|
|
60
|
+
onChange,
|
|
61
|
+
disabled,
|
|
62
|
+
readOnly,
|
|
63
|
+
size,
|
|
64
|
+
arg_schema,
|
|
65
|
+
}) => {
|
|
66
|
+
const record = (value ?? {}) as Record<string, unknown>;
|
|
67
|
+
const fields = Object.entries(arg_schema ?? {});
|
|
68
|
+
|
|
69
|
+
if (!fields.length) {
|
|
70
|
+
return (
|
|
71
|
+
<StringFormField
|
|
72
|
+
value={JSON.stringify(record)}
|
|
73
|
+
onChange={() => {}}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
readOnly
|
|
76
|
+
size={size as any}
|
|
77
|
+
aria-label='Array item input'
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ReqoreControlGroup vertical fluid gapSize='small'>
|
|
84
|
+
{fields.map(([fieldName, fieldSchema]) => (
|
|
85
|
+
<StringFormField
|
|
86
|
+
key={fieldName}
|
|
87
|
+
value={(record[fieldName] as string) ?? ''}
|
|
88
|
+
onChange={(v) => onChange({ ...record, [fieldName]: v })}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
readOnly={readOnly}
|
|
91
|
+
size={size as any}
|
|
92
|
+
aria-label={
|
|
93
|
+
((fieldSchema as { display_name?: string })?.display_name as string) ?? fieldName
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</ReqoreControlGroup>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
49
101
|
const meta = {
|
|
50
102
|
component: ArrayAutoField,
|
|
51
103
|
title: 'Components/Form/Auto Array',
|
|
@@ -322,6 +374,7 @@ export const ComplexHashItems: Story = {
|
|
|
322
374
|
type: 'hash',
|
|
323
375
|
value: [{ key: 'value' }],
|
|
324
376
|
display_name: 'Hash Items',
|
|
377
|
+
renderItem: hashRenderItem,
|
|
325
378
|
},
|
|
326
379
|
parameters: {
|
|
327
380
|
docs: {
|
|
@@ -339,3 +392,84 @@ export const ComplexHashItems: Story = {
|
|
|
339
392
|
await expect(document.querySelectorAll('.array-auto-compact-tag').length).toBe(0);
|
|
340
393
|
},
|
|
341
394
|
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The editable list, headed by what identifies each record.
|
|
398
|
+
*
|
|
399
|
+
* The rows used to read `#1 #2 #3`, so finding one method meant opening each in
|
|
400
|
+
* turn — and the collapsed preview beside it already headed the same records
|
|
401
|
+
* `init` / `onOrderStatus`, which made one item answer to two names. Both now
|
|
402
|
+
* resolve the heading through `recordIdentity`: the first field the SCHEMA
|
|
403
|
+
* declares that holds a scalar, skipping any it leaves unset. The position keeps
|
|
404
|
+
* a badge, so "the third one" is still answerable.
|
|
405
|
+
*/
|
|
406
|
+
export const HashItemsHeadedByIdentity: Story = {
|
|
407
|
+
args: {
|
|
408
|
+
type: 'hash',
|
|
409
|
+
display_name: 'Service Methods',
|
|
410
|
+
arg_schema: {
|
|
411
|
+
name: { type: 'string', ui_type: 'string', display_name: 'Method Name', required: true },
|
|
412
|
+
description: { type: 'string', ui_type: 'string', display_name: 'Description' },
|
|
413
|
+
},
|
|
414
|
+
value: [
|
|
415
|
+
{ name: 'init', description: 'The init method' },
|
|
416
|
+
{ name: 'onOrderStatus', description: 'Handles order status callbacks' },
|
|
417
|
+
{ description: 'Added but not yet named' },
|
|
418
|
+
{},
|
|
419
|
+
],
|
|
420
|
+
renderItem: hashRenderItem,
|
|
421
|
+
},
|
|
422
|
+
parameters: {
|
|
423
|
+
docs: {
|
|
424
|
+
description: {
|
|
425
|
+
story:
|
|
426
|
+
'Renders four hash items headed by their identifying value: two named methods read as "init" and "onOrderStatus", one with no name falls through to its description, and an entirely empty one keeps its position number.',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
async play() {
|
|
431
|
+
await waitFor(() => expect(document.querySelectorAll('.array-auto-item').length).toBe(4), {
|
|
432
|
+
timeout: 5000,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const headings = [...document.querySelectorAll('.array-auto-item')].map(
|
|
436
|
+
(item) => item.textContent ?? ''
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await expect(headings[0]).toContain('init');
|
|
440
|
+
await expect(headings[1]).toContain('onOrderStatus');
|
|
441
|
+
// An unset field is skipped rather than promoted blank, so this one is
|
|
442
|
+
// headed by the next field that IS set.
|
|
443
|
+
await expect(headings[2]).toContain('Added but not yet named');
|
|
444
|
+
// And a record with nothing set at all keeps its position — there is
|
|
445
|
+
// genuinely nothing to call it.
|
|
446
|
+
await expect(headings[3]).toContain('#4');
|
|
447
|
+
|
|
448
|
+
// The heading is only half the story: each item must also render the record
|
|
449
|
+
// it is heading, field by field. Asserting the inputs is what would have
|
|
450
|
+
// caught them all printing "[object Object]" — the heading resolved
|
|
451
|
+
// correctly while every value below it was a stringified hash.
|
|
452
|
+
const nameInputs = document.querySelectorAll<HTMLInputElement>(
|
|
453
|
+
'input[aria-label="Method Name"]'
|
|
454
|
+
);
|
|
455
|
+
const descInputs = document.querySelectorAll<HTMLInputElement>(
|
|
456
|
+
'input[aria-label="Description"]'
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
await expect(nameInputs.length).toBe(4);
|
|
460
|
+
await expect(descInputs.length).toBe(4);
|
|
461
|
+
await expect(nameInputs[0].value).toBe('init');
|
|
462
|
+
await expect(descInputs[0].value).toBe('The init method');
|
|
463
|
+
await expect(nameInputs[1].value).toBe('onOrderStatus');
|
|
464
|
+
// The unnamed record: no name to show, but the description it IS headed by
|
|
465
|
+
// has to be there.
|
|
466
|
+
await expect(nameInputs[2].value).toBe('');
|
|
467
|
+
await expect(descInputs[2].value).toBe('Added but not yet named');
|
|
468
|
+
// And the empty one renders empty inputs rather than an empty item.
|
|
469
|
+
await expect(nameInputs[3].value).toBe('');
|
|
470
|
+
await expect(descInputs[3].value).toBe('');
|
|
471
|
+
|
|
472
|
+
// Nothing anywhere may read as a stringified record.
|
|
473
|
+
await expect(document.body.textContent).not.toContain('[object Object]');
|
|
474
|
+
},
|
|
475
|
+
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@qoretechnologies/reqore';
|
|
10
10
|
import { IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
11
11
|
import { map, size } from 'lodash';
|
|
12
|
+
import { recordIdentity } from '../../engine/readFirst';
|
|
12
13
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
13
14
|
import { useDebounce } from 'react-use';
|
|
14
15
|
import { validateFieldWithResult } from '../../../../helpers/validations';
|
|
@@ -279,11 +280,27 @@ export const ArrayAutoField = memo(
|
|
|
279
280
|
|
|
280
281
|
return (
|
|
281
282
|
<ReqoreControlGroup vertical fluid>
|
|
282
|
-
{map(localValue, (val, idx) =>
|
|
283
|
+
{map(localValue, (val, idx) => {
|
|
284
|
+
// The same field the collapsed preview promotes, resolved by the same
|
|
285
|
+
// definition — see `recordIdentity`. Both editable list renderers head
|
|
286
|
+
// their records with it, so an item cannot be called `init` in one
|
|
287
|
+
// place and `#1` in another.
|
|
288
|
+
const identity =
|
|
289
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
290
|
+
? recordIdentity(val as Record<string, unknown>, arg_schema)
|
|
291
|
+
: undefined;
|
|
292
|
+
|
|
293
|
+
return (
|
|
283
294
|
<ReqorePanel
|
|
284
295
|
key={idx}
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
// `#N` stays as the fallback: a record whose identifying field is
|
|
297
|
+
// still empty has nothing to be called yet, and a blank heading
|
|
298
|
+
// would be worse than a number.
|
|
299
|
+
label={identity?.text || `#${idx + 1}`}
|
|
300
|
+
tooltip={identity?.label}
|
|
301
|
+
// The position moves to the badge once the name owns the heading,
|
|
302
|
+
// so "which of these is third" stays answerable.
|
|
303
|
+
badge={identity ? `#${idx + 1}` : display_name || name}
|
|
287
304
|
collapsible
|
|
288
305
|
unMountContentOnCollapse={false}
|
|
289
306
|
responsiveActions={false}
|
|
@@ -321,7 +338,8 @@ export const ArrayAutoField = memo(
|
|
|
321
338
|
})}
|
|
322
339
|
{renderValidationMessage(val)}
|
|
323
340
|
</ReqorePanel>
|
|
324
|
-
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
325
343
|
|
|
326
344
|
<ReqoreButton
|
|
327
345
|
onClick={addItem}
|
|
@@ -18,6 +18,7 @@ import React, { memo, useCallback, useEffect, useState } from 'react';
|
|
|
18
18
|
import { useUpdateEffect } from 'react-use';
|
|
19
19
|
import useMount from 'react-use/lib/useMount';
|
|
20
20
|
import { typedToYaml, yamlToTyped } from '../../../../helpers/common';
|
|
21
|
+
import { getListElementValue } from '../../../../helpers/options';
|
|
21
22
|
import {
|
|
22
23
|
getTypeFromValue,
|
|
23
24
|
getValueOrDefaultValue,
|
|
@@ -363,6 +364,18 @@ function AutoField<T = any>({
|
|
|
363
364
|
return (
|
|
364
365
|
<Override
|
|
365
366
|
{...rest}
|
|
367
|
+
// The field's SHAPE, which `rest` does not carry: these three are
|
|
368
|
+
// destructured into named props above, so a host editor received a
|
|
369
|
+
// field it could not describe. `arg_schema` is the sub-schema a host
|
|
370
|
+
// sub-form renders FROM — without it the IDE's test-cases drawer fell
|
|
371
|
+
// back to humanising each key, so `Path`, `Actual` and `Expected` were
|
|
372
|
+
// asked for with no label of their own and no description, while the
|
|
373
|
+
// text sat one fetch away. `element_type`/`ui_element_type` travel with
|
|
374
|
+
// it: a sub-schema alone cannot say whether the value is one hash or a
|
|
375
|
+
// list of them.
|
|
376
|
+
arg_schema={arg_schema}
|
|
377
|
+
element_type={element_type}
|
|
378
|
+
ui_element_type={ui_element_type}
|
|
366
379
|
name={name}
|
|
367
380
|
value={value}
|
|
368
381
|
onChange={(val: any, emittedType?: IQorusType, emittedIsFunction?: boolean) => {
|
|
@@ -424,6 +437,9 @@ function AutoField<T = any>({
|
|
|
424
437
|
value={value}
|
|
425
438
|
name={name}
|
|
426
439
|
onChange={handleChange}
|
|
440
|
+
// The picker heads itself with the field's own name rather than a
|
|
441
|
+
// generic "Select one:".
|
|
442
|
+
label={rest.display_name}
|
|
427
443
|
size={rest.size}
|
|
428
444
|
disabled={rest.disabled}
|
|
429
445
|
app={rest.app}
|
|
@@ -507,6 +523,12 @@ function AutoField<T = any>({
|
|
|
507
523
|
return (
|
|
508
524
|
<LongStringFormField
|
|
509
525
|
{...rest}
|
|
526
|
+
// These four share one editor but not one shape: a `string` holds
|
|
527
|
+
// exactly one line, while `data`, `binary` and `long-string` hold
|
|
528
|
+
// a document. Without the type the field cannot tell them apart
|
|
529
|
+
// and treats them all as documents, so an interface's Internal
|
|
530
|
+
// Name — which becomes a YAML key — accepted Enter.
|
|
531
|
+
type={currentType}
|
|
510
532
|
onChange={(value) => handleChange(name, value)}
|
|
511
533
|
value={value}
|
|
512
534
|
/>
|
|
@@ -644,8 +666,16 @@ function AutoField<T = any>({
|
|
|
644
666
|
});
|
|
645
667
|
};
|
|
646
668
|
|
|
669
|
+
// An element may arrive bare or in a `{value, type}` envelope. The
|
|
670
|
+
// envelope is what `formatToServerValue` writes, so a list the user
|
|
671
|
+
// has edited in this session is always wrapped — but a list read
|
|
672
|
+
// back from storage holds exactly what the server contract asks
|
|
673
|
+
// for, which for a list of strings is the strings themselves.
|
|
674
|
+
// Reaching for `.value` on those answered `undefined` for every
|
|
675
|
+
// element: the row rendered as an empty, invalid item, and the
|
|
676
|
+
// emptiness was then written back over the stored value.
|
|
647
677
|
const formatFromServerValue = (value) => {
|
|
648
|
-
return (value || []).map(
|
|
678
|
+
return (value || []).map(getListElementValue);
|
|
649
679
|
};
|
|
650
680
|
|
|
651
681
|
const mappedAllowedValues = rest.element_allowed_values?.map((ev) => ({
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { ReqoreTextarea } from '@qoretechnologies/reqore';
|
|
2
2
|
import { IReqoreTextareaProps } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
3
|
-
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
|
4
4
|
import { useDebounce } from 'react-use';
|
|
5
|
+
import {
|
|
6
|
+
flattenToSingleLine,
|
|
7
|
+
hasLineBreak,
|
|
8
|
+
isSingleLineStringType,
|
|
9
|
+
} from '../../../../helpers/singleLineString';
|
|
5
10
|
|
|
6
11
|
export interface ILongStringFormFieldProps extends Omit<IReqoreTextareaProps, 'onChange'> {
|
|
7
12
|
value?: string;
|
|
8
13
|
onChange?: (value: string, event?: ChangeEvent<HTMLTextAreaElement>) => void;
|
|
14
|
+
/**
|
|
15
|
+
* The field's declared type, which decides whether it holds one line.
|
|
16
|
+
*
|
|
17
|
+
* Without it every string field is a growing textarea, so a technical name, a
|
|
18
|
+
* version and an IP address all accept Enter — and become YAML keys, class
|
|
19
|
+
* names and URLs with a newline in them. See `helpers/singleLineString`.
|
|
20
|
+
*/
|
|
21
|
+
type?: string;
|
|
9
22
|
}
|
|
10
23
|
|
|
11
24
|
export const LongStringFormField = ({
|
|
12
25
|
value,
|
|
13
26
|
onChange,
|
|
14
27
|
onClearClick,
|
|
28
|
+
type,
|
|
15
29
|
...rest
|
|
16
30
|
}: ILongStringFormFieldProps) => {
|
|
17
31
|
const [localValue, setLocalValue] = useState<string>(value ?? '');
|
|
@@ -32,9 +46,32 @@ export const LongStringFormField = ({
|
|
|
32
46
|
[localValue, onChange]
|
|
33
47
|
);
|
|
34
48
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
const singleLine = isSingleLineStringType(type);
|
|
50
|
+
|
|
51
|
+
const handleChange = useCallback(
|
|
52
|
+
(event: ChangeEvent<HTMLTextAreaElement>): void => {
|
|
53
|
+
const raw = event.target.value;
|
|
54
|
+
// Flatten here rather than only blocking the Enter key: a line break also
|
|
55
|
+
// arrives by paste, by drag-and-drop, from autofill and from an IME, and
|
|
56
|
+
// a keydown guard catches none of those.
|
|
57
|
+
setLocalValue(singleLine && hasLineBreak(raw) ? flattenToSingleLine(raw) : raw);
|
|
58
|
+
},
|
|
59
|
+
[singleLine]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Stop Enter before it inserts anything, so the caret does not jump to a
|
|
63
|
+
// second line that is then flattened away under the cursor.
|
|
64
|
+
const handleKeyDown = useCallback(
|
|
65
|
+
(event: KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
66
|
+
if (singleLine && event.key === 'Enter') {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
}
|
|
69
|
+
(rest as { onKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void }).onKeyDown?.(
|
|
70
|
+
event
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
[singleLine, rest.onKeyDown]
|
|
74
|
+
);
|
|
38
75
|
|
|
39
76
|
const handleClearClick = useCallback(() => {
|
|
40
77
|
setLocalValue('');
|
|
@@ -44,12 +81,16 @@ export const LongStringFormField = ({
|
|
|
44
81
|
|
|
45
82
|
return (
|
|
46
83
|
<ReqoreTextarea
|
|
47
|
-
|
|
84
|
+
// A one-line field must not grow, and must not offer a resize grip that
|
|
85
|
+
// implies it can hold more than it will keep.
|
|
86
|
+
scaleWithContent={!singleLine}
|
|
87
|
+
rows={singleLine ? 1 : undefined}
|
|
48
88
|
fluid
|
|
49
89
|
value={localValue}
|
|
50
90
|
onChange={handleChange}
|
|
51
91
|
onClearClick={handleClearClick}
|
|
52
92
|
{...rest}
|
|
93
|
+
onKeyDown={handleKeyDown}
|
|
53
94
|
/>
|
|
54
95
|
);
|
|
55
96
|
};
|
|
@@ -55,6 +55,11 @@ export const Empty: Story = {
|
|
|
55
55
|
await expect(canvas.getByText('Tables')).toBeInTheDocument();
|
|
56
56
|
await expect(canvas.getByText('Sequences')).toBeInTheDocument();
|
|
57
57
|
await expect(canvas.getByText('Migrations')).toBeInTheDocument();
|
|
58
|
+
// The form rows fade in after the chrome — wait for the Schema tab's own
|
|
59
|
+
// catalogue rows, or the capture shows empty shells where the form goes
|
|
60
|
+
// (rejected on build #123 for exactly that).
|
|
61
|
+
await expect(await canvas.findByText('Schema Name')).toBeInTheDocument();
|
|
62
|
+
await expect(await canvas.findByText('Version')).toBeInTheDocument();
|
|
58
63
|
},
|
|
59
64
|
};
|
|
60
65
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { expect, fireEvent, fn, waitFor } from 'storybook/test';
|
|
3
|
+
import { LONG_EXAMPLE_VALUE } from './__fixtures__/longExampleValue';
|
|
4
|
+
import { ReqraftTemplateExampleValueModal } from './ExampleValueModal';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
component: ReqraftTemplateExampleValueModal,
|
|
8
|
+
title: 'Components/Form/TemplateExampleValueModal',
|
|
9
|
+
args: {
|
|
10
|
+
// Standalone render — in the app the modal rides reqore's global modal
|
|
11
|
+
// queue, whose wrapper injects `isOpen`/`onClose` itself.
|
|
12
|
+
isOpen: true,
|
|
13
|
+
label: 'Attachment Body',
|
|
14
|
+
value: LONG_EXAMPLE_VALUE,
|
|
15
|
+
onClose: fn(),
|
|
16
|
+
},
|
|
17
|
+
} as Meta<typeof ReqraftTemplateExampleValueModal>;
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
export const Default: StoryObj<typeof meta> = {
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
story:
|
|
26
|
+
'The full example-value viewer for template items whose value is too long for the picker: the whole value in a selectable, scrollable textarea filling the modal body, with a Copy action. Copy surfaces a notification either way — success, or a danger notice when the browser denies clipboard access — never an unhandled rejection.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
play: async () => {
|
|
31
|
+
const modal = await waitFor(
|
|
32
|
+
() => {
|
|
33
|
+
const el = document.querySelector('.reqraft-template-example-value-modal');
|
|
34
|
+
if (!el) throw new Error('modal not rendered yet');
|
|
35
|
+
return el;
|
|
36
|
+
},
|
|
37
|
+
{ timeout: 10000 }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// The modal holds the WHOLE value — down to the sentinel at the very end.
|
|
41
|
+
const textarea = modal.querySelector('textarea') as HTMLTextAreaElement;
|
|
42
|
+
await expect(textarea.value).toBe(LONG_EXAMPLE_VALUE);
|
|
43
|
+
await expect(textarea.value.endsWith('THE_VERY_END')).toBe(true);
|
|
44
|
+
|
|
45
|
+
// Copy notifies (success locally, clipboard-denied danger in headless CI).
|
|
46
|
+
const copy = Array.from(modal.querySelectorAll('button')).find((button) =>
|
|
47
|
+
button.textContent?.includes('Copy')
|
|
48
|
+
);
|
|
49
|
+
await expect(copy).toBeTruthy();
|
|
50
|
+
fireEvent.click(copy);
|
|
51
|
+
await waitFor(
|
|
52
|
+
() => {
|
|
53
|
+
if (!document.querySelector('.reqore-notification')) {
|
|
54
|
+
throw new Error('no notification yet');
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{ timeout: 10000 }
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const Mobile: StoryObj<typeof meta> = {
|
|
63
|
+
args: {
|
|
64
|
+
// The overridable strings double as the prop-passthrough coverage: the
|
|
65
|
+
// play below asserts THIS notification text, proving consumers (e.g.
|
|
66
|
+
// qorus-ide under Lingui) can supply translated copy.
|
|
67
|
+
copyLabel: 'Copy value',
|
|
68
|
+
copyNotificationContent: 'Value copied',
|
|
69
|
+
copyFailedNotificationContent: 'Copy was blocked',
|
|
70
|
+
},
|
|
71
|
+
parameters: {
|
|
72
|
+
qlip: { viewport: { width: 390, height: 844 } },
|
|
73
|
+
docs: {
|
|
74
|
+
description: {
|
|
75
|
+
story:
|
|
76
|
+
'The phone presentation of the full-value modal — the textarea still fills the modal body at phone width, and every user-visible string (Copy label, both notifications) is prop-overridden here, which doubles as coverage that consumers can pass translated copy.',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
play: async () => {
|
|
81
|
+
const modal = await waitFor(
|
|
82
|
+
() => {
|
|
83
|
+
const el = document.querySelector('.reqraft-template-example-value-modal');
|
|
84
|
+
if (!el) throw new Error('modal not rendered yet');
|
|
85
|
+
return el;
|
|
86
|
+
},
|
|
87
|
+
{ timeout: 10000 }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const textarea = modal.querySelector('textarea') as HTMLTextAreaElement;
|
|
91
|
+
await expect(textarea.value).toBe(LONG_EXAMPLE_VALUE);
|
|
92
|
+
|
|
93
|
+
// The overridden copy label renders…
|
|
94
|
+
const copy = Array.from(modal.querySelectorAll('button')).find((button) =>
|
|
95
|
+
button.textContent?.includes('Copy value')
|
|
96
|
+
);
|
|
97
|
+
await expect(copy).toBeTruthy();
|
|
98
|
+
|
|
99
|
+
// …and the overridden notification content is what surfaces (success or
|
|
100
|
+
// clipboard-denied, both prop-supplied).
|
|
101
|
+
fireEvent.click(copy);
|
|
102
|
+
await waitFor(
|
|
103
|
+
() => {
|
|
104
|
+
const notification = document.querySelector('.reqore-notification');
|
|
105
|
+
if (!notification) throw new Error('no notification yet');
|
|
106
|
+
const text = notification.textContent || '';
|
|
107
|
+
if (!text.includes('Value copied') && !text.includes('Copy was blocked')) {
|
|
108
|
+
throw new Error(`unexpected notification: ${text}`);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{ timeout: 10000 }
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
};
|