@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.
- package/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js +5 -1
- package/dist/components/composer/Composer.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts +20 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +279 -41
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +72 -72
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +203 -98
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +55 -13
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
- package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
- package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +21 -4
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +24 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +84 -6
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +1 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +64 -14
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +25 -4
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
- package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
- package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
- package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +35 -6
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
- package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +1 -1
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/common.d.ts +8 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +9 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +156 -0
- package/dist/helpers/optionUiTypes.js.map +1 -0
- package/dist/helpers/options.d.ts +19 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +25 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/providerValue.d.ts +68 -0
- package/dist/helpers/providerValue.d.ts.map +1 -0
- package/dist/helpers/providerValue.js +50 -0
- package/dist/helpers/providerValue.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts +37 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -0
- package/dist/helpers/singleLineString.js +66 -0
- package/dist/helpers/singleLineString.js.map +1 -0
- package/dist/helpers/templates.d.ts +100 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +237 -4
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +103 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +858 -91
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer/Composer.tsx +7 -3
- package/src/components/form/engine/CompactRow.tsx +391 -29
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +863 -12
- package/src/components/form/engine/FormEngine.tsx +231 -68
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +38 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +125 -35
- package/src/components/form/fields/Field.tsx +3 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
- package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
- package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
- package/src/components/form/fields/template/TemplateField.tsx +47 -8
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
- package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
- package/src/components/ticketThread/TicketThread.tsx +7 -0
- package/src/helpers/common.ts +11 -1
- package/src/helpers/optionUiTypes.ts +161 -0
- package/src/helpers/options.ts +23 -0
- package/src/helpers/providerValue.ts +125 -0
- package/src/helpers/singleLineString.ts +61 -0
- package/src/helpers/templates.ts +272 -3
- package/src/helpers/validations.ts +1177 -109
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
- package/src/index.tsx +1 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
355
|
-
//
|
|
356
|
-
// is
|
|
357
|
-
//
|
|
358
|
-
|
|
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
|
-
|
|
364
|
-
style={{ marginLeft: 4, verticalAlign: 'middle' }}
|
|
365
|
-
onClick={(event) => event.stopPropagation()}
|
|
402
|
+
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
|
366
403
|
>
|
|
367
|
-
|
|
368
|
-
</
|
|
369
|
-
:
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
219
|
-
|
|
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) =>
|
|
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)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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 = (
|
|
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(
|
|
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;
|