@qoretechnologies/reqraft 0.10.45-pr.95.gc02205d → 0.10.45

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 (144) 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/composer/Composer.d.ts.map +1 -1
  5. package/dist/components/composer/Composer.js +5 -1
  6. package/dist/components/composer/Composer.js.map +1 -1
  7. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  8. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  9. package/dist/components/form/engine/CompactRow.js +279 -41
  10. package/dist/components/form/engine/CompactRow.js.map +1 -1
  11. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.js +72 -72
  13. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  14. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  15. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  16. package/dist/components/form/engine/FormEngine.js +203 -98
  17. package/dist/components/form/engine/FormEngine.js.map +1 -1
  18. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.js +55 -13
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  21. package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
  22. package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
  23. package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
  24. package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
  25. package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
  26. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactRowStyles.js +21 -4
  28. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  29. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  30. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  31. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  32. package/dist/components/form/engine/readFirst.d.ts +24 -0
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +84 -6
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/fields/Field.d.ts.map +1 -1
  37. package/dist/components/form/fields/Field.js +1 -1
  38. package/dist/components/form/fields/Field.js.map +1 -1
  39. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  40. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  41. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  42. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  43. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  44. package/dist/components/form/fields/auto/AutoFormField.js +64 -14
  45. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  46. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  47. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  48. package/dist/components/form/fields/long-string/LongString.js +25 -4
  49. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  50. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  51. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  52. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  53. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  54. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  55. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  56. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  57. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  58. package/dist/components/form/fields/template/TemplateField.js +35 -6
  59. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  60. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  61. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  62. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  63. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  64. package/dist/components/form/index.d.ts +1 -0
  65. package/dist/components/form/index.d.ts.map +1 -1
  66. package/dist/components/form/index.js +1 -0
  67. package/dist/components/form/index.js.map +1 -1
  68. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
  69. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
  70. package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
  71. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
  72. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  73. package/dist/components/ticketThread/TicketThread.js +1 -1
  74. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  75. package/dist/helpers/common.d.ts +8 -1
  76. package/dist/helpers/common.d.ts.map +1 -1
  77. package/dist/helpers/common.js +9 -1
  78. package/dist/helpers/common.js.map +1 -1
  79. package/dist/helpers/optionUiTypes.d.ts +30 -0
  80. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  81. package/dist/helpers/optionUiTypes.js +156 -0
  82. package/dist/helpers/optionUiTypes.js.map +1 -0
  83. package/dist/helpers/options.d.ts +19 -0
  84. package/dist/helpers/options.d.ts.map +1 -1
  85. package/dist/helpers/options.js +25 -1
  86. package/dist/helpers/options.js.map +1 -1
  87. package/dist/helpers/providerValue.d.ts +68 -0
  88. package/dist/helpers/providerValue.d.ts.map +1 -0
  89. package/dist/helpers/providerValue.js +50 -0
  90. package/dist/helpers/providerValue.js.map +1 -0
  91. package/dist/helpers/singleLineString.d.ts +37 -0
  92. package/dist/helpers/singleLineString.d.ts.map +1 -0
  93. package/dist/helpers/singleLineString.js +66 -0
  94. package/dist/helpers/singleLineString.js.map +1 -0
  95. package/dist/helpers/templates.d.ts +100 -0
  96. package/dist/helpers/templates.d.ts.map +1 -1
  97. package/dist/helpers/templates.js +237 -4
  98. package/dist/helpers/templates.js.map +1 -1
  99. package/dist/helpers/validations.d.ts +103 -2
  100. package/dist/helpers/validations.d.ts.map +1 -1
  101. package/dist/helpers/validations.js +858 -91
  102. package/dist/helpers/validations.js.map +1 -1
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2 -1
  106. package/dist/index.js.map +1 -1
  107. package/package.json +1 -1
  108. package/src/components/composer/Composer.tsx +7 -3
  109. package/src/components/form/engine/CompactRow.tsx +391 -29
  110. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  111. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  112. package/src/components/form/engine/FormEngine.tsx +231 -68
  113. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
  114. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
  115. package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
  116. package/src/components/form/engine/compactRowStyles.ts +38 -1
  117. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  118. package/src/components/form/engine/readFirst.ts +125 -35
  119. package/src/components/form/fields/Field.tsx +3 -1
  120. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  121. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
  122. package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
  123. package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
  124. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  125. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  126. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  127. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  128. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  129. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  130. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  131. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  132. package/src/components/form/index.tsx +1 -0
  133. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
  134. package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
  135. package/src/components/ticketThread/TicketThread.tsx +7 -0
  136. package/src/helpers/common.ts +11 -1
  137. package/src/helpers/optionUiTypes.ts +161 -0
  138. package/src/helpers/options.ts +23 -0
  139. package/src/helpers/providerValue.ts +125 -0
  140. package/src/helpers/singleLineString.ts +61 -0
  141. package/src/helpers/templates.ts +272 -3
  142. package/src/helpers/validations.ts +1177 -109
  143. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  144. package/src/index.tsx +1 -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
  }
