@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
|
@@ -49,7 +49,12 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
|
|
|
49
49
|
import { useMeasure, useMount, useUpdateEffect } from 'react-use';
|
|
50
50
|
import styled from 'styled-components';
|
|
51
51
|
import { createContext } from 'use-context-selector';
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
fixOperatorValue,
|
|
54
|
+
getDefaultValue,
|
|
55
|
+
insertAtIndex,
|
|
56
|
+
richtextToString,
|
|
57
|
+
} from '../../../helpers/common';
|
|
53
58
|
import { getRequiredOptionMessage } from '../../../helpers/options';
|
|
54
59
|
import {
|
|
55
60
|
IValidationResult,
|
|
@@ -70,11 +75,7 @@ import {
|
|
|
70
75
|
isValueTemplate,
|
|
71
76
|
} from '../fields/template/TemplateField';
|
|
72
77
|
import { CompactRow } from './CompactRow';
|
|
73
|
-
import {
|
|
74
|
-
CompactRowContext,
|
|
75
|
-
ICompactRowContext,
|
|
76
|
-
TCodePreviewRenderer,
|
|
77
|
-
} from './compactRowContext';
|
|
78
|
+
import { CompactRowContext, ICompactRowContext, TCodePreviewRenderer } from './compactRowContext';
|
|
78
79
|
import {
|
|
79
80
|
GROUP_INDENT,
|
|
80
81
|
LABEL_AFFORDANCE_WIDTH,
|
|
@@ -117,9 +118,25 @@ import {
|
|
|
117
118
|
isOptionValueEmpty,
|
|
118
119
|
isFixedCompactAllowedValueOption,
|
|
119
120
|
shouldAutoCollapseCompactOption,
|
|
121
|
+
isUnsetSchemaHash,
|
|
120
122
|
} from './readFirst';
|
|
121
123
|
|
|
122
124
|
// Re-export types for consumers
|
|
125
|
+
/** The compact form's three status boxes. */
|
|
126
|
+
export type TFormEngineBoxKey = 'attention' | 'set' | 'optional';
|
|
127
|
+
|
|
128
|
+
/** Which parts of the compact toolbar to render — see `compactToolbar`. */
|
|
129
|
+
export interface IFormEngineToolbarParts {
|
|
130
|
+
/** The "Ready / 4 of 6 set / 67%" line and its meter. */
|
|
131
|
+
completion?: boolean;
|
|
132
|
+
/** The "Filter fields…" input. */
|
|
133
|
+
search?: boolean;
|
|
134
|
+
/** The field-picker dropdown (add optional fields, reset, revert). */
|
|
135
|
+
fields?: boolean;
|
|
136
|
+
/** The help button that opens the options dialog. */
|
|
137
|
+
help?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
123
140
|
export type IOptions = TQorusForm;
|
|
124
141
|
export type TFlatOptions = TQorusFlatForm;
|
|
125
142
|
export type TOption = IQorusFormField;
|
|
@@ -133,8 +150,7 @@ export type TOperatorValue = TQorusFormOperatorValue;
|
|
|
133
150
|
* consumer gets.
|
|
134
151
|
*/
|
|
135
152
|
export interface IOptionFieldMessage
|
|
136
|
-
extends IQorusFormFieldMessage,
|
|
137
|
-
Pick<IConditionalFieldMessage, 'when' | 'unless'> {}
|
|
153
|
+
extends IQorusFormFieldMessage, Pick<IConditionalFieldMessage, 'when' | 'unless'> {}
|
|
138
154
|
export type IOptionsSchemaArg = TQorusFormFieldSchema;
|
|
139
155
|
export interface IOptionsSchema extends IQorusFormSchema {}
|
|
140
156
|
export interface IOperator extends IQorusFormOperator {}
|
|
@@ -292,9 +308,10 @@ const getTypeFromOperator = (
|
|
|
292
308
|
return (operators[last(operatorData) as string]?.type as TQorusType) || null;
|
|
293
309
|
};
|
|
294
310
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
311
|
+
/** Re-exported from `helpers/common` so existing importers keep working; it
|
|
312
|
+
* lives there because `helpers/validations` needs it and cannot import this
|
|
313
|
+
* module (this module imports the validators). */
|
|
314
|
+
export { fixOperatorValue } from '../../../helpers/common';
|
|
298
315
|
|
|
299
316
|
/**
|
|
300
317
|
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
@@ -307,11 +324,9 @@ const getOptionSchemaStorageType = (
|
|
|
307
324
|
option?: TQorusFormFieldSchema,
|
|
308
325
|
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
309
326
|
): TQorusType =>
|
|
310
|
-
((isFixedCompactAllowedValueOption(option)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
? option?.type || option?.ui_type
|
|
314
|
-
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
327
|
+
((isFixedCompactAllowedValueOption(option) ? option?.type || option?.ui_type
|
|
328
|
+
: isRendererOnly(option?.ui_type) ? option?.type || option?.ui_type
|
|
329
|
+
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
315
330
|
|
|
316
331
|
const getOptionFieldStorageType = (
|
|
317
332
|
optionName: string,
|
|
@@ -323,9 +338,9 @@ const getOptionFieldStorageType = (
|
|
|
323
338
|
): TQorusType => {
|
|
324
339
|
const schemaOption = schema?.[optionName];
|
|
325
340
|
const storedType =
|
|
326
|
-
fieldType && !(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
327
|
-
|
|
328
|
-
|
|
341
|
+
fieldType && !(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType)) ?
|
|
342
|
+
fieldType
|
|
343
|
+
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
329
344
|
|
|
330
345
|
return getType(storedType as TQorusType, operators, operatorData);
|
|
331
346
|
};
|
|
@@ -373,6 +388,23 @@ export const fixOptions = (
|
|
|
373
388
|
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
374
389
|
) {
|
|
375
390
|
obj = option.default_value as IQorusFormField;
|
|
391
|
+
} else if (
|
|
392
|
+
option.default_view === 'expression' &&
|
|
393
|
+
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
394
|
+
) {
|
|
395
|
+
// A field the schema declares as opening in expression mode starts as an
|
|
396
|
+
// EMPTY expression, not as its raw default. `isDefaultFunction` already
|
|
397
|
+
// makes the renderer open that way, so without this the data disagreed
|
|
398
|
+
// with what the operator was looking at: an expression editor whose
|
|
399
|
+
// value was still the plain default, and no `is_expression` on the field
|
|
400
|
+
// until the first edit.
|
|
401
|
+
obj = {
|
|
402
|
+
type,
|
|
403
|
+
value: {
|
|
404
|
+
args: [],
|
|
405
|
+
},
|
|
406
|
+
is_expression: true,
|
|
407
|
+
};
|
|
376
408
|
} else {
|
|
377
409
|
obj = {
|
|
378
410
|
type,
|
|
@@ -438,9 +470,19 @@ export const fixOptions = (
|
|
|
438
470
|
// every value declared the bare way: the collapsed row still showed its
|
|
439
471
|
// display name while the editor showed "—" and the value never reached
|
|
440
472
|
// the submitted data.
|
|
473
|
+
// `allowed_values: []` is NOT "no value is permitted" — it is the server
|
|
474
|
+
// saying it has no reference values to offer right now (the app-action
|
|
475
|
+
// catalogue ships exactly that, with an
|
|
476
|
+
// `option_reference_values_unavailable` message attached). An empty array
|
|
477
|
+
// is truthy, so testing the array itself made every such option erase the
|
|
478
|
+
// operator's stored value on the first render, before the
|
|
479
|
+
// connection-refreshed schema arrived to say otherwise — and the emptied
|
|
480
|
+
// form then autosaved over the draft, so the value was gone for good.
|
|
481
|
+
// Only an option that actually declares choices can have a value that is
|
|
482
|
+
// not one of them.
|
|
441
483
|
if (
|
|
442
484
|
newOption.value !== undefined &&
|
|
443
|
-
options?.[optionName]?.allowed_values &&
|
|
485
|
+
options?.[optionName]?.allowed_values?.length &&
|
|
444
486
|
!findAllowedValueOption(newOption.value, options?.[optionName]) &&
|
|
445
487
|
!isValueTemplate(newOption.value) &&
|
|
446
488
|
!options?.[optionName]?.multiselect &&
|
|
@@ -626,6 +668,37 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
626
668
|
* No-op in classic (non-compact) mode.
|
|
627
669
|
*/
|
|
628
670
|
compactPanelProps?: IReqorePanelProps;
|
|
671
|
+
/**
|
|
672
|
+
* Compact mode only: which status boxes start collapsed. Defaults to
|
|
673
|
+
* `['optional']` — a form opens on what is in use.
|
|
674
|
+
*
|
|
675
|
+
* Pass `['set', 'optional']` where the form is one item in a longer list and
|
|
676
|
+
* the reader is choosing WHICH to open rather than reading this one, e.g. a
|
|
677
|
+
* template's per-action option forms stacked in a drawer.
|
|
678
|
+
*
|
|
679
|
+
* Two escapes override this and are not negotiable: a box always opens while
|
|
680
|
+
* a search query is running (`ReqorePanel` unmounts collapsed content, so a
|
|
681
|
+
* match inside a closed box would be unreachable), and the Optional box stays
|
|
682
|
+
* open when every row is optional, because collapsing it would render a card
|
|
683
|
+
* that looks empty and broken.
|
|
684
|
+
*/
|
|
685
|
+
compactCollapsedGroups?: TFormEngineBoxKey[];
|
|
686
|
+
/**
|
|
687
|
+
* Compact mode only: which parts of the form's toolbar to show. `true`
|
|
688
|
+
* (default) shows all of it; `false` hides the whole thing.
|
|
689
|
+
*
|
|
690
|
+
* The parts already hide themselves when they have nothing to say — the
|
|
691
|
+
* completion meter needs at least one counted option, and the search and
|
|
692
|
+
* field-picker need more than one option to pick between — so reach for this
|
|
693
|
+
* only to hide something that WOULD have rendered. A per-action form asking
|
|
694
|
+
* for one value, say, has a meter reading 0/1 that tells the reader nothing
|
|
695
|
+
* they cannot see in the row below it.
|
|
696
|
+
*
|
|
697
|
+
* Hiding `search` removes the only way to run a query, which is what forces
|
|
698
|
+
* a collapsed box open; `compactCollapsedGroups` then fully determines what
|
|
699
|
+
* is open.
|
|
700
|
+
*/
|
|
701
|
+
compactToolbar?: boolean | IFormEngineToolbarParts;
|
|
629
702
|
/** Compact mode only: per-group display metadata (label / icon / subtitle /
|
|
630
703
|
* order) — the server only sends the bare group key. */
|
|
631
704
|
groups?: Record<string, IFormEngineGroup>;
|
|
@@ -792,6 +865,8 @@ const FormEngineImpl = ({
|
|
|
792
865
|
compactFlush = false,
|
|
793
866
|
compactNested = false,
|
|
794
867
|
compactPanelProps,
|
|
868
|
+
compactCollapsedGroups = ['optional'],
|
|
869
|
+
compactToolbar = true,
|
|
795
870
|
commitMode = 'immediate',
|
|
796
871
|
expandMode = 'single',
|
|
797
872
|
onCommit,
|
|
@@ -1129,7 +1204,10 @@ const FormEngineImpl = ({
|
|
|
1129
1204
|
setOptions({});
|
|
1130
1205
|
return;
|
|
1131
1206
|
}
|
|
1132
|
-
setLocalValue({
|
|
1207
|
+
setLocalValue({
|
|
1208
|
+
fields: fixOptions(value, data.data, undefined, isRendererOnly),
|
|
1209
|
+
meta: undefined,
|
|
1210
|
+
});
|
|
1133
1211
|
if (!operatorsUrl) {
|
|
1134
1212
|
setLoading(false);
|
|
1135
1213
|
}
|
|
@@ -1174,7 +1252,10 @@ const FormEngineImpl = ({
|
|
|
1174
1252
|
}
|
|
1175
1253
|
setOptions(data.data);
|
|
1176
1254
|
onOptionsLoaded?.(data.data);
|
|
1177
|
-
setLocalValue({
|
|
1255
|
+
setLocalValue({
|
|
1256
|
+
fields: fixOptions({}, data.data, undefined, isRendererOnly),
|
|
1257
|
+
meta: undefined,
|
|
1258
|
+
});
|
|
1178
1259
|
})();
|
|
1179
1260
|
}
|
|
1180
1261
|
}, [url, customUrl]);
|
|
@@ -1231,12 +1312,14 @@ const FormEngineImpl = ({
|
|
|
1231
1312
|
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
1232
1313
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
1233
1314
|
const resolvedSchemaType =
|
|
1234
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1315
|
+
isAnyLike && (fields[optionName] as IQorusFormField)?.type ?
|
|
1316
|
+
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
1317
|
+
: (
|
|
1318
|
+
(fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
|
|
1319
|
+
isRendererOnly((fields[optionName] as IQorusFormField)?.type)
|
|
1320
|
+
) ?
|
|
1321
|
+
schemaType
|
|
1322
|
+
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
1240
1323
|
const type =
|
|
1241
1324
|
_type ||
|
|
1242
1325
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
@@ -1310,11 +1393,7 @@ const FormEngineImpl = ({
|
|
|
1310
1393
|
|
|
1311
1394
|
onSingleOptionsChange?.(optionName, updatedValue[optionName]);
|
|
1312
1395
|
|
|
1313
|
-
if (
|
|
1314
|
-
compact &&
|
|
1315
|
-
!readOnly &&
|
|
1316
|
-
shouldAutoCollapseCompactOption(options?.[optionName], val)
|
|
1317
|
-
) {
|
|
1396
|
+
if (compact && !readOnly && shouldAutoCollapseCompactOption(options?.[optionName], val)) {
|
|
1318
1397
|
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1319
1398
|
}
|
|
1320
1399
|
|
|
@@ -1757,7 +1836,11 @@ const FormEngineImpl = ({
|
|
|
1757
1836
|
isRendererOnly
|
|
1758
1837
|
);
|
|
1759
1838
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1760
|
-
|
|
1839
|
+
// A schema-declared hash whose every entry is a materialised-but-unset
|
|
1840
|
+
// field holds nothing, so it is not "set" — without this the row showed a
|
|
1841
|
+
// green set-dot and counted toward the completion meter while the
|
|
1842
|
+
// sub-form it summarises said 0/3 set.
|
|
1843
|
+
const empty = isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema);
|
|
1761
1844
|
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
1762
1845
|
const required = !!(schema?.required || reqGroups.length);
|
|
1763
1846
|
const covered =
|
|
@@ -2060,7 +2143,10 @@ const FormEngineImpl = ({
|
|
|
2060
2143
|
next[optionName] = value as IQorusFormField;
|
|
2061
2144
|
}
|
|
2062
2145
|
});
|
|
2063
|
-
return {
|
|
2146
|
+
return {
|
|
2147
|
+
fields: fixOptions(next, options || {}, operators, isRendererOnly),
|
|
2148
|
+
meta: undefined,
|
|
2149
|
+
};
|
|
2064
2150
|
});
|
|
2065
2151
|
setRequiredOnly(false);
|
|
2066
2152
|
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
@@ -2108,11 +2194,10 @@ const FormEngineImpl = ({
|
|
|
2108
2194
|
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
2109
2195
|
const uiTypeIsAnyLike = schemaUiType === 'any' || schemaUiType === 'auto';
|
|
2110
2196
|
const resolvedType =
|
|
2111
|
-
(isFixedCompactAllowedValueOption(optionSchema)
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
: undefined) ||
|
|
2197
|
+
(isFixedCompactAllowedValueOption(optionSchema) ?
|
|
2198
|
+
getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
2199
|
+
: schemaUiType && !uiTypeIsAnyLike ? schemaUiType
|
|
2200
|
+
: undefined) ||
|
|
2116
2201
|
type ||
|
|
2117
2202
|
schemaUiType ||
|
|
2118
2203
|
(optionSchema?.type as TQorusType) ||
|
|
@@ -2307,6 +2392,11 @@ const FormEngineImpl = ({
|
|
|
2307
2392
|
allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
|
|
2308
2393
|
disabled={
|
|
2309
2394
|
options?.[optionName]?.disabled ||
|
|
2395
|
+
// a system-owned field states that as `readonly`; only the older
|
|
2396
|
+
// compat projection also sent `disabled`, so honouring only
|
|
2397
|
+
// `disabled` rendered an editable control for a value the user
|
|
2398
|
+
// cannot change (see CompactRow's `fieldReadonly`)
|
|
2399
|
+
(options?.[optionName] as { readonly?: boolean } | undefined)?.readonly ||
|
|
2310
2400
|
readOnly ||
|
|
2311
2401
|
!hasAllDependenciesFullfilled(
|
|
2312
2402
|
options?.[optionName]?.depends_on,
|
|
@@ -2503,6 +2593,14 @@ const FormEngineImpl = ({
|
|
|
2503
2593
|
]
|
|
2504
2594
|
);
|
|
2505
2595
|
|
|
2596
|
+
// `compactToolbar` resolved to a full object, so neither the context type nor
|
|
2597
|
+
// the toolbar itself has to re-default anything. `null` means no toolbar.
|
|
2598
|
+
const compactToolbarParts = useMemo(() => {
|
|
2599
|
+
if (compactToolbar === false) return null;
|
|
2600
|
+
const all = { completion: true, search: true, fields: true, help: true };
|
|
2601
|
+
return compactToolbar === true ? all : { ...all, ...compactToolbar };
|
|
2602
|
+
}, [JSON.stringify(compactToolbar)]);
|
|
2603
|
+
|
|
2506
2604
|
const compactToolbarContextValue = useMemo<ICompactToolbarContext>(
|
|
2507
2605
|
() => ({
|
|
2508
2606
|
readOnly,
|
|
@@ -2511,6 +2609,12 @@ const FormEngineImpl = ({
|
|
|
2511
2609
|
completion: readFirstCompletion,
|
|
2512
2610
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
2513
2611
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
2612
|
+
parts: compactToolbarParts ?? {
|
|
2613
|
+
completion: false,
|
|
2614
|
+
search: false,
|
|
2615
|
+
fields: false,
|
|
2616
|
+
help: false,
|
|
2617
|
+
},
|
|
2514
2618
|
hasMultipleOptions: size(availableOptions) > 1,
|
|
2515
2619
|
compactQuery,
|
|
2516
2620
|
setCompactQuery,
|
|
@@ -2561,10 +2665,14 @@ const FormEngineImpl = ({
|
|
|
2561
2665
|
|
|
2562
2666
|
// The engine's own toolbar action, plus whatever the consumer added through
|
|
2563
2667
|
// `compactPanelProps.actions` — appended, so an outside action can never
|
|
2564
|
-
// knock the form's toolbar out of the header.
|
|
2668
|
+
// knock the form's toolbar out of the header. Only `compactToolbar={false}`
|
|
2669
|
+
// removes it, and then the header carries just the consumer's own actions.
|
|
2565
2670
|
const compactHeaderActions = useMemo(
|
|
2566
|
-
() => [
|
|
2567
|
-
|
|
2671
|
+
() => [
|
|
2672
|
+
...(compactToolbarParts ? [{ as: CompactToolbar, responsive: false }] : []),
|
|
2673
|
+
...(compactPanelProps?.actions || []),
|
|
2674
|
+
],
|
|
2675
|
+
[compactPanelProps?.actions, compactToolbarParts]
|
|
2568
2676
|
);
|
|
2569
2677
|
const renderCompact = () => {
|
|
2570
2678
|
const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
|
|
@@ -2577,9 +2685,40 @@ const FormEngineImpl = ({
|
|
|
2577
2685
|
if (requiredOnly && !(schema?.required || schema?.required_groups)) {
|
|
2578
2686
|
return false;
|
|
2579
2687
|
}
|
|
2580
|
-
|
|
2688
|
+
// A field that absorbs a sibling also answers for it: searching
|
|
2689
|
+
// "language" has to surface the row that now holds the language control,
|
|
2690
|
+
// or the field becomes unreachable through the filter.
|
|
2691
|
+
if (matchesQuery(optionName)) return true;
|
|
2692
|
+
const absorb = (schema as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2693
|
+
return !!absorb?.some((absorbedName) => matchesQuery(absorbedName));
|
|
2581
2694
|
};
|
|
2582
2695
|
|
|
2696
|
+
// Fields a sibling absorbs into its own container.
|
|
2697
|
+
//
|
|
2698
|
+
// A field declaring `absorb_fields: ['language']` renders that sibling's
|
|
2699
|
+
// control inside its own row instead of letting it have a row of its own.
|
|
2700
|
+
// The pair this exists for is a code editor and its language: they are one
|
|
2701
|
+
// decision — "what is this code, and in what language" — and reading them
|
|
2702
|
+
// as two unrelated rows one above the other is what the layout used to say.
|
|
2703
|
+
//
|
|
2704
|
+
// Absorbed fields stay in `grouped`, and therefore in the Set / Optional /
|
|
2705
|
+
// Needs-attention counts and in the completion meter. Only the ROW is
|
|
2706
|
+
// skipped. Dropping them from the buckets instead would leave the meter
|
|
2707
|
+
// ("13/18 set", computed from `availableOptions`) disagreeing with the box
|
|
2708
|
+
// badge beside it, which counts row entries.
|
|
2709
|
+
const absorbedBy: Record<string, string> = {};
|
|
2710
|
+
forEach(options, (option, optionName) => {
|
|
2711
|
+
const absorb = (option as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2712
|
+
if (!absorb?.length) return;
|
|
2713
|
+
absorb.forEach((absorbedName) => {
|
|
2714
|
+
// Only absorb a sibling that exists and is not already spoken for; a
|
|
2715
|
+
// schema naming a missing field must not make the field disappear.
|
|
2716
|
+
if (options?.[absorbedName] && !absorbedBy[absorbedName]) {
|
|
2717
|
+
absorbedBy[absorbedName] = optionName;
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
});
|
|
2721
|
+
|
|
2583
2722
|
// Group the options by their raw `group` key, remembering the order groups
|
|
2584
2723
|
// first appear in the schema. Each row carries a `hidden` flag.
|
|
2585
2724
|
const groupOrder: string[] = [];
|
|
@@ -2668,17 +2807,20 @@ const FormEngineImpl = ({
|
|
|
2668
2807
|
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
2669
2808
|
// unmet one-of group's members all land in attention and still rail up.
|
|
2670
2809
|
type TRowEntry = { name: string; hidden: boolean };
|
|
2671
|
-
|
|
2672
|
-
const buckets: Record<TBucketKey, Record<string, TRowEntry[]>> = {
|
|
2810
|
+
const buckets: Record<TFormEngineBoxKey, Record<string, TRowEntry[]>> = {
|
|
2673
2811
|
attention: {},
|
|
2674
2812
|
set: {},
|
|
2675
2813
|
optional: {},
|
|
2676
2814
|
};
|
|
2677
|
-
const bucketGroups: Record<
|
|
2815
|
+
const bucketGroups: Record<TFormEngineBoxKey, string[]> = {
|
|
2816
|
+
attention: [],
|
|
2817
|
+
set: [],
|
|
2818
|
+
optional: [],
|
|
2819
|
+
};
|
|
2678
2820
|
// Freeze the box of any field currently being edited (or whose one-of group
|
|
2679
2821
|
// has an edited member) to its last settled box — so finishing an edit that
|
|
2680
2822
|
// flips its status doesn't remount it in another box and steal focus.
|
|
2681
|
-
const stableBucketOf = (entry: TRowEntry):
|
|
2823
|
+
const stableBucketOf = (entry: TRowEntry): TFormEngineBoxKey => {
|
|
2682
2824
|
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
2683
2825
|
const groupBeingEdited =
|
|
2684
2826
|
!entry.hidden &&
|
|
@@ -2702,7 +2844,7 @@ const FormEngineImpl = ({
|
|
|
2702
2844
|
buckets[b][groupName].push(entry);
|
|
2703
2845
|
});
|
|
2704
2846
|
});
|
|
2705
|
-
const bucketCount = (b:
|
|
2847
|
+
const bucketCount = (b: TFormEngineBoxKey) =>
|
|
2706
2848
|
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2707
2849
|
|
|
2708
2850
|
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
@@ -2718,7 +2860,7 @@ const FormEngineImpl = ({
|
|
|
2718
2860
|
const showGroupSubLabel = (groupName: string) =>
|
|
2719
2861
|
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
2720
2862
|
const STATUS_BOXES: Array<{
|
|
2721
|
-
key:
|
|
2863
|
+
key: TFormEngineBoxKey;
|
|
2722
2864
|
label: string;
|
|
2723
2865
|
intent?: 'warning' | 'success';
|
|
2724
2866
|
icon: IReqoreIconName;
|
|
@@ -2732,6 +2874,11 @@ const FormEngineImpl = ({
|
|
|
2732
2874
|
// together at the first member's slot and rendered as a connected rail (flat
|
|
2733
2875
|
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
2734
2876
|
// are drawn per member). Narrow stacks fall back to flat rows.
|
|
2877
|
+
// The absorbed siblings a host row must render, in schema order. Empty for
|
|
2878
|
+
// every row that absorbs nothing, which is all of them by default.
|
|
2879
|
+
const absorbedNamesFor = (hostName: string): string[] =>
|
|
2880
|
+
Object.keys(absorbedBy).filter((absorbedName) => absorbedBy[absorbedName] === hostName);
|
|
2881
|
+
|
|
2735
2882
|
const renderGroupRows = (names: Array<{ name: string; hidden: boolean }>) => {
|
|
2736
2883
|
const renderRow = (
|
|
2737
2884
|
entry: { name: string; hidden?: boolean },
|
|
@@ -2754,6 +2901,9 @@ const FormEngineImpl = ({
|
|
|
2754
2901
|
clustered={clustered}
|
|
2755
2902
|
clusterFirst={clusterFirst}
|
|
2756
2903
|
clusterLast={clusterLast}
|
|
2904
|
+
// The siblings this row renders inside itself instead of leaving
|
|
2905
|
+
// them a row of their own — see `absorbedBy`.
|
|
2906
|
+
absorbedFields={absorbedNamesFor(entry.name)}
|
|
2757
2907
|
/>
|
|
2758
2908
|
);
|
|
2759
2909
|
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
@@ -2762,6 +2912,10 @@ const FormEngineImpl = ({
|
|
|
2762
2912
|
const emitted = new Set<string>();
|
|
2763
2913
|
const groupOf = (name: string) =>
|
|
2764
2914
|
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
2915
|
+
// An absorbed field keeps its place in the buckets — so the Set /
|
|
2916
|
+
// Optional counts and the completion meter still see it — but its host
|
|
2917
|
+
// renders its control, so it gets no row of its own here.
|
|
2918
|
+
names = names.filter((entry) => !absorbedBy[entry.name]);
|
|
2765
2919
|
return names.map((entry) => {
|
|
2766
2920
|
const grp = groupOf(entry.name);
|
|
2767
2921
|
if (!grp) return renderRow(entry, false);
|
|
@@ -2812,6 +2966,10 @@ const FormEngineImpl = ({
|
|
|
2812
2966
|
$flush={compactFlush || compactNested}
|
|
2813
2967
|
>
|
|
2814
2968
|
<StyledCompactPanel
|
|
2969
|
+
// Mirrors the toolbar's own search-row gate: when no search
|
|
2970
|
+
// row renders, the header is a thin strip and sits tight to
|
|
2971
|
+
// the first status box (see StyledCompactPanel).
|
|
2972
|
+
$tightHeader={!(compactToolbarParts?.search && size(availableOptions) > 1)}
|
|
2815
2973
|
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
2816
2974
|
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
2817
2975
|
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
@@ -2878,24 +3036,29 @@ const FormEngineImpl = ({
|
|
|
2878
3036
|
minimal
|
|
2879
3037
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
2880
3038
|
collapsible
|
|
2881
|
-
//
|
|
2882
|
-
//
|
|
2883
|
-
//
|
|
2884
|
-
//
|
|
2885
|
-
//
|
|
2886
|
-
//
|
|
2887
|
-
//
|
|
2888
|
-
//
|
|
2889
|
-
//
|
|
2890
|
-
//
|
|
2891
|
-
//
|
|
2892
|
-
//
|
|
2893
|
-
//
|
|
2894
|
-
//
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2898
|
-
|
|
3039
|
+
// Which boxes start closed is the consumer's call
|
|
3040
|
+
// (`compactCollapsedGroups`, default `['optional']`: the
|
|
3041
|
+
// Optional box holds every not-yet-added field, so a form
|
|
3042
|
+
// opens on what is in use). Two rules override it:
|
|
3043
|
+
//
|
|
3044
|
+
// A SEARCH forces every box open — ReqorePanel unmounts
|
|
3045
|
+
// collapsed content, so a match inside a closed box would
|
|
3046
|
+
// be unreachable. (isCollapsed is the panel's controllable
|
|
3047
|
+
// state; manual toggling still works with no query set.)
|
|
3048
|
+
//
|
|
3049
|
+
// The Optional box stays open when it is the ONLY box,
|
|
3050
|
+
// because collapsing it renders a card that looks empty and
|
|
3051
|
+
// broken: an auth profile's Authorization block, whose nine
|
|
3052
|
+
// requirement fields are all optional, showed as a titled
|
|
3053
|
+
// card containing one collapsed "Optional 9" strip and
|
|
3054
|
+
// nothing else. This does not generalise to the other two —
|
|
3055
|
+
// a lone Set box collapsed is a deliberate ask, and its
|
|
3056
|
+
// header still reports what is in there.
|
|
3057
|
+
isCollapsed={
|
|
3058
|
+
compactCollapsedGroups.includes(box.key) &&
|
|
3059
|
+
!query &&
|
|
3060
|
+
!(box.key === 'optional' && onlyOptionalRows)
|
|
3061
|
+
}
|
|
2899
3062
|
label={
|
|
2900
3063
|
<StyledGroupHeader>
|
|
2901
3064
|
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
@@ -55,6 +55,78 @@ const methodValue = [
|
|
|
55
55
|
{ type: 'hash', value: { name: 'onConnect', desc: 'connection lifecycle hook' } },
|
|
56
56
|
];
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A schema whose hash values hold hash values of their own — three levels, not
|
|
60
|
+
* one.
|
|
61
|
+
*
|
|
62
|
+
* Every other fixture here is a flat record, and a flat record cannot show what
|
|
63
|
+
* this view does with depth: whether a nested level is legible, whether the
|
|
64
|
+
* rules stack sensibly, whether a heading two levels down still reads as a
|
|
65
|
+
* heading. Judging the treatment on a single `key: value` pair is what the
|
|
66
|
+
* review said was impossible, and it was right.
|
|
67
|
+
*/
|
|
68
|
+
const NestedSchema = {
|
|
69
|
+
name: { type: 'string', display_name: 'Endpoint' },
|
|
70
|
+
auth: {
|
|
71
|
+
type: 'hash',
|
|
72
|
+
display_name: 'Authentication',
|
|
73
|
+
arg_schema: {
|
|
74
|
+
scheme: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
display_name: 'Scheme',
|
|
77
|
+
allowed_values: [
|
|
78
|
+
{ value: 'oauth2', display_name: 'OAuth2' },
|
|
79
|
+
{ value: 'basic', display_name: 'Basic' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
token: {
|
|
83
|
+
type: 'hash',
|
|
84
|
+
display_name: 'Token',
|
|
85
|
+
arg_schema: {
|
|
86
|
+
url: { type: 'string', display_name: 'Token URL' },
|
|
87
|
+
ttl: { type: 'int', display_name: 'Lifetime (s)' },
|
|
88
|
+
rotate: { type: 'bool', display_name: 'Rotate Automatically' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
retry: {
|
|
94
|
+
type: 'hash',
|
|
95
|
+
display_name: 'Retry Policy',
|
|
96
|
+
arg_schema: {
|
|
97
|
+
attempts: { type: 'int', display_name: 'Attempts' },
|
|
98
|
+
backoff: { type: 'string', display_name: 'Backoff' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
} as never;
|
|
102
|
+
|
|
103
|
+
const nestedValue = [
|
|
104
|
+
{
|
|
105
|
+
type: 'hash',
|
|
106
|
+
value: {
|
|
107
|
+
name: 'POST /orders',
|
|
108
|
+
auth: {
|
|
109
|
+
scheme: 'oauth2',
|
|
110
|
+
token: {
|
|
111
|
+
url: 'https://auth.example.com/oauth2/token',
|
|
112
|
+
ttl: 3600,
|
|
113
|
+
rotate: true,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
retry: { attempts: 3, backoff: 'exponential' },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
// A second record that fills only part of the tree: the nested level is
|
|
121
|
+
// present but shallow, which is where "no data under the label" shows up.
|
|
122
|
+
type: 'hash',
|
|
123
|
+
value: {
|
|
124
|
+
name: 'GET /orders/{id}',
|
|
125
|
+
auth: { scheme: 'basic' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
58
130
|
const colors = { key: '#f1f0ee', muted: '#a0a0a0', border: '#a0a0a066', accent: '#3b8eea' };
|
|
59
131
|
|
|
60
132
|
export const Schemes: Story = {
|
|
@@ -127,3 +199,35 @@ export const Undescribed: Story = {
|
|
|
127
199
|
await expect(label!.offsetHeight).toBeLessThan(lineHeight * 1.6);
|
|
128
200
|
},
|
|
129
201
|
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The deeper case: hash values holding hash values.
|
|
205
|
+
*
|
|
206
|
+
* Asked for in review because the flat fixtures could not answer whether the
|
|
207
|
+
* treatment survives nesting — three levels here (endpoint → authentication →
|
|
208
|
+
* token), plus a second record that stops after one, so a nested level with
|
|
209
|
+
* nothing under it renders beside one that is full.
|
|
210
|
+
*/
|
|
211
|
+
export const NestedSchemas: Story = {
|
|
212
|
+
parameters: {
|
|
213
|
+
docs: {
|
|
214
|
+
description: {
|
|
215
|
+
story:
|
|
216
|
+
'A described list whose records nest two levels deeper: an endpoint carrying an Authentication hash, which itself carries a Token hash of URL, lifetime and a bool. The second record fills only the first nested level, so a shallow branch sits next to a full one.',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
args: { value: nestedValue, schema: NestedSchema, colors },
|
|
221
|
+
play: async ({ canvasElement }) => {
|
|
222
|
+
const titles = [...canvasElement.querySelectorAll('.schema-view-item-title')].map((element) =>
|
|
223
|
+
(element.textContent ?? '').trim()
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await expect(titles).toEqual(['POST /orders', 'GET /orders/{id}']);
|
|
227
|
+
// The deepest level actually rendered: a label from the third tier, and the
|
|
228
|
+
// bool beneath it resolved to a word rather than printed as `true`.
|
|
229
|
+
await expect(canvasElement.textContent).toContain('Token URL');
|
|
230
|
+
await expect(canvasElement.textContent).toContain('Lifetime (s)');
|
|
231
|
+
await expect(canvasElement.textContent).not.toContain('[object Object]');
|
|
232
|
+
},
|
|
233
|
+
};
|