@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
@@ -1,4 +1,10 @@
1
- import { ReqoreCollapsibleContent, ReqoreIcon, ReqoreP, ReqoreTag } from '@qoretechnologies/reqore';
1
+ import {
2
+ ReqoreCollapsibleContent,
3
+ ReqoreIcon,
4
+ ReqoreLink,
5
+ ReqoreP,
6
+ ReqoreTag,
7
+ } from '@qoretechnologies/reqore';
2
8
  import {
3
9
  IQorusFormField,
4
10
  IQorusFormSchema,
@@ -9,7 +15,16 @@ import styled from 'styled-components';
9
15
  import { useContextSelector } from 'use-context-selector';
10
16
  import { CompactRowContext } from '../compactRowContext';
11
17
  import { MONO_FONT_STACK, StyledCodePreview, StyledRowLabel, StyledRowValue } from '../compactRowStyles';
12
- import { findAllowedValueOption, formatOptionValue, getAllowedValueImage } from '../readFirst';
18
+ import {
19
+ fieldLabel,
20
+ findAllowedValueOption,
21
+ formatOptionValue,
22
+ getAllowedValueImage,
23
+ isCodeField,
24
+ orderedKeys,
25
+ recordIdentity,
26
+ titleKeyFor,
27
+ } from '../readFirst';
13
28
  import { isUiEncodedValue } from './structuredData';
14
29
 
15
30
  /**
@@ -26,10 +41,11 @@ import { isUiEncodedValue } from './structuredData';
26
41
  * So a described value is rendered the way the form asked for it, in the form's
27
42
  * own clothes:
28
43
  *
29
- * - Field names use `StyledRowLabel` and values use `StyledRowValue` — the very
30
- * components the option rows above are built from. Not a copy of their CSS: a
31
- * copy is a second definition of "what a form label looks like", and the two
32
- * drift the first time either is touched.
44
+ * - Field names use `StyledFieldLabel` (a `StyledRowLabel` in the read-only
45
+ * eyebrow treatment) and values use `StyledRowValue` — the very components the
46
+ * option rows above are built from. Not a copy of their CSS: a copy is a
47
+ * second definition of "what a form label looks like", and the two drift the
48
+ * first time either is touched.
33
49
  * - Values are formatted by the same `formatOptionValue` the rows use, so an
34
50
  * `allowed_values` entry resolves to its display name, a bool to Yes/No, a
35
51
  * date to the app's format. The same words appear in the summary, the preview
@@ -129,10 +145,39 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
129
145
  ${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
130
146
  `;
131
147
 
132
- /** The rule tying an item's fields to its number. */
133
- const StyledItemFields = styled.div<{ $border: string }>`
134
- border-left: 1px solid ${({ $border }) => $border};
135
- padding-left: 10px;
148
+ /**
149
+ * Does this record have anything to show BELOW its heading?
150
+ *
151
+ * The heading consumes the identifying field, so a record holding only that has
152
+ * an empty body — and a rule drawn down an empty body is a line to nowhere.
153
+ */
154
+ const hasContentUnderHeading = (record: unknown, schema?: IQorusFormSchema): boolean => {
155
+ if (!isRecord(record)) {
156
+ return true;
157
+ }
158
+ const titleKey = titleKeyFor(record, schema);
159
+
160
+ return Object.entries(record).some(
161
+ ([key, value]) =>
162
+ key !== titleKey && value !== undefined && value !== null && value !== ''
163
+ );
164
+ };
165
+
166
+ /** The rule tying an item's fields to its number.
167
+ *
168
+ * Same 3px as the heading's accent bar directly above it: at 1px the two read as
169
+ * two different devices stacked on each other rather than one rule running down
170
+ * the item, and the padding drops to 8px so the text stays on the same column.
171
+ *
172
+ * The rule is drawn only when something is actually under the heading. The
173
+ * heading is pulled left of the rule by its own negative margin, so an item
174
+ * whose record holds nothing but its identity showed a 1-2px sliver of line and
175
+ * nothing else — which reads as a rendering artefact, because that is what it
176
+ * was. It keeps a transparent border in that case so the column does not shift.
177
+ */
178
+ const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
179
+ border-left: 3px solid ${({ $border, $rule }) => ($rule ? $border : 'transparent')};
180
+ padding-left: 8px;
136
181
  min-width: 0;
137
182
  `;
138
183
 
@@ -144,6 +189,30 @@ const StyledItemFields = styled.div<{ $border: string }>`
144
189
  * and leave the items ragged against each other, which reads as a rendering
145
190
  * fault rather than as a list.
146
191
  */
192
+ /**
193
+ * A record field's name, in the detail-page vocabulary.
194
+ *
195
+ * Uppercase, tracked and dimmed, so a field name reads as furniture and the
196
+ * VALUE beside it is what the eye lands on. Same treatment the read-only
197
+ * interface panels use, which is the point: an in-editor list of records and a
198
+ * detail-page panel showing the same shape should not look like two components.
199
+ *
200
+ * A variant rather than a change to `StyledRowLabel`: that one is the FORM's own
201
+ * option-row label, and the form's fields are not read-only furniture — they are
202
+ * the things you are about to edit. Composing it keeps the colour, weight base
203
+ * and wrapping behaviour in one place instead of copying them.
204
+ *
205
+ * Applies to the label only. A VALUE is never uppercased: values are frequently
206
+ * case-sensitive — a service method is `onOrderStatus`, not `ONORDERSTATUS` —
207
+ * and the item heading is a value.
208
+ */
209
+ const StyledFieldLabel = styled(StyledRowLabel)`
210
+ font-size: 12px;
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.06em;
213
+ opacity: 0.55;
214
+ `;
215
+
147
216
  const StyledFields = styled.div`
148
217
  /* The supporting fields step in under the heading, so the heading keeps the
149
218
  left edge to itself. This is the half that answers "the name is written in
@@ -224,59 +293,6 @@ const unwrap = (item: unknown): unknown => (isUiEncodedValue(item) ? item.value
224
293
  const isRecord = (value: unknown): value is Record<string, unknown> =>
225
294
  !!value && typeof value === 'object' && !Array.isArray(value);
226
295
 
227
- /** Whether a value is worth a row of its own. `false` and `0` are values. */
228
- const isSet = (value: unknown): boolean =>
229
- value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
230
-
231
- /**
232
- * The keys to render, in the order to render them: the schema's own order first
233
- * (that is the order the form puts the fields in), then anything stored that the
234
- * schema does not mention, so undescribed data is shown rather than dropped.
235
- */
236
- const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
237
- const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
238
- const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
239
- return [...described, ...extra];
240
- };
241
-
242
- /** A field whose content is source code. `ui_type` is what the form renders by,
243
- * so it is what the preview reads by too — the storage type is just `string`. */
244
- const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
245
- const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
246
- return uiType === 'code-editor';
247
- };
248
-
249
- /**
250
- * The field whose value heads the item — the first DECLARED one holding a plain
251
- * scalar.
252
- *
253
- * Schema order, not value order: the first declared field is the one the form
254
- * puts at the top of an item, which is the one that says which item it is. A
255
- * code body or a nested level is skipped, not because it is unimportant but
256
- * because it has no one-line form — it belongs in the rows below where it can
257
- * actually be read.
258
- *
259
- * One definition, used both to RENDER the heading and to omit that field from
260
- * the rows. Computing it twice is how the heading and the rows start disagreeing
261
- * about which field was promoted, and the item shows its name twice or not at
262
- * all.
263
- */
264
- const titleKeyFor = (
265
- record: Record<string, unknown>,
266
- schema: IQorusFormSchema
267
- ): string | undefined =>
268
- orderedKeys(record, schema).find((key) => {
269
- const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
270
- if (isCodeField(fieldSchema)) {
271
- return false;
272
- }
273
- const raw = unwrap(record[key]);
274
- return typeof raw === 'string' || typeof raw === 'number';
275
- });
276
-
277
- const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
278
- (fieldSchema as { display_name?: string } | undefined)?.display_name || key;
279
-
280
296
  /**
281
297
  * The width of the shared label column, measured from the longest name that will
282
298
  * actually be rendered.
@@ -286,6 +302,12 @@ const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined)
286
302
  * the `%` ceiling stops one long name from taking the whole row when the inset
287
303
  * is narrow — a phone, or a deeply nested level.
288
304
  */
