@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.
Files changed (137) hide show
  1. package/.claude/CLAUDE.md +12 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
  4. package/dist/components/codeSize/index.d.ts +37 -0
  5. package/dist/components/codeSize/index.d.ts.map +1 -0
  6. package/dist/components/codeSize/index.js +87 -0
  7. package/dist/components/codeSize/index.js.map +1 -0
  8. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  9. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  10. package/dist/components/form/engine/CompactRow.js +284 -42
  11. package/dist/components/form/engine/CompactRow.js.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  13. package/dist/components/form/engine/CompactToolbar.js +72 -72
  14. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  15. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  16. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  17. package/dist/components/form/engine/FormEngine.js +163 -75
  18. package/dist/components/form/engine/FormEngine.js.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js +102 -87
  21. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  22. package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
  23. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  24. package/dist/components/form/engine/compactRowStyles.js +22 -5
  25. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  26. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  27. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  28. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  29. package/dist/components/form/engine/readFirst.d.ts +57 -1
  30. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  31. package/dist/components/form/engine/readFirst.js +143 -4
  32. package/dist/components/form/engine/readFirst.js.map +1 -1
  33. package/dist/components/form/fields/Field.d.ts.map +1 -1
  34. package/dist/components/form/fields/Field.js +1 -1
  35. package/dist/components/form/fields/Field.js.map +1 -1
  36. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  37. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  38. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  39. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  40. package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
  41. package/dist/components/form/fields/array/ArrayAuto.js +44 -26
  42. package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
  43. package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
  44. package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
  45. package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
  46. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  47. package/dist/components/form/fields/auto/AutoFormField.js +32 -4
  48. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  49. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  50. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  51. package/dist/components/form/fields/long-string/LongString.js +25 -4
  52. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  53. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  54. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  55. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  56. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  57. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  58. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  59. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  60. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  61. package/dist/components/form/fields/template/TemplateField.js +35 -6
  62. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  63. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  64. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  65. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  66. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  67. package/dist/components/form/index.d.ts +1 -0
  68. package/dist/components/form/index.d.ts.map +1 -1
  69. package/dist/components/form/index.js +1 -0
  70. package/dist/components/form/index.js.map +1 -1
  71. package/dist/helpers/common.d.ts +8 -1
  72. package/dist/helpers/common.d.ts.map +1 -1
  73. package/dist/helpers/common.js +9 -1
  74. package/dist/helpers/common.js.map +1 -1
  75. package/dist/helpers/optionUiTypes.d.ts +30 -0
  76. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  77. package/dist/helpers/optionUiTypes.js +148 -0
  78. package/dist/helpers/optionUiTypes.js.map +1 -0
  79. package/dist/helpers/options.d.ts +19 -0
  80. package/dist/helpers/options.d.ts.map +1 -1
  81. package/dist/helpers/options.js +25 -1
  82. package/dist/helpers/options.js.map +1 -1
  83. package/dist/helpers/providerValue.d.ts +68 -0
  84. package/dist/helpers/providerValue.d.ts.map +1 -0
  85. package/dist/helpers/providerValue.js +50 -0
  86. package/dist/helpers/providerValue.js.map +1 -0
  87. package/dist/helpers/singleLineString.d.ts +37 -0
  88. package/dist/helpers/singleLineString.d.ts.map +1 -0
  89. package/dist/helpers/singleLineString.js +66 -0
  90. package/dist/helpers/singleLineString.js.map +1 -0
  91. package/dist/helpers/templates.d.ts +100 -0
  92. package/dist/helpers/templates.d.ts.map +1 -1
  93. package/dist/helpers/templates.js +237 -4
  94. package/dist/helpers/templates.js.map +1 -1
  95. package/dist/helpers/validations.d.ts +103 -2
  96. package/dist/helpers/validations.d.ts.map +1 -1
  97. package/dist/helpers/validations.js +858 -91
  98. package/dist/helpers/validations.js.map +1 -1
  99. package/dist/index.d.ts +2 -1
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +3 -1
  102. package/dist/index.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/components/codeSize/index.tsx +79 -0
  105. package/src/components/form/engine/CompactRow.tsx +407 -34
  106. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  107. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  108. package/src/components/form/engine/FormEngine.tsx +183 -33
  109. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +110 -103
  110. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +132 -80
  111. package/src/components/form/engine/compactRowStyles.ts +44 -1
  112. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  113. package/src/components/form/engine/readFirst.ts +163 -4
  114. package/src/components/form/fields/Field.tsx +3 -1
  115. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  116. package/src/components/form/fields/array/ArrayAuto.tsx +23 -4
  117. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
  118. package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
  119. package/src/components/form/fields/auto/AutoFormField.tsx +31 -1
  120. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  121. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  122. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  123. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  124. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  125. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  126. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  127. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  128. package/src/components/form/index.tsx +1 -0
  129. package/src/helpers/common.ts +11 -1
  130. package/src/helpers/optionUiTypes.ts +153 -0
  131. package/src/helpers/options.ts +23 -0
  132. package/src/helpers/providerValue.ts +125 -0
  133. package/src/helpers/singleLineString.ts +61 -0
  134. package/src/helpers/templates.ts +272 -3
  135. package/src/helpers/validations.ts +1177 -109
  136. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  137. 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
- {multiSelect ? 'Select one or more:' : 'Select one:'}
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
- : JSON.stringify(value) === JSON.stringify(item.value)
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
- label={`#${idx + 1}`}
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={display_name || name}
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
- label={`#${idx + 1}`}
286
- badge={display_name || name}
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((item) => item?.value);
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 handleChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>): void => {
36
- setLocalValue(event.target.value);
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
- scaleWithContent
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
+ };