@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
@@ -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,
@@ -8,7 +14,12 @@ import React from 'react';
8
14
  import styled from 'styled-components';
9
15
  import { useContextSelector } from 'use-context-selector';
10
16
  import { CompactRowContext } from '../compactRowContext';
11
- import { MONO_FONT_STACK, StyledCodePreview, StyledRowLabel, StyledRowValue } from '../compactRowStyles';
17
+ import {
18
+ MONO_FONT_STACK,
19
+ StyledCodePreview,
20
+ StyledRowLabel,
21
+ StyledRowValue,
22
+ } from '../compactRowStyles';
12
23
  import {
13
24
  fieldLabel,
14
25
  findAllowedValueOption,
@@ -19,7 +30,7 @@ import {
19
30
  recordIdentity,
20
31
  titleKeyFor,
21
32
  } from '../readFirst';
22
- import { isUiEncodedValue } from './structuredData';
33
+ import { isEmptyUiEnvelope, isUiEncodedValue } from './structuredData';
23
34
 
24
35
  /**
25
36
  * A read-only rendering of a value THROUGH the schema that describes it.
@@ -139,10 +150,38 @@ const StyledItemTitle = styled.div<{ $color: string; $mono: boolean; $accent: st
139
150
  ${({ $mono }) => ($mono ? `font-family: ${MONO_FONT_STACK}; font-size: 15px;` : '')}
140
151
  `;
141
152
 
142
- /** The rule tying an item's fields to its number. */
143
- const StyledItemFields = styled.div<{ $border: string }>`
144
- border-left: 1px solid ${({ $border }) => $border};
145
- padding-left: 10px;
153
+ /**
154
+ * Does this record have anything to show BELOW its heading?
155
+ *
156
+ * The heading consumes the identifying field, so a record holding only that has
157
+ * an empty body — and a rule drawn down an empty body is a line to nowhere.
158
+ */
159
+ const hasContentUnderHeading = (record: unknown, schema?: IQorusFormSchema): boolean => {
160
+ if (!isRecord(record)) {
161
+ return true;
162
+ }
163
+ const titleKey = titleKeyFor(record, schema);
164
+
165
+ return Object.entries(record).some(
166
+ ([key, value]) => key !== titleKey && value !== undefined && value !== null && value !== ''
167
+ );
168
+ };
169
+
170
+ /** The rule tying an item's fields to its number.
171
+ *
172
+ * Same 3px as the heading's accent bar directly above it: at 1px the two read as
173
+ * two different devices stacked on each other rather than one rule running down
174
+ * the item, and the padding drops to 8px so the text stays on the same column.
175
+ *
176
+ * The rule is drawn only when something is actually under the heading. The
177
+ * heading is pulled left of the rule by its own negative margin, so an item
178
+ * whose record holds nothing but its identity showed a 1-2px sliver of line and
179
+ * nothing else — which reads as a rendering artefact, because that is what it
180
+ * was. It keeps a transparent border in that case so the column does not shift.
181
+ */
182
+ const StyledItemFields = styled.div<{ $border: string; $rule: boolean }>`
183
+ border-left: 3px solid ${({ $border, $rule }) => ($rule ? $border : 'transparent')};
184
+ padding-left: 8px;
146
185
  min-width: 0;
147
186
  `;
148
187
 
@@ -349,24 +388,22 @@ const FieldValue = ({
349
388
  style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
350
389
  />
351
390
  : null}
352
- {formatted}
353
391
  {isNavigableUrl(field.value) ?
354
- // The read-only detail views make an address openable, and a
355
- // preview that shows the same value should not be the one place it
356
- // is inert. A real anchor, so middle-click and "copy link address"
357
- // work; `stopPropagation` because the row around it toggles.
358
- <a
392
+ // The address IS the link — it used to render as inert text with a
393
+ // separate icon button beside it to open it, which is two things
394
+ // where there is one. ReqoreLink renders a real anchor, so
395
+ // middle-click and "copy link address" still work;
396
+ // `stopPropagation` because the row around it toggles.
397
+ <ReqoreLink
359
398
  className='schema-view-open-url'
360
399
  href={field.value}
361
400
  target='_blank'
362
401
  rel='noreferrer noopener'
363
- aria-label='Open in a new tab'
364
- style={{ marginLeft: 4, verticalAlign: 'middle' }}
365
- onClick={(event) => event.stopPropagation()}
402
+ onClick={(event: React.MouseEvent) => event.stopPropagation()}
366
403
  >
367
- <ReqoreIcon icon='ExternalLinkLine' size='11px' />
368
- </a>
369
- : null}
404
+ {formatted}
405
+ </ReqoreLink>
406
+ : formatted}
370
407
  </span>
371
408
  }
372
409
  </StyledRowValue>
@@ -391,92 +428,109 @@ const SchemaRecord = ({
391
428
  // The SAME host renderer the row above uses for a `code-editor` field. Read
392
429
  // from context rather than threaded through props: a nested level is arbitrarily
393
430
  // deep, and the renderer is a property of the form, not of any one level.
394
- const codePreviewRenderer = useContextSelector(
395
- CompactRowContext,
396
- (v) => v.codePreviewRenderer
397
- );
431
+ const codePreviewRenderer = useContextSelector(CompactRowContext, (v) => v.codePreviewRenderer);
398
432
  const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
399
433
 
400
434
  return (
401
- <StyledFields className='schema-view-fields'>
402
- {orderedKeys(record, schema)
403
- .filter((key) => key !== skipKey)
404
- .map((key) => {
405
- const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
406
- const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
407
- const value = unwrap(record[key]);
408
- const label = fieldLabel(key, fieldSchema);
409
- const labelWithType =
410
- showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
411
-
412
- // A field that describes its own level renders AS that level, indented
413
- // under its name — the same decision, one step down.
414
- if (nested && (isRecord(value) || Array.isArray(value))) {
415
- return (
416
- <React.Fragment key={key}>
417
- <StyledNestedLabel>
418
- <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
419
- </StyledNestedLabel>
420
- <StyledNested $border={colors.border}>
421
- <SchemaLevel value={value} schema={nested} showTypes={showTypes} colors={colors} />
422
- </StyledNested>
423
- </React.Fragment>
424
- );
425
- }
426
-
427
- // A code value is source, and source is read as source: the host's renderer
428
- // gives it the syntax highlighting (and read-only LSP hover) it has in the
429
- // Source Code field, and "Show more" keeps a long body from taking over the
430
- // preview. Flattening it to one mono line — which is what a plain value cell
431
- // does — throws all of that away for a field whose whole content is code.
432
- if (isCodeField(fieldSchema) && typeof value === 'string') {
433
- return (
434
- <React.Fragment key={key}>
435
- <StyledNestedLabel>
436
- <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
437
- </StyledNestedLabel>
438
- <StyledCodeCell onClick={(event) => event.stopPropagation()}>
439
- <ReqoreCollapsibleContent
440
- maxCollapsedHeight={96}
441
- buttonProps={{ className: 'options-readfirst-viewmore' }}
442
- >
443
- {codePreviewRenderer ?
444
- codePreviewRenderer({
445
- value,
446
- name: key,
447
- schema: fieldSchema,
448
- options: schema,
449
- values: record as never,
450
- })
451
- : <StyledCodePreview
452
- className='options-readfirst-code'
453
- $bg={cHover}
454
- $border={`${colors.border}88`}
455
- $fg={colors.key}
435
+ <StyledFields className='schema-view-fields'>
436
+ {orderedKeys(record, schema)
437
+ .filter((key) => key !== skipKey)
438
+ .map((key) => {
439
+ const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
440
+ const nested = (fieldSchema as { arg_schema?: IQorusFormSchema } | undefined)?.arg_schema;
441
+ // A schema-declared field the form materialised but never filled in
442
+ // arrives as `{type: 'int'}` with no `value` key. `unwrap` cannot see
443
+ // that — `isUiEncodedValue` requires a `value` — so it fell through as
444
+ // CONTENT and was wrapped as one below, and `formatOptionValue` counted
445
+ // the envelope's own `type` key: an untouched Runtime Defaults read
446
+ // "Timeout (Seconds) 1 field" three times over while the sub-form it
447
+ // previews correctly said 0/3 set. `fieldSchema` is what licenses the
448
+ // reading: the schema declares this field, so a `{type}`-only object
449
+ // under it is an empty envelope, never somebody's data.
450
+ const raw = record[key];
451
+ // `!nested` is the discriminator, and it is not a detail: a field that
452
+ // declares its OWN level describes what is inside it, so `{type: 'default'}`
453
+ // there is content — the scheme's Scheme Type, reading "Default RBAC" —
454
+ // not an envelope. Only a leaf can be empty in this sense.
455
+ const value =
456
+ fieldSchema && !nested && isEmptyUiEnvelope(raw) ? undefined : unwrap(raw);
457
+ const label = fieldLabel(key, fieldSchema);
458
+ const labelWithType =
459
+ showTypes && fieldSchema?.type ? `${label} · ${fieldSchema.type}` : label;
460
+
461
+ // A field that describes its own level renders AS that level, indented
462
+ // under its name — the same decision, one step down.
463
+ if (nested && (isRecord(value) || Array.isArray(value))) {
464
+ return (
465
+ <React.Fragment key={key}>
466
+ <StyledNestedLabel>
467
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
468
+ </StyledNestedLabel>
469
+ <StyledNested $border={colors.border}>
470
+ <SchemaLevel
471
+ value={value}
472
+ schema={nested}
473
+ showTypes={showTypes}
474
+ colors={colors}
475
+ />
476
+ </StyledNested>
477
+ </React.Fragment>
478
+ );
479
+ }
480
+
481
+ // A code value is source, and source is read as source: the host's renderer
482
+ // gives it the syntax highlighting (and read-only LSP hover) it has in the
483
+ // Source Code field, and "Show more" keeps a long body from taking over the
484
+ // preview. Flattening it to one mono line — which is what a plain value cell
485
+ // does — throws all of that away for a field whose whole content is code.
486
+ if (isCodeField(fieldSchema) && typeof value === 'string') {
487
+ return (
488
+ <React.Fragment key={key}>
489
+ <StyledNestedLabel>
490
+ <StyledFieldLabel $color={colors.key}>{labelWithType}</StyledFieldLabel>
491
+ </StyledNestedLabel>
492
+ <StyledCodeCell onClick={(event) => event.stopPropagation()}>
493
+ <ReqoreCollapsibleContent
494
+ maxCollapsedHeight={96}
495
+ buttonProps={{ className: 'options-readfirst-viewmore' }}
456
496
  >
457
- {value}
458
- </StyledCodePreview>
459
- }
460
- </ReqoreCollapsibleContent>
461
- </StyledCodeCell>
462
- </React.Fragment>
463
- );
464
- }
497
+ {codePreviewRenderer ?
498
+ codePreviewRenderer({
499
+ value,
500
+ name: key,
501
+ schema: fieldSchema,
502
+ options: schema,
503
+ values: record as never,
504
+ })
505
+ : <StyledCodePreview
506
+ className='options-readfirst-code'
507
+ $bg={cHover}
508
+ $border={`${colors.border}88`}
509
+ $fg={colors.key}
510
+ >
511
+ {value}
512
+ </StyledCodePreview>
513
+ }
514
+ </ReqoreCollapsibleContent>
515
+ </StyledCodeCell>
516
+ </React.Fragment>
517
+ );
518
+ }
465
519
 
466
- return (
467
- <React.Fragment key={key}>
468
- <StyledFieldLabel $color={colors.key} title={label}>
469
- {labelWithType}
470
- </StyledFieldLabel>
471
- <FieldValue
472
- field={{ type: fieldSchema?.type, value } as IQorusFormField}
473
- fieldSchema={fieldSchema}
474
- color={colors.key}
475
- />
476
- </React.Fragment>
477
- );
478
- })}
479
- </StyledFields>
520
+ return (
521
+ <React.Fragment key={key}>
522
+ <StyledFieldLabel $color={colors.key} title={label}>
523
+ {labelWithType}
524
+ </StyledFieldLabel>
525
+ <FieldValue
526
+ field={{ type: fieldSchema?.type, value } as IQorusFormField}
527
+ fieldSchema={fieldSchema}
528
+ color={colors.key}
529
+ />
530
+ </React.Fragment>
531
+ );
532
+ })}
533
+ </StyledFields>
480
534
  );
481
535
  };
482
536
 
@@ -502,7 +556,10 @@ const SchemaLevel = ({
502
556
  return (
503
557
  <StyledItem key={index} className='schema-view-item'>
504
558
  <StyledMarker $color={colors.muted}>{index + 1}.</StyledMarker>
505
- <StyledItemFields $border={colors.border}>
559
+ <StyledItemFields
560
+ $border={colors.border}
561
+ $rule={hasContentUnderHeading(record, schema)}
562
+ >
506
563
  {isRecord(record) ?
507
564
  <>
508
565
  {(() => {
@@ -52,6 +52,26 @@ export const isRecord = (value: unknown): value is Record<string, unknown> =>
52
52
  export const isStructuredContainerValue = (value: unknown): boolean =>
53
53
  Array.isArray(value) || isRecord(value);
54
54
 
55
+ /**
56
+ * A field the form materialised but left UNSET: `{type: 'int'}`, no `value` key.
57
+ *
58
+ * `fixOptions` omits the key entirely when the value is undefined, so this is
59
+ * what an untouched sub-field of a schema-declared hash looks like on the wire.
60
+ * It is an EMPTY ENVELOPE, never content — and every renderer has to agree on
61
+ * that, because the ones that do not count the envelope's own `type` key and
62
+ * report an unset field as "1 field" while the sub-form correctly says 0 set.
63
+ *
64
+ * Deliberately separate from {@link isUiEncodedValue}, which requires a `value`
65
+ * and therefore cannot see this shape. Callers gate on knowing the field is
66
+ * schema-declared: without a schema saying so, a `{type: …}` object could
67
+ * legitimately be somebody's data.
68
+ */
69
+ export const isEmptyUiEnvelope = (value: unknown): boolean =>
70
+ isRecord(value) &&
71
+ typeof value.type === 'string' &&
72
+ !('value' in value) &&
73
+ Object.keys(value).every((key) => UI_ENVELOPE_KEYS.has(key));
74
+
55
75
  export const isUiEncodedValue = (value: unknown): value is IUiEncodedValue => {
56
76
  if (!isRecord(value) || typeof value.type !== 'string' || !('value' in value)) {
57
77
  return false;
@@ -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
@@ -690,3 +703,27 @@ export const StyledGroupBody = styled.div<{
690
703
  padding-bottom: 0;
691
704
  }
692
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;
@@ -5,7 +5,7 @@ import {
5
5
  TQorusType,
6
6
  } from '@qoretechnologies/ts-toolkit';
7
7
  import { isRendererOnlyUiType } from './rendererTypes';
8
- import { isUiEncodedValue } from './_structuredData/structuredData';
8
+ import { isEmptyUiEnvelope, isUiEncodedValue } from './_structuredData/structuredData';
9
9
  import { renderExpressionToText } from '../expressions/renderExpressionToText';
10
10
  import { IExpressionValue } from '../expressions/types';
11
11
  import yaml from 'js-yaml';
@@ -155,6 +155,45 @@ export const isOptionValueEmpty = (value: unknown): boolean =>
155
155
  value === '' ||
156
156
  (Array.isArray(value) && value.length === 0);
157
157
 
158
+ /**
159
+ * A schema-declared hash whose every entry is a materialised-but-unset field.
160
+ *
161
+ * `preselected` on a sub-field means "show this in the form so the author sees
162
+ * it exists" — a FORM instruction. Opening the sub-form materialises each one,
163
+ * `fixOptions` writes `{type: 'int'}` with no value, and the draft autosaves
164
+ * that, so the stored hash permanently carries keys that stand for nothing.
165
+ *
166
+ * The row then read those keys as content: `Runtime Defaults` listed three
167
+ * sub-fields with no values, counted toward "7/12 set", and showed a green
168
+ * set-dot — while the sub-form it summarises correctly said 0/3 set. Nothing
169
+ * was ever entered.
170
+ *
171
+ * Requires the sub-schema, and requires each key to be declared by it: without
172
+ * that, `{type: 'x'}` could be somebody's data rather than an empty envelope.
173
+ */
174
+ export const isUnsetSchemaHash = (value: unknown, schema?: TQorusFormFieldSchema): boolean => {
175
+ const argSchema = (schema as { arg_schema?: Record<string, unknown> } | undefined)?.arg_schema;
176
+ if (!argSchema || typeof argSchema !== 'object') {
177
+ return false;
178
+ }
179
+ const inner =
180
+ value && typeof value === 'object' && !Array.isArray(value) && 'value' in (value as object) ?
181
+ (value as { value: unknown }).value
182
+ : value;
183
+ if (!inner || typeof inner !== 'object' || Array.isArray(inner)) {
184
+ return false;
185
+ }
186
+ const keys = Object.keys(inner as object);
187
+ return (
188
+ keys.length > 0 &&
189
+ keys.every(
190
+ (key) =>
191
+ !!(argSchema as Record<string, unknown>)[key] &&
192
+ isEmptyUiEnvelope((inner as Record<string, unknown>)[key])
193
+ )
194
+ );
195
+ };
196
+
158
197
  export const isFixedCompactAllowedValueOption = (
159
198
  schema: TQorusFormFieldSchema | undefined
160
199
  ): boolean => {
@@ -200,6 +239,13 @@ export const shouldAutoCollapseCompactOption = (
200
239
  * Prefer the matching allowed_values entry's display_name (fallback `name`)
201
240
  * over the raw stored value.
202
241
  *
242
+ * A LIST carries its options under `element_allowed_values` — they constrain
243
+ * each element, not the list itself — so a multi-select that was missing here
244
+ * printed what it stores rather than what you picked: `orders, batch` for
245
+ * fields whose picker reads "Orders, Batch". The gap shows worst exactly where
246
+ * allowed values earn their keep, since the stored form is often not readable
247
+ * at all (a permission code, `PO_REQUIRE_TYPES`, an app-specific id).
248
+ *
203
249
  * Exported (as `findAllowedValueOption`) because the form engine's value
204
250
  * validation has to answer the SAME question — "is this stored value one of the
205
251
  * declared choices?" — and answering it differently loses data. An
@@ -215,13 +261,30 @@ export const findAllowedValueOption = (
215
261
  value: unknown,
216
262
  schema?: TQorusFormFieldSchema
217
263
  ): any | undefined => {
218
- const s = schema as { allowed_values?: any[]; items?: any[] } | undefined;
219
- const options = s?.allowed_values?.length ? s.allowed_values : s?.items;
264
+ const s = schema as
265
+ { allowed_values?: any[]; element_allowed_values?: any[]; items?: any[] } | undefined;
266
+ const options =
267
+ (s?.allowed_values?.length && s.allowed_values) ||
268
+ (s?.element_allowed_values?.length && s.element_allowed_values) ||
269
+ s?.items;
220
270
  if (!options?.length) {
221
271
  return undefined;
222
272
  }
273
+ // A stored element can be the bare value or the typed `{type, value}`
274
+ // envelope the form engine round-trips; match either against either.
275
+ const stored =
276
+ value && typeof value === 'object' && 'value' in (value as Record<string, unknown>) ?
277
+ (value as Record<string, unknown>).value
278
+ : value;
279
+
223
280
  return options.find(
224
- (option) => option?.value?.value === value || option?.value === value || option?.name === value
281
+ (option) =>
282
+ option?.value?.value === value ||
283
+ option?.value === value ||
284
+ option?.name === value ||
285
+ option?.value?.value === stored ||
286
+ option?.value === stored ||
287
+ option?.name === stored
225
288
  );
226
289
  };
227
290
 
@@ -499,7 +562,7 @@ export const formatOptionValue = (
499
562
  ): string => {
500
563
  const value = option?.value;
501
564
 
502
- if (isOptionValueEmpty(value)) {
565
+ if (isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema)) {
503
566
  return '';
504
567
  }
505
568
 
@@ -641,34 +704,55 @@ export const getHashEntries = (
641
704
  const argSchema = (schema as { arg_schema?: Record<string, TQorusFormFieldSchema> } | undefined)
642
705
  ?.arg_schema;
643
706
 
644
- return Object.keys(hashObject).map((key) => {
645
- const raw = hashObject![key];
646
- const subSchema = argSchema?.[key];
647
- // A schema-declared (`arg_schema`) hash stores each entry as a
648
- // `{ type, value }` field descriptor — gate on the sub-schema's presence.
649
- // A schema-less hash *usually* stores raw values, but the server's typed
650
- // serialization can nest envelopes there too (e.g. a hash default_value
651
- // whose entries are `{ type: 'string', value: 'x' }`) — recognise those by
652
- // their strict shape (a string `type`, a `value`, and no foreign keys) so
653
- // the sub-row shows 'x' rather than counting the envelope's own keys.
654
- const isFieldShape =
655
- (!!subSchema &&
656
- !!raw &&
657
- typeof raw === 'object' &&
658
- !Array.isArray(raw) &&
659
- 'value' in (raw as object)) ||
660
- isTypedEnvelope(raw);
661
- const subOption: IQorusFormField =
662
- isFieldShape ?
663
- (raw as IQorusFormField)
664
- : ({ type: subSchema?.type as IQorusFormField['type'], value: raw } as IQorusFormField);
665
-
666
- return {
667
- name: key,
668
- label: subSchema?.display_name || key,
669
- value: formatOptionValue(subOption, subSchema),
670
- };
671
- });
707
+ return Object.keys(hashObject)
708
+ .filter((key) => {
709
+ // Only sub-fields with a value belong in a read view. `preselected`
710
+ // materialises a field so the FORM shows it; that is not a statement
711
+ // that the object has one, and listing it here says the opposite of
712
+ // what the sub-form says.
713
+ const subSchema = argSchema?.[key];
714
+ return !(subSchema && isEmptyUiEnvelope(hashObject![key]));
715
+ })
716
+ .map((key) => {
717
+ const raw = hashObject![key];
718
+ const subSchema = argSchema?.[key];
719
+ // A schema-declared (`arg_schema`) hash stores each entry as a
720
+ // `{ type, value }` field descriptor — gate on the sub-schema's presence.
721
+ // A schema-less hash *usually* stores raw values, but the server's typed
722
+ // serialization can nest envelopes there too (e.g. a hash default_value
723
+ // whose entries are `{ type: 'string', value: 'x' }`) — recognise those by
724
+ // their strict shape (a string `type`, a `value`, and no foreign keys) so
725
+ // the sub-row shows 'x' rather than counting the envelope's own keys.
726
+ // `'value' in raw` is FALSE for a materialised-but-unset field: `fixOptions`
727
+ // omits the key entirely when the value is undefined, leaving `{type: 'x'}`.
728
+ // Requiring `value` therefore classed that envelope as a raw hash VALUE and
729
+ // counted its own `type` key, so an untouched Runtime Defaults collapsed to
730
+ // "Timeout (Seconds) 1 field" three times over while the sub-form correctly
731
+ // said 0/3 set. With a sub-schema present, a `type`-only object is an empty
732
+ // envelope, never content.
733
+ // Shared with SchemaDataView, which previews this same value one level
734
+ // down: two renderers disagreeing about what an unset field looks like is
735
+ // how the row said "1 field" long after this one stopped.
736
+ const isEmptyEnvelope = !!subSchema && isEmptyUiEnvelope(raw);
737
+ const isFieldShape =
738
+ (!!subSchema &&
739
+ !!raw &&
740
+ typeof raw === 'object' &&
741
+ !Array.isArray(raw) &&
742
+ 'value' in (raw as object)) ||
743
+ isEmptyEnvelope ||
744
+ isTypedEnvelope(raw);
745
+ const subOption: IQorusFormField =
746
+ isFieldShape ?
747
+ (raw as IQorusFormField)
748
+ : ({ type: subSchema?.type as IQorusFormField['type'], value: raw } as IQorusFormField);
749
+
750
+ return {
751
+ name: key,
752
+ label: subSchema?.display_name || key,
753
+ value: formatOptionValue(subOption, subSchema),
754
+ };
755
+ });
672
756
  };
673
757
 
674
758
  const titleCase = (value: string): string =>
@@ -823,7 +907,10 @@ export const isSet = (value: unknown): boolean =>
823
907
  * (that is the order the form puts the fields in), then anything stored that the
824
908
  * schema does not mention, so undescribed data is shown rather than dropped.
825
909
  */
826
- export const orderedKeys = (record: Record<string, unknown>, schema: IQorusFormSchema): string[] => {
910
+ export const orderedKeys = (
911
+ record: Record<string, unknown>,
912
+ schema: IQorusFormSchema
913
+ ): string[] => {
827
914
  const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
828
915
  const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
829
916
  return [...described, ...extra];
@@ -904,7 +991,10 @@ export const recordIdentity = (
904
991
 
905
992
  return {
906
993
  key,
907
- text: formatOptionValue({ type: fieldSchema?.type, value: raw } as IQorusFormField, fieldSchema),
994
+ text: formatOptionValue(
995
+ { type: fieldSchema?.type, value: raw } as IQorusFormField,
996
+ fieldSchema
997
+ ),
908
998
  label: fieldLabel(key, fieldSchema),
909
999
  mono: !findAllowedValueOption(raw, fieldSchema),
910
1000
  };
@@ -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;