305
+ /** Approximate rendered width of one label character, in px. See `labelColumn`. */
306
+ const LABEL_CHAR_PX = 9.5;
307
+
308
+ /** Gap to the value, so the longest label is not flush against it. */
309
+ const LABEL_SLACK_PX = 14;
310
+
289
311
  const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
290
312
  const inner = unwrap(value);
291
313
  const records = (Array.isArray(inner) ? inner.map(unwrap) : [inner]).filter(isRecord);
@@ -297,9 +319,26 @@ const labelColumn = (value: unknown, schema: IQorusFormSchema): string => {
297
319
  );
298
320
  return Math.max(widest, own);
299
321
  }, 0);
300
- return longest ? `min(${longest + 1}ch, 45%)` : '140px';
322
+ // px, not `ch`. `ch` is the base font's `0` advance and knows nothing about
323
+ // the label's own treatment — uppercase, semibold, `letter-spacing: 0.06em`,
324
+ // at 12px — so it under-measures by about a tenth. With `overflow-wrap:
325
+ // anywhere` on the label, an under-measured column does not ellipsise, it
326
+ // BREAKS THE NAME IN HALF: `LEGACY_FLAG` came out over two lines the moment
327
+ // the labels became uppercase. Erring high costs a few px of gap; erring low
328
+ // costs a broken word.
329
+ return longest ? `min(${Math.ceil(longest * LABEL_CHAR_PX + LABEL_SLACK_PX)}px, 45%)` : '140px';
301
330
  };