@@ -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: {
@@ -364,6 +417,7 @@ export const HashItemsHeadedByIdentity: Story = {
364
417
  { description: 'Added but not yet named' },
365
418
  {},
366
419
  ],
420
+ renderItem: hashRenderItem,
367
421
  },
368
422
  parameters: {
369
423
  docs: {
@@ -390,5 +444,32 @@ export const HashItemsHeadedByIdentity: Story = {
390
444
  // And a record with nothing set at all keeps its position — there is
391
445
  // genuinely nothing to call it.
392
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]');
393
474
  },
394
475
  };
@@ -11,7 +11,7 @@
11
11
  import { StoryObj } from '@storybook/react-vite';
12
12
  import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
13
13
  import jsyaml from 'js-yaml';
14
- import { useState } from 'react';
14
+ import { useEffect, useState } from 'react';
15
15
 
16
16
  import { StoryMeta } from '../../../../types';
17
17
  import { buildTemplates } from '../../../../helpers/templates';
@@ -512,3 +512,49 @@ export const ListWithElementType: Story = {
512
512
  },
513
513
  },
514
514
  };
515
+
516
+ /**
517
+ * A named `arg_schema` whose fetch fails.
518
+ *
519
+ * The field asks the server for `dataprovider/arg_schemas/<name>`. When that
520
+ * request rejected — a malformed instance URL, an aborted request, a transport
521
+ * error — the rejection escaped the effect's async IIFE unhandled, `setError`
522
+ * never ran, and `finalArgSchema` stayed undefined. The loading branch then
523
+ * returned on every subsequent render and the error branch below it was
524
+ * unreachable, so the field showed "Loading field data..." for the life of the
525
+ * form: a request that failed instantly and one that is merely slow looked
526
+ * exactly the same, forever.
527
+ *
528
+ * The transport is stubbed rather than left to the dev server, so the story
529
+ * exercises that path and only that path.
530
+ */
531
+ export const NamedArgSchemaFetchFails: Story = {
532
+ args: {
533
+ name: 'rules',
534
+ type: 'list',
535
+ element_type: 'hash',
536
+ arg_schema: 'a-schema-that-does-not-resolve' as any,
537
+ },
538
+ parameters: {
539
+ docs: {
540
+ description: {
541
+ story:
542
+ 'The named arg schema could not be fetched, so the field says so instead of showing an endless "Loading field data..." spinner with the error it had already received never rendered.',
543
+ },
544
+ },
545
+ },
546
+ decorators: [
547
+ (Story) => {
548
+ const real = window.fetch;
549
+ window.fetch = (() =>
550
+ Promise.reject(new Error('Failed to fetch the field schema'))) as typeof window.fetch;
551
+ useEffect(() => () => {
552
+ window.fetch = real;
553
+ }, []);
554
+ return <Story />;
555
+ },
556
+ ],
557
+ play: async () => {
558
+ await waitForText(/Failed to fetch the field schema|could not be loaded/);
559
+ },
560
+ };
@@ -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,
@@ -212,16 +213,31 @@ function AutoField<T = any>({
212
213
  useEffect(() => {
213
214
  if (typeof arg_schema === 'string') {
214
215
  (async () => {
215
- const schema = await query<IOptionsSchema>({
216
- url: `dataprovider/arg_schemas/${arg_schema}`,
217
- method: 'GET',
218
- });
219
-
220
- if (schema.ok) {
221
- setError(undefined);
222
- setFinalArgSchema(schema.data);
223
- } else {
224
- setError(schema.error);
216
+ /* The await is guarded because `query` can REJECT rather than answer
217
+ `{ ok: false }` — a malformed instance URL, an aborted request, a
218
+ transport error. Unguarded, that rejection escaped the async IIFE as
219
+ an unhandled promise rejection, `setError` never ran, `finalArgSchema`
220
+ stayed undefined, and the field showed "Loading field data..." for the
221
+ life of the form: a request that failed instantly and a request that
222
+ is merely slow looked exactly the same, forever. */
223
+ try {
224
+ const schema = await query<IOptionsSchema>({
225
+ url: `dataprovider/arg_schemas/${arg_schema}`,
226
+ method: 'GET',
227
+ });
228
+
229
+ if (schema.ok) {
230
+ setError(undefined);
231
+ setFinalArgSchema(schema.data);
232
+ } else {
233
+ setError(schema.error ?? `The schema "${arg_schema}" could not be loaded.`);
234
+ }
235
+ } catch (fetchError) {
236
+ setError(
237
+ fetchError instanceof Error ?
238
+ fetchError.message
239
+ : `The schema "${arg_schema}" could not be loaded.`
240
+ );
225
241
  }
226
242
  })();
227
243
  } else {
@@ -363,6 +379,18 @@ function AutoField<T = any>({
363
379
  return (
364
380
  <Override
365
381
  {...rest}
382
+ // The field's SHAPE, which `rest` does not carry: these three are
383
+ // destructured into named props above, so a host editor received a
384
+ // field it could not describe. `arg_schema` is the sub-schema a host
385
+ // sub-form renders FROM — without it the IDE's test-cases drawer fell
386
+ // back to humanising each key, so `Path`, `Actual` and `Expected` were
387
+ // asked for with no label of their own and no description, while the
388
+ // text sat one fetch away. `element_type`/`ui_element_type` travel with
389
+ // it: a sub-schema alone cannot say whether the value is one hash or a
390
+ // list of them.
391
+ arg_schema={arg_schema}
392
+ element_type={element_type}
393
+ ui_element_type={ui_element_type}
366
394
  name={name}
367
395
  value={value}
368
396
  onChange={(val: any, emittedType?: IQorusType, emittedIsFunction?: boolean) => {
@@ -407,6 +435,23 @@ function AutoField<T = any>({
407
435
  );
408
436
  }
409
437
 
438
+ /* The error is checked BEFORE the spinner, and the spinner waits on the error
439
+ being absent.
440
+
441
+ A named `arg_schema` is fetched (`dataprovider/arg_schemas/<name>`), and when
442
+ that fetch failed `finalArgSchema` stayed undefined forever — so this spinner
443
+ returned on every subsequent render and the `if (error)` branch further down
444
+ was unreachable. The field sat on "Loading field data..." for the life of the
445
+ form with no way to find out why, which is indistinguishable from a request
446
+ that is merely slow. */
447
+ if (arg_schema && !finalArgSchema && error) {
448
+ return (
449
+ <ReqoreMessage intent='danger' size='small'>
450
+ {error}
451
+ </ReqoreMessage>
452
+ );
453
+ }
454
+
410
455
  if (arg_schema && !finalArgSchema) {
411
456
  return <ReqoreSpinner size='small'>Loading field data...</ReqoreSpinner>;
412
457
  }
@@ -424,6 +469,9 @@ function AutoField<T = any>({
424
469
  value={value}
425
470
  name={name}
426
471
  onChange={handleChange}
472
+ // The picker heads itself with the field's own name rather than a
473
+ // generic "Select one:".
474
+ label={rest.display_name}
427
475
  size={rest.size}
428
476
  disabled={rest.disabled}
429
477
  app={rest.app}
@@ -507,6 +555,12 @@ function AutoField<T = any>({
507
555
  return (
508
556
  <LongStringFormField
509
557
  {...rest}
558
+ // These four share one editor but not one shape: a `string` holds
559
+ // exactly one line, while `data`, `binary` and `long-string` hold
560
+ // a document. Without the type the field cannot tell them apart
561
+ // and treats them all as documents, so an interface's Internal
562
+ // Name — which becomes a YAML key — accepted Enter.
563
+ type={currentType}
510
564
  onChange={(value) => handleChange(name, value)}
511
565
  value={value}
512
566
  />
@@ -644,8 +698,16 @@ function AutoField<T = any>({
644
698
  });
645
699
  };
646
700
 
701
+ // An element may arrive bare or in a `{value, type}` envelope. The
702
+ // envelope is what `formatToServerValue` writes, so a list the user
703
+ // has edited in this session is always wrapped — but a list read
704
+ // back from storage holds exactly what the server contract asks
705
+ // for, which for a list of strings is the strings themselves.
706
+ // Reaching for `.value` on those answered `undefined` for every
707
+ // element: the row rendered as an empty, invalid item, and the
708
+ // emptiness was then written back over the stored value.
647
709
  const formatFromServerValue = (value) => {
648
- return (value || []).map((item) => item?.value);
710
+ return (value || []).map(getListElementValue);
649
711
  };
650
712
 
651
713
  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
+ };
@@ -0,0 +1,99 @@
1
+ import { ReqoreModal, ReqoreTextarea, useReqoreProperty } from '@qoretechnologies/reqore';
2
+ import { IReqoreModalProps } from '@qoretechnologies/reqore/dist/components/Modal';
3
+ import { CSSProperties, memo, useCallback } from 'react';
4
+
5
+ export interface IReqraftTemplateExampleValueModalProps extends Partial<IReqoreModalProps> {
6
+ /** The template field's display name — the modal heading. */
7
+ label?: string;
8
+ /** The FULL serialized example value. */
9
+ value: string;
10
+ /** Copy-action label. @default 'Copy' */
11
+ copyLabel?: string;
12
+ /**
13
+ * Notification shown after a successful copy (prop-overridable so consumers
14
+ * can pass translated copy — same contract as `ReqoreExportModal`).
15
+ * @default 'Example value copied to clipboard'
16
+ */
17
+ copyNotificationContent?: string;
18
+ /** Notification shown when the browser denies clipboard access. @default 'Could not copy to clipboard' */
19
+ copyFailedNotificationContent?: string;
20
+ }
21
+
22
+ /* Fill the modal's fixed-height body instead of scaling with content: a
23
+ content-scaled textarea either stops short of the body (dead space below, a
24
+ partial-height scrollbar) or explodes it on huge values. Filling hands the
25
+ whole body to the textarea, so its scrollbar spans the modal body. Module
26
+ scope for stable identity. */
27
+ const FILL_STYLE: CSSProperties = { height: '100%' };
28
+
29
+ /**
30
+ * Full example-value viewer for template/context-data items whose
31
+ * `example_value` is too long for the picker's item description (a
32
+ * binary-carrying field's example can be an entire base64 file). Opened by the
33
+ * "?" item action wired in `helpers/templates.ts` via reqore's global
34
+ * `modalStore.addModal()` — the modals wrapper injects `isOpen`/`onClose`, so
35
+ * this component stays presentation-only (pass `isOpen` yourself only when
36
+ * rendering it standalone, e.g. in a story).
37
+ */
38
+ export const ReqraftTemplateExampleValueModal = memo(
39
+ ({
40
+ label,
41
+ value,
42
+ copyLabel = 'Copy',
43
+ copyNotificationContent = 'Example value copied to clipboard',
44
+ copyFailedNotificationContent = 'Could not copy to clipboard',
45
+ ...rest
46
+ }: IReqraftTemplateExampleValueModalProps) => {
47
+ const addNotification = useReqoreProperty('addNotification');
48
+
49
+ const handleCopyClick = useCallback(async () => {
50
+ // Clipboard access can be denied (permissions, insecure context) — the
51
+ // failure surfaces as a notification instead of an unhandled rejection.
52
+ try {
53
+ await navigator.clipboard.writeText(value);
54
+ addNotification({
55
+ content: copyNotificationContent,
56
+ intent: 'success',
57
+ duration: 3000,
58
+ });
59
+ } catch (error) {
60
+ addNotification({
61
+ content: copyFailedNotificationContent,
62
+ intent: 'danger',
63
+ duration: 3000,
64
+ });
65
+ }
66
+ }, [value, addNotification, copyNotificationContent, copyFailedNotificationContent]);
67
+
68
+ return (
69
+ <ReqoreModal
70
+ label={label || 'Example value'}
71
+ icon='FileTextLine'
72
+ height='70vh'
73
+ className='reqraft-template-example-value-modal'
74
+ bottomActions={[
75
+ {
76
+ position: 'right',
77
+ label: copyLabel,
78
+ icon: 'ClipboardLine',
79
+ onClick: () => {
80
+ handleCopyClick();
81
+ },
82
+ },
83
+ ]}
84
+ {...rest}
85
+ >
86
+ <ReqoreTextarea
87
+ readOnly
88
+ value={value}
89
+ minimal
90
+ fluid
91
+ style={FILL_STYLE}
92
+ wrapperStyle={FILL_STYLE}
93
+ />
94
+ </ReqoreModal>
95
+ );
96
+ }
97
+ );
98
+
99
+ ReqraftTemplateExampleValueModal.displayName = 'ReqraftTemplateExampleValueModal';