302
331
 
332
+ /**
333
+ * `true` when a value is an address a browser can open.
334
+ *
335
+ * Deliberately per VALUE, not per field: a field named `url` frequently holds
336
+ * something that is not one — a bare path, a regex, a template — and offering
337
+ * those a link promises a destination there is not.
338
+ */
339
+ const isNavigableUrl = (value: unknown): value is string =>
340
+ typeof value === 'string' && /^(https?|wss?|ftps?):\/\//i.test(value);
341
+
303
342
  /** One field's value, rendered the way its row above renders it. */
304
343
  const FieldValue = ({
305
344
  field,
@@ -345,7 +384,22 @@ const FieldValue = ({
345
384
  style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
346
385
  />
347
386
  : null}
348
- {formatted}
387
+ {isNavigableUrl(field.value) ?
388
+ // The address IS the link — it used to render as inert text with a
389
+ // separate icon button beside it to open it, which is two things
390
+ // where there is one. ReqoreLink renders a real anchor, so
391
+ // middle-click and "copy link address" still work;
392
+ // `stopPropagation` because the row around it toggles.
393
+ <ReqoreLink
394
+ className='schema-view-open-url'
395
+ href={field.value}
396
+ target='_blank'
397
+ rel='noreferrer noopener'
398
+ onClick={(event: React.MouseEvent) => event.stopPropagation()}
399
+ >
400
+ {formatted}
401
+ </ReqoreLink>
402
+ : formatted}
349
403
  </span>
350
404
  }
351
405
  </StyledRowValue>
@@ -394,7 +448,7 @@ const SchemaRecord = ({
394
448
  return (
395
449
  <React.Fragment key={key}>
396
450
  <StyledNestedLabel>
397
- <StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
451
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
398
452
  </StyledNestedLabel>
399
453
  <StyledNested $border={colors.border}>
400
454
  <SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
@@ -412,7 +466,7 @@ const SchemaRecord = ({
412
466
  return (
413
467
  <React.Fragment key={key}>
414
468
  <StyledNestedLabel>
415
- <StyledRowLabel $color={colors.key}>{labelWithType}</StyledRowLabel>
469
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
416
470
  </StyledNestedLabel>
417
471
  <StyledCodeCell onClick={(event) => event.stopPropagation()}>
418
472
  <ReqoreCollapsibleContent
@@ -444,9 +498,9 @@ const SchemaRecord = ({
444
498
 
445
499
  return (
446
500
  <React.Fragment key={key}>
447
- <StyledRowLabel $color={colors.key} title={label}>
501
+ <StyledFieldLabel $color={colors.key} title={label}>
448
502
  {labelWithType}
449
- </StyledRowLabel>
503
+ </StyledFieldLabel>
450
504
  <FieldValue
451
505
  field={{ type: fieldSchema?.type, value } as IQorusFormField}
452
506
  fieldSchema={fieldSchema}
@@ -481,16 +535,17 @@ const SchemaLevel = ({
481
535
  return (
482
536
  <StyledItem key={index} className='schema-view-item'>
483
537
  <StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
484
- <StyledItemFields $border={colors.border}>
538
+ <StyledItemFields
539
+ $border={colors.border}
540
+ $rule={hasContentUnderHeading(record, schema)}
541
+ >
485
542
  {isRecord(record) ?
486
543
  <>
487
544
  {(() => {
488
- const titleKey = titleKeyFor(record, schema);
489
- if (!titleKey) {
545
+ const identity = recordIdentity(record, schema);
546
+ if (!identity) {
490
547
  return null;
491
548
  }
492
- const fieldSchema = schema[titleKey] as TQorusFormFieldSchema | undefined;
493
- const raw = unwrap(record[titleKey]);
494
549
  return (
495
550
  <StyledItemTitle
496
551
  className='schema-view-item-title'
@@ -498,14 +553,11 @@ const SchemaLevel = ({
498
553
  // A literal keeps the mono treatment it has as a value —
499
554
  // promoting it must not change what it IS. A chosen
500
555
  // label ("Default RBAC") is prose and stays prose.
501
- $mono={!findAllowedValueOption(raw, fieldSchema)}
556
+ $mono={identity.mono}
502
557
  $accent={colors.accent}
503
- title={fieldLabel(titleKey, fieldSchema)}
558
+ title={identity.label}
504
559
  >
505
- {formatOptionValue(
506
- { type: fieldSchema?.type, value: raw } as IQorusFormField,
507
- fieldSchema
508
- )}
560
+ {identity.text}
509
561
  </StyledItemTitle>
510
562
  );
511
563
  })()}
@@ -61,6 +61,9 @@ export const PANEL_LEFT_CSS = `calc(${LABEL_COL} + ${COMPACT_ROW_PAD_X + COMPACT
61
61
  export const StyledCompactPanel = styled(ReqorePanel)<{
62
62
  $headerBg: string;
63
63
  $nested?: boolean;
64
+ /** True when the toolbar renders no search row (hidden via `compactToolbar`,
65
+ * self-hidden on a single-option form, or no toolbar at all). */
66
+ $tightHeader?: boolean;
64
67
  }>`
65
68
  > .reqore-panel-title {
66
69
  /* The blur + translateZ exist only to make the STICKY top-level toolbar ghost
@@ -71,7 +74,17 @@ export const StyledCompactPanel = styled(ReqorePanel)<{
71
74
  'backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transform: translateZ(0);'
72
75
  )}
73
76
  padding-top: ${GAP_FROM_SIZE[HEADER_GAP]}px;
74
- padding-bottom: ${GAP_FROM_SIZE[HEADER_GAP]}px;
77
+ /* A header with a SEARCH ROW keeps the \`big\` gap below it — a tall
78
+ interactive control earns separation from the content it filters. A
79
+ header without one (search hidden via \`compactToolbar\`, self-hidden on
80
+ a single-option form, or no toolbar at all) is a thin strip, and at
81
+ \`big\` its padding stacked onto the panel content's own 8px into a 26px
82
+ dead band between the completion meter and the first status box —
83
+ rejected on build #140 as "unnecessarily big". \`normal\` lands it at
84
+ the boxes' own 10px rhythm, and scoping the change here keeps every
85
+ search-bearing form pixel-identical. */
86
+ padding-bottom: ${({ $tightHeader }) =>
87
+ $tightHeader ? GAP_FROM_SIZE.normal : GAP_FROM_SIZE[HEADER_GAP]}px;
75
88
  }
76
89
 
77
90
  /* Group framing. The HORIZONTAL rule is a real element in the header
@@ -320,6 +333,12 @@ export const StyledRowValue = styled.div<{ $color: string; $empty?: boolean }>`
320
333
  text-overflow: ellipsis;
321
334
  white-space: nowrap;
322
335
  }
336
+ /* The code size chip kept above an OPEN editor. The read row puts the same chip
337
+ at the top of the value cell with a 2px row-gap under it; matching that here is
338
+ what stops the editor moving as the field opens. */
339
+ .options-readfirst-editing-summary {
340
+ margin-bottom: 2px;
341
+ }
323
342
  .options-readfirst-reason {
324
343
  font-style: italic;
325
344
  font-size: 12px;
@@ -684,3 +703,27 @@ export const StyledGroupBody = styled.div<{
684
703
  padding-bottom: 0;
685
704
  }
686
705
  `;
706
+
707
+ // One absorbed sibling, rendered inside its host row's container rather than
708
+ // on a row of its own (see `absorb_fields`). The pair this exists for is a code
709
+ // editor and its language: reading them as two unrelated rows one above the
710
+ // other said they were separate decisions, and they are not.
711
+ //
712
+ // Laid out as label-then-control on one line so it reads as a property of the
713
+ // element it now sits in, and separated from the editor below it by a hairline
714
+ // rather than a gap — the two share one container, so the seam has to look like
715
+ // a division inside it and not like two stacked things.
716
+ export const StyledAbsorbedField = styled.div`
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 10px;
720
+ min-width: 0;
721
+ padding-bottom: 8px;
722
+ margin-bottom: 8px;
723
+ border-bottom: 1px solid ${({ theme }) => `${theme.main}22`};
724
+ `;
725
+
726
+ export const StyledAbsorbedLabel = styled(ReqoreP)`
727
+ flex: 0 0 auto;
728
+ margin: 0;
729
+ `;
@@ -24,6 +24,14 @@ export interface ICompactToolbarOptionalField {
24
24
  */
25
25
  export interface ICompactToolbarContext {
26
26
  readOnly?: boolean;
27
+ /** Which parts to render, already resolved from the engine's `compactToolbar`
28
+ * prop — every key present, no undefined to re-default here. */
29
+ parts: {
30
+ completion: boolean;
31
+ search: boolean;
32
+ fields: boolean;
33
+ help: boolean;
34
+ };
27
35
  // Completion meter
28
36
  invalidCount: number;
29
37
  attentionCount: number;
@@ -1,4 +1,9 @@
1
- import { IQorusFormField, TQorusFormFieldSchema, TQorusType } from '@qoretechnologies/ts-toolkit';
1
+ import {
2
+ IQorusFormField,
3
+ IQorusFormSchema,
4
+ TQorusFormFieldSchema,
5
+ TQorusType,
6
+ } from '@qoretechnologies/ts-toolkit';
2
7
  import { isRendererOnlyUiType } from './rendererTypes';
3
8
  import { isUiEncodedValue } from './_structuredData/structuredData';
4
9
  import { renderExpressionToText } from '../expressions/renderExpressionToText';
@@ -195,6 +200,13 @@ export const shouldAutoCollapseCompactOption = (
195
200
  * Prefer the matching allowed_values entry's display_name (fallback `name`)
196
201
  * over the raw stored value.
197
202
  *
203
+ * A LIST carries its options under `element_allowed_values` — they constrain
204
+ * each element, not the list itself — so a multi-select that was missing here
205
+ * printed what it stores rather than what you picked: `orders, batch` for
206
+ * fields whose picker reads "Orders, Batch". The gap shows worst exactly where
207
+ * allowed values earn their keep, since the stored form is often not readable
208
+ * at all (a permission code, `PO_REQUIRE_TYPES`, an app-specific id).
209
+ *
198
210
  * Exported (as `findAllowedValueOption`) because the form engine's value
199
211
  * validation has to answer the SAME question — "is this stored value one of the
200
212
  * declared choices?" — and answering it differently loses data. An
@@ -210,13 +222,31 @@ export const findAllowedValueOption = (
210
222
  value: unknown,
211
223
  schema?: TQorusFormFieldSchema
212
224
  ): any | undefined => {
213
- const s = schema as { allowed_values?: any[]; items?: any[] } | undefined;
214
- const options = s?.allowed_values?.length ? s.allowed_values : s?.items;
225
+ const s = schema as
226
+ | { allowed_values?: any[]; element_allowed_values?: any[]; items?: any[] }
227
+ | undefined;
228
+ const options =
229
+ (s?.allowed_values?.length && s.allowed_values) ||
230
+ (s?.element_allowed_values?.length && s.element_allowed_values) ||
231
+ s?.items;
215
232
  if (!options?.length) {
216
233
  return undefined;
217
234
  }
235
+ // A stored element can be the bare value or the typed `{type, value}`
236
+ // envelope the form engine round-trips; match either against either.
237
+ const stored =
238
+ value && typeof value === 'object' && 'value' in (value as Record<string, unknown>) ?
239
+ (value as Record<string, unknown>).value
240
+ : value;
241
+
218
242
  return options.find(
219
- (option) => option?.value?.value === value || option?.value === value || option?.name === value
243
+ (option) =>
244
+ option?.value?.value === value ||
245
+ option?.value === value ||
246
+ option?.name === value ||
247
+ option?.value?.value === stored ||
248
+ option?.value === stored ||
249
+ option?.name === stored
220
250
  );
221
251
  };
222
252
 
@@ -646,12 +676,27 @@ export const getHashEntries = (
646
676
  // whose entries are `{ type: 'string', value: 'x' }`) — recognise those by
647
677
  // their strict shape (a string `type`, a `value`, and no foreign keys) so
648
678
  // the sub-row shows 'x' rather than counting the envelope's own keys.
679
+ // `'value' in raw` is FALSE for a materialised-but-unset field: `fixOptions`
680
+ // omits the key entirely when the value is undefined, leaving `{type: 'x'}`.
681
+ // Requiring `value` therefore classed that envelope as a raw hash VALUE and
682
+ // counted its own `type` key, so an untouched Runtime Defaults collapsed to
683
+ // "Timeout (Seconds) 1 field" three times over while the sub-form correctly
684
+ // said 0/3 set. With a sub-schema present, a `type`-only object is an empty
685
+ // envelope, never content.
686
+ const isEmptyEnvelope =
687
+ !!subSchema &&
688
+ !!raw &&
689
+ typeof raw === 'object' &&
690
+ !Array.isArray(raw) &&
691
+ !('value' in (raw as object)) &&
692
+ Object.keys(raw as object).every((k) => k === 'type' || k === 'is_expression');
649
693
  const isFieldShape =
650
694
  (!!subSchema &&
651
695
  !!raw &&
652
696
  typeof raw === 'object' &&
653
697
  !Array.isArray(raw) &&
654
698
  'value' in (raw as object)) ||
699
+ isEmptyEnvelope ||
655
700
  isTypedEnvelope(raw);
656
701
  const subOption: IQorusFormField =
657
702
  isFieldShape ?
@@ -793,3 +838,117 @@ export const getFirstAttentionOptionName = (
793
838
 
794
839
  return undefined;
795
840
  };
841
+
842
+ /* ------------------------------------------------------------------------- */
843
+ /* Record identity — which field says WHICH item this is */
844
+ /* */
845
+ /* Lives here, beside the formatters it uses, rather than in the view that */
846
+ /* first needed it: it is data logic, not rendering, and BOTH list renderers */
847
+ /* plus their tests import it. Keeping it in `SchemaDataView` dragged React, */
848
+ /* styled-components and every Reqore component it renders into any module */
849
+ /* that only wanted to know an item's name — which broke `arrayAutoField`'s */
850
+ /* mocked-Reqore test the moment the editable list started using it. */
851
+ /* ------------------------------------------------------------------------- */
852
+
853
+ /** A UI-encoded value carries its type alongside it; the record's own value is inside. */
854
+ const unwrap = (value: unknown): unknown =>
855
+ isUiEncodedValue(value) ? (value as { value: unknown }).value : value;
856
+
857
+ /** Whether a value is worth a row of its own. `false` and `0` are values. */
858
+ export const isSet = (value: unknown): boolean =>
859
+ value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
860
+
861
+ /**
862
+ * The keys to render, in the order to render them: the schema's own order first
863
+ * (that is the order the form puts the fields in), then anything stored that the
864
+ * schema does not mention, so undescribed data is shown rather than dropped.
865
+ */
866
+ export const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
867
+ const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
868
+ const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
869
+ return [...described, ...extra];
870
+ };
871
+
872
+ /** A field whose content is source code. `ui_type` is what the form renders by,
873
+ * so it is what the preview reads by too — the storage type is just `string`. */
874
+ export const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
875
+ const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
876
+ return uiType === 'code-editor';
877
+ };
878
+
879
+ /**
880
+ * The field whose value heads the item — the first DECLARED one holding a plain
881
+ * scalar.
882
+ *
883
+ * Schema order, not value order: the first declared field is the one the form
884
+ * puts at the top of an item, which is the one that says which item it is. A
885
+ * code body or a nested level is skipped, not because it is unimportant but
886
+ * because it has no one-line form — it belongs in the rows below where it can
887
+ * actually be read.
888
+ *
889
+ * One definition, used both to RENDER the heading and to omit that field from
890
+ * the rows. Computing it twice is how the heading and the rows start disagreeing
891
+ * about which field was promoted, and the item shows its name twice or not at
892
+ * all.
893
+ */
894
+ export const titleKeyFor = (
895
+ record: Record<string, unknown>,
896
+ schema: IQorusFormSchema
897
+ ): string | undefined =>
898
+ orderedKeys(record, schema).find((key) => {
899
+ const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
900
+ if (isCodeField(fieldSchema)) {
901
+ return false;
902
+ }
903
+ const raw = unwrap(record[key]);
904
+ return typeof raw === 'string' || typeof raw === 'number';
905
+ });
906
+
907
+ /** What identifies one record: which field was promoted, and how to draw it. */
908
+ export interface IRecordIdentity {
909
+ /** The promoted field's key, so a caller can omit it from the rows below. */
910
+ key: string;
911
+ /** The value, formatted the way its own row would format it. */
912
+ text: string;
913
+ /** The promoted field's label — a caption for the heading, not a second line. */
914
+ label: string;
915
+ /** A literal keeps its mono face; a chosen label is prose. */
916
+ mono: boolean;
917
+ }
918
+
919
+ /**
920
+ * The identity of one record, for any surface that heads a list item with it.
921
+ *
922
+ * Exported because the preview is not the only place a list of records is shown:
923
+ * the editable list (`ArrayAuto`) heads the same records, and it must promote the
924
+ * same field and format it the same way. Two implementations would drift the
925
+ * moment one of them learned about a new field type, and the reader would meet
926
+ * an item called `init` in the preview and `#1` in the editor.
927
+ */
928
+ export const recordIdentity = (
929
+ record: Record<string, unknown>,
930
+ schema: IQorusFormSchema | undefined
931
+ ): IRecordIdentity | undefined => {
932
+ if (!schema) {
933
+ return undefined;
934
+ }
935
+
936
+ const key = titleKeyFor(record, schema);
937
+
938
+ if (!key) {
939
+ return undefined;
940
+ }
941
+
942
+ const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
943
+ const raw = unwrap(record[key]);
944
+
945
+ return {
946
+ key,
947
+ text: formatOptionValue({ type: fieldSchema?.type, value: raw } as IQorusFormField, fieldSchema),
948
+ label: fieldLabel(key, fieldSchema),
949
+ mono: !findAllowedValueOption(raw, fieldSchema),
950
+ };
951
+ };
952
+
953
+ export const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
954
+ (fieldSchema as { display_name?: string } | undefined)?.display_name || key;
@@ -569,7 +569,9 @@ export const FormField = <T extends TFormFieldType>({
569
569
  return (
570
570
  <ReqoreControlGroup vertical size='tiny'>
571
571
  <ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
572
- Select one:
572
+ {/* Headed by the field's own name; "Select one:" only when it has
573
+ none. Same rule as FieldAllowedValuesCheckGroup. */}
574
+ {display_name ? `${display_name}:` : 'Select one:'}
573
575
  </ReqoreSpan>
574
576
  {allowed_values.map((item, index) => {
575
577
  const itemValue = item.value?.value;