@qoretechnologies/reqraft 0.10.41-pr.95.g294e5aa → 0.10.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/codeSize/index.d.ts +37 -0
- package/dist/components/codeSize/index.d.ts.map +1 -0
- package/dist/components/codeSize/index.js +87 -0
- package/dist/components/codeSize/index.js.map +1 -0
- 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 +284 -42
- 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 +163 -75
- 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 +102 -87
- package/dist/components/form/engine/_structuredData/SchemaDataView.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 +22 -5
- 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 +57 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +143 -4
- 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/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +44 -26
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +32 -4
- 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/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 +148 -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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/codeSize/index.tsx +79 -0
- package/src/components/form/engine/CompactRow.tsx +407 -34
- 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 +183 -33
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +110 -103
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +132 -80
- package/src/components/form/engine/compactRowStyles.ts +44 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +163 -4
- 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/ArrayAuto.tsx +23 -4
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
- package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
- package/src/components/form/fields/auto/AutoFormField.tsx +31 -1
- 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/helpers/common.ts +11 -1
- package/src/helpers/optionUiTypes.ts +153 -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 +2 -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,
|
|
@@ -120,6 +125,21 @@ import {
|
|
|
120
125
|
} from './readFirst';
|
|
121
126
|
|
|
122
127
|
// Re-export types for consumers
|
|
128
|
+
/** The compact form's three status boxes. */
|
|
129
|
+
export type TFormEngineBoxKey = 'attention' | 'set' | 'optional';
|
|
130
|
+
|
|
131
|
+
/** Which parts of the compact toolbar to render — see `compactToolbar`. */
|
|
132
|
+
export interface IFormEngineToolbarParts {
|
|
133
|
+
/** The "Ready / 4 of 6 set / 67%" line and its meter. */
|
|
134
|
+
completion?: boolean;
|
|
135
|
+
/** The "Filter fields…" input. */
|
|
136
|
+
search?: boolean;
|
|
137
|
+
/** The field-picker dropdown (add optional fields, reset, revert). */
|
|
138
|
+
fields?: boolean;
|
|
139
|
+
/** The help button that opens the options dialog. */
|
|
140
|
+
help?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
export type IOptions = TQorusForm;
|
|
124
144
|
export type TFlatOptions = TQorusFlatForm;
|
|
125
145
|
export type TOption = IQorusFormField;
|
|
@@ -292,9 +312,10 @@ const getTypeFromOperator = (
|
|
|
292
312
|
return (operators[last(operatorData) as string]?.type as TQorusType) || null;
|
|
293
313
|
};
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
315
|
+
/** Re-exported from `helpers/common` so existing importers keep working; it
|
|
316
|
+
* lives there because `helpers/validations` needs it and cannot import this
|
|
317
|
+
* module (this module imports the validators). */
|
|
318
|
+
export { fixOperatorValue } from '../../../helpers/common';
|
|
298
319
|
|
|
299
320
|
/**
|
|
300
321
|
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
@@ -373,6 +394,23 @@ export const fixOptions = (
|
|
|
373
394
|
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
374
395
|
) {
|
|
375
396
|
obj = option.default_value as IQorusFormField;
|
|
397
|
+
} else if (
|
|
398
|
+
option.default_view === 'expression' &&
|
|
399
|
+
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
400
|
+
) {
|
|
401
|
+
// A field the schema declares as opening in expression mode starts as an
|
|
402
|
+
// EMPTY expression, not as its raw default. `isDefaultFunction` already
|
|
403
|
+
// makes the renderer open that way, so without this the data disagreed
|
|
404
|
+
// with what the operator was looking at: an expression editor whose
|
|
405
|
+
// value was still the plain default, and no `is_expression` on the field
|
|
406
|
+
// until the first edit.
|
|
407
|
+
obj = {
|
|
408
|
+
type,
|
|
409
|
+
value: {
|
|
410
|
+
args: [],
|
|
411
|
+
},
|
|
412
|
+
is_expression: true,
|
|
413
|
+
};
|
|
376
414
|
} else {
|
|
377
415
|
obj = {
|
|
378
416
|
type,
|
|
@@ -438,9 +476,19 @@ export const fixOptions = (
|
|
|
438
476
|
// every value declared the bare way: the collapsed row still showed its
|
|
439
477
|
// display name while the editor showed "—" and the value never reached
|
|
440
478
|
// the submitted data.
|
|
479
|
+
// `allowed_values: []` is NOT "no value is permitted" — it is the server
|
|
480
|
+
// saying it has no reference values to offer right now (the app-action
|
|
481
|
+
// catalogue ships exactly that, with an
|
|
482
|
+
// `option_reference_values_unavailable` message attached). An empty array
|
|
483
|
+
// is truthy, so testing the array itself made every such option erase the
|
|
484
|
+
// operator's stored value on the first render, before the
|
|
485
|
+
// connection-refreshed schema arrived to say otherwise — and the emptied
|
|
486
|
+
// form then autosaved over the draft, so the value was gone for good.
|
|
487
|
+
// Only an option that actually declares choices can have a value that is
|
|
488
|
+
// not one of them.
|
|
441
489
|
if (
|
|
442
490
|
newOption.value !== undefined &&
|
|
443
|
-
options?.[optionName]?.allowed_values &&
|
|
491
|
+
options?.[optionName]?.allowed_values?.length &&
|
|
444
492
|
!findAllowedValueOption(newOption.value, options?.[optionName]) &&
|
|
445
493
|
!isValueTemplate(newOption.value) &&
|
|
446
494
|
!options?.[optionName]?.multiselect &&
|
|
@@ -626,6 +674,37 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
626
674
|
* No-op in classic (non-compact) mode.
|
|
627
675
|
*/
|
|
628
676
|
compactPanelProps?: IReqorePanelProps;
|
|
677
|
+
/**
|
|
678
|
+
* Compact mode only: which status boxes start collapsed. Defaults to
|
|
679
|
+
* `['optional']` — a form opens on what is in use.
|
|
680
|
+
*
|
|
681
|
+
* Pass `['set', 'optional']` where the form is one item in a longer list and
|
|
682
|
+
* the reader is choosing WHICH to open rather than reading this one, e.g. a
|
|
683
|
+
* template's per-action option forms stacked in a drawer.
|
|
684
|
+
*
|
|
685
|
+
* Two escapes override this and are not negotiable: a box always opens while
|
|
686
|
+
* a search query is running (`ReqorePanel` unmounts collapsed content, so a
|
|
687
|
+
* match inside a closed box would be unreachable), and the Optional box stays
|
|
688
|
+
* open when every row is optional, because collapsing it would render a card
|
|
689
|
+
* that looks empty and broken.
|
|
690
|
+
*/
|
|
691
|
+
compactCollapsedGroups?: TFormEngineBoxKey[];
|
|
692
|
+
/**
|
|
693
|
+
* Compact mode only: which parts of the form's toolbar to show. `true`
|
|
694
|
+
* (default) shows all of it; `false` hides the whole thing.
|
|
695
|
+
*
|
|
696
|
+
* The parts already hide themselves when they have nothing to say — the
|
|
697
|
+
* completion meter needs at least one counted option, and the search and
|
|
698
|
+
* field-picker need more than one option to pick between — so reach for this
|
|
699
|
+
* only to hide something that WOULD have rendered. A per-action form asking
|
|
700
|
+
* for one value, say, has a meter reading 0/1 that tells the reader nothing
|
|
701
|
+
* they cannot see in the row below it.
|
|
702
|
+
*
|
|
703
|
+
* Hiding `search` removes the only way to run a query, which is what forces
|
|
704
|
+
* a collapsed box open; `compactCollapsedGroups` then fully determines what
|
|
705
|
+
* is open.
|
|
706
|
+
*/
|
|
707
|
+
compactToolbar?: boolean | IFormEngineToolbarParts;
|
|
629
708
|
/** Compact mode only: per-group display metadata (label / icon / subtitle /
|
|
630
709
|
* order) — the server only sends the bare group key. */
|
|
631
710
|
groups?: Record<string, IFormEngineGroup>;
|
|
@@ -792,6 +871,8 @@ const FormEngineImpl = ({
|
|
|
792
871
|
compactFlush = false,
|
|
793
872
|
compactNested = false,
|
|
794
873
|
compactPanelProps,
|
|
874
|
+
compactCollapsedGroups = ['optional'],
|
|
875
|
+
compactToolbar = true,
|
|
795
876
|
commitMode = 'immediate',
|
|
796
877
|
expandMode = 'single',
|
|
797
878
|
onCommit,
|
|
@@ -2307,6 +2388,11 @@ const FormEngineImpl = ({
|
|
|
2307
2388
|
allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
|
|
2308
2389
|
disabled={
|
|
2309
2390
|
options?.[optionName]?.disabled ||
|
|
2391
|
+
// a system-owned field states that as `readonly`; only the older
|
|
2392
|
+
// compat projection also sent `disabled`, so honouring only
|
|
2393
|
+
// `disabled` rendered an editable control for a value the user
|
|
2394
|
+
// cannot change (see CompactRow's `fieldReadonly`)
|
|
2395
|
+
(options?.[optionName] as { readonly?: boolean } | undefined)?.readonly ||
|
|
2310
2396
|
readOnly ||
|
|
2311
2397
|
!hasAllDependenciesFullfilled(
|
|
2312
2398
|
options?.[optionName]?.depends_on,
|
|
@@ -2503,6 +2589,14 @@ const FormEngineImpl = ({
|
|
|
2503
2589
|
]
|
|
2504
2590
|
);
|
|
2505
2591
|
|
|
2592
|
+
// `compactToolbar` resolved to a full object, so neither the context type nor
|
|
2593
|
+
// the toolbar itself has to re-default anything. `null` means no toolbar.
|
|
2594
|
+
const compactToolbarParts = useMemo(() => {
|
|
2595
|
+
if (compactToolbar === false) return null;
|
|
2596
|
+
const all = { completion: true, search: true, fields: true, help: true };
|
|
2597
|
+
return compactToolbar === true ? all : { ...all, ...compactToolbar };
|
|
2598
|
+
}, [JSON.stringify(compactToolbar)]);
|
|
2599
|
+
|
|
2506
2600
|
const compactToolbarContextValue = useMemo<ICompactToolbarContext>(
|
|
2507
2601
|
() => ({
|
|
2508
2602
|
readOnly,
|
|
@@ -2511,6 +2605,7 @@ const FormEngineImpl = ({
|
|
|
2511
2605
|
completion: readFirstCompletion,
|
|
2512
2606
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
2513
2607
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
2608
|
+
parts: compactToolbarParts ?? { completion: false, search: false, fields: false, help: false },
|
|
2514
2609
|
hasMultipleOptions: size(availableOptions) > 1,
|
|
2515
2610
|
compactQuery,
|
|
2516
2611
|
setCompactQuery,
|
|
@@ -2561,10 +2656,14 @@ const FormEngineImpl = ({
|
|
|
2561
2656
|
|
|
2562
2657
|
// The engine's own toolbar action, plus whatever the consumer added through
|
|
2563
2658
|
// `compactPanelProps.actions` — appended, so an outside action can never
|
|
2564
|
-
// knock the form's toolbar out of the header.
|
|
2659
|
+
// knock the form's toolbar out of the header. Only `compactToolbar={false}`
|
|
2660
|
+
// removes it, and then the header carries just the consumer's own actions.
|
|
2565
2661
|
const compactHeaderActions = useMemo(
|
|
2566
|
-
() => [
|
|
2567
|
-
|
|
2662
|
+
() => [
|
|
2663
|
+
...(compactToolbarParts ? [{ as: CompactToolbar, responsive: false }] : []),
|
|
2664
|
+
...(compactPanelProps?.actions || []),
|
|
2665
|
+
],
|
|
2666
|
+
[compactPanelProps?.actions, compactToolbarParts]
|
|
2568
2667
|
);
|
|
2569
2668
|
const renderCompact = () => {
|
|
2570
2669
|
const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
|
|
@@ -2577,9 +2676,40 @@ const FormEngineImpl = ({
|
|
|
2577
2676
|
if (requiredOnly && !(schema?.required || schema?.required_groups)) {
|
|
2578
2677
|
return false;
|
|
2579
2678
|
}
|
|
2580
|
-
|
|
2679
|
+
// A field that absorbs a sibling also answers for it: searching
|
|
2680
|
+
// "language" has to surface the row that now holds the language control,
|
|
2681
|
+
// or the field becomes unreachable through the filter.
|
|
2682
|
+
if (matchesQuery(optionName)) return true;
|
|
2683
|
+
const absorb = (schema as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2684
|
+
return !!absorb?.some((absorbedName) => matchesQuery(absorbedName));
|
|
2581
2685
|
};
|
|
2582
2686
|
|
|
2687
|
+
// Fields a sibling absorbs into its own container.
|
|
2688
|
+
//
|
|
2689
|
+
// A field declaring `absorb_fields: ['language']` renders that sibling's
|
|
2690
|
+
// control inside its own row instead of letting it have a row of its own.
|
|
2691
|
+
// The pair this exists for is a code editor and its language: they are one
|
|
2692
|
+
// decision — "what is this code, and in what language" — and reading them
|
|
2693
|
+
// as two unrelated rows one above the other is what the layout used to say.
|
|
2694
|
+
//
|
|
2695
|
+
// Absorbed fields stay in `grouped`, and therefore in the Set / Optional /
|
|
2696
|
+
// Needs-attention counts and in the completion meter. Only the ROW is
|
|
2697
|
+
// skipped. Dropping them from the buckets instead would leave the meter
|
|
2698
|
+
// ("13/18 set", computed from `availableOptions`) disagreeing with the box
|
|
2699
|
+
// badge beside it, which counts row entries.
|
|
2700
|
+
const absorbedBy: Record<string, string> = {};
|
|
2701
|
+
forEach(options, (option, optionName) => {
|
|
2702
|
+
const absorb = (option as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2703
|
+
if (!absorb?.length) return;
|
|
2704
|
+
absorb.forEach((absorbedName) => {
|
|
2705
|
+
// Only absorb a sibling that exists and is not already spoken for; a
|
|
2706
|
+
// schema naming a missing field must not make the field disappear.
|
|
2707
|
+
if (options?.[absorbedName] && !absorbedBy[absorbedName]) {
|
|
2708
|
+
absorbedBy[absorbedName] = optionName;
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2583
2713
|
// Group the options by their raw `group` key, remembering the order groups
|
|
2584
2714
|
// first appear in the schema. Each row carries a `hidden` flag.
|
|
2585
2715
|
const groupOrder: string[] = [];
|
|
@@ -2668,17 +2798,16 @@ const FormEngineImpl = ({
|
|
|
2668
2798
|
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
2669
2799
|
// unmet one-of group's members all land in attention and still rail up.
|
|
2670
2800
|
type TRowEntry = { name: string; hidden: boolean };
|
|
2671
|
-
|
|
2672
|
-
const buckets: Record<TBucketKey, Record<string, TRowEntry[]>> = {
|
|
2801
|
+
const buckets: Record<TFormEngineBoxKey, Record<string, TRowEntry[]>> = {
|
|
2673
2802
|
attention: {},
|
|
2674
2803
|
set: {},
|
|
2675
2804
|
optional: {},
|
|
2676
2805
|
};
|
|
2677
|
-
const bucketGroups: Record<
|
|
2806
|
+
const bucketGroups: Record<TFormEngineBoxKey, string[]> = { attention: [], set: [], optional: [] };
|
|
2678
2807
|
// Freeze the box of any field currently being edited (or whose one-of group
|
|
2679
2808
|
// has an edited member) to its last settled box — so finishing an edit that
|
|
2680
2809
|
// flips its status doesn't remount it in another box and steal focus.
|
|
2681
|
-
const stableBucketOf = (entry: TRowEntry):
|
|
2810
|
+
const stableBucketOf = (entry: TRowEntry): TFormEngineBoxKey => {
|
|
2682
2811
|
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
2683
2812
|
const groupBeingEdited =
|
|
2684
2813
|
!entry.hidden &&
|
|
@@ -2702,7 +2831,7 @@ const FormEngineImpl = ({
|
|
|
2702
2831
|
buckets[b][groupName].push(entry);
|
|
2703
2832
|
});
|
|
2704
2833
|
});
|
|
2705
|
-
const bucketCount = (b:
|
|
2834
|
+
const bucketCount = (b: TFormEngineBoxKey) =>
|
|
2706
2835
|
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2707
2836
|
|
|
2708
2837
|
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
@@ -2718,7 +2847,7 @@ const FormEngineImpl = ({
|
|
|
2718
2847
|
const showGroupSubLabel = (groupName: string) =>
|
|
2719
2848
|
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
2720
2849
|
const STATUS_BOXES: Array<{
|
|
2721
|
-
key:
|
|
2850
|
+
key: TFormEngineBoxKey;
|
|
2722
2851
|
label: string;
|
|
2723
2852
|
intent?: 'warning' | 'success';
|
|
2724
2853
|
icon: IReqoreIconName;
|
|
@@ -2732,6 +2861,11 @@ const FormEngineImpl = ({
|
|
|
2732
2861
|
// together at the first member's slot and rendered as a connected rail (flat
|
|
2733
2862
|
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
2734
2863
|
// are drawn per member). Narrow stacks fall back to flat rows.
|
|
2864
|
+
// The absorbed siblings a host row must render, in schema order. Empty for
|
|
2865
|
+
// every row that absorbs nothing, which is all of them by default.
|
|
2866
|
+
const absorbedNamesFor = (hostName: string): string[] =>
|
|
2867
|
+
Object.keys(absorbedBy).filter((absorbedName) => absorbedBy[absorbedName] === hostName);
|
|
2868
|
+
|
|
2735
2869
|
const renderGroupRows = (names: Array<{ name: string; hidden: boolean }>) => {
|
|
2736
2870
|
const renderRow = (
|
|
2737
2871
|
entry: { name: string; hidden?: boolean },
|
|
@@ -2754,6 +2888,9 @@ const FormEngineImpl = ({
|
|
|
2754
2888
|
clustered={clustered}
|
|
2755
2889
|
clusterFirst={clusterFirst}
|
|
2756
2890
|
clusterLast={clusterLast}
|
|
2891
|
+
// The siblings this row renders inside itself instead of leaving
|
|
2892
|
+
// them a row of their own — see `absorbedBy`.
|
|
2893
|
+
absorbedFields={absorbedNamesFor(entry.name)}
|
|
2757
2894
|
/>
|
|
2758
2895
|
);
|
|
2759
2896
|
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
@@ -2762,6 +2899,10 @@ const FormEngineImpl = ({
|
|
|
2762
2899
|
const emitted = new Set<string>();
|
|
2763
2900
|
const groupOf = (name: string) =>
|
|
2764
2901
|
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
2902
|
+
// An absorbed field keeps its place in the buckets — so the Set /
|
|
2903
|
+
// Optional counts and the completion meter still see it — but its host
|
|
2904
|
+
// renders its control, so it gets no row of its own here.
|
|
2905
|
+
names = names.filter((entry) => !absorbedBy[entry.name]);
|
|
2765
2906
|
return names.map((entry) => {
|
|
2766
2907
|
const grp = groupOf(entry.name);
|
|
2767
2908
|
if (!grp) return renderRow(entry, false);
|
|
@@ -2812,6 +2953,10 @@ const FormEngineImpl = ({
|
|
|
2812
2953
|
$flush={compactFlush || compactNested}
|
|
2813
2954
|
>
|
|
2814
2955
|
<StyledCompactPanel
|
|
2956
|
+
// Mirrors the toolbar's own search-row gate: when no search
|
|
2957
|
+
// row renders, the header is a thin strip and sits tight to
|
|
2958
|
+
// the first status box (see StyledCompactPanel).
|
|
2959
|
+
$tightHeader={!(compactToolbarParts?.search && size(availableOptions) > 1)}
|
|
2815
2960
|
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
2816
2961
|
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
2817
2962
|
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
@@ -2878,24 +3023,29 @@ const FormEngineImpl = ({
|
|
|
2878
3023
|
minimal
|
|
2879
3024
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
2880
3025
|
collapsible
|
|
2881
|
-
//
|
|
2882
|
-
//
|
|
2883
|
-
//
|
|
2884
|
-
//
|
|
2885
|
-
//
|
|
2886
|
-
//
|
|
2887
|
-
//
|
|
2888
|
-
//
|
|
2889
|
-
//
|
|
2890
|
-
//
|
|
2891
|
-
//
|
|
2892
|
-
//
|
|
2893
|
-
//
|
|
2894
|
-
//
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2898
|
-
|
|
3026
|
+
// Which boxes start closed is the consumer's call
|
|
3027
|
+
// (`compactCollapsedGroups`, default `['optional']`: the
|
|
3028
|
+
// Optional box holds every not-yet-added field, so a form
|
|
3029
|
+
// opens on what is in use). Two rules override it:
|
|
3030
|
+
//
|
|
3031
|
+
// A SEARCH forces every box open — ReqorePanel unmounts
|
|
3032
|
+
// collapsed content, so a match inside a closed box would
|
|
3033
|
+
// be unreachable. (isCollapsed is the panel's controllable
|
|
3034
|
+
// state; manual toggling still works with no query set.)
|
|
3035
|
+
//
|
|
3036
|
+
// The Optional box stays open when it is the ONLY box,
|
|
3037
|
+
// because collapsing it renders a card that looks empty and
|
|
3038
|
+
// broken: an auth profile's Authorization block, whose nine
|
|
3039
|
+
// requirement fields are all optional, showed as a titled
|
|
3040
|
+
// card containing one collapsed "Optional 9" strip and
|
|
3041
|
+
// nothing else. This does not generalise to the other two —
|
|
3042
|
+
// a lone Set box collapsed is a deliberate ask, and its
|
|
3043
|
+
// header still reports what is in there.
|
|
3044
|
+
isCollapsed={
|
|
3045
|
+
compactCollapsedGroups.includes(box.key) &&
|
|
3046
|
+
!query &&
|
|
3047
|
+
!(box.key === 'optional' && onlyOptionalRows)
|
|
3048
|
+
}
|
|
2899
3049
|
label={
|
|
2900
3050
|
<StyledGroupHeader>
|
|
2901
3051
|
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { ReqoreControlGroup, ReqoreP, ReqorePanel } from '@qoretechnologies/reqore';
|
|
2
1
|
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
2
|
import { expect } from 'storybook/test';
|
|
4
|
-
import styled from 'styled-components';
|
|
5
3
|
import { SchemaDataView } from './SchemaDataView';
|
|
6
4
|
|
|
7
5
|
/**
|
|
@@ -57,6 +55,78 @@ const methodValue = [
|
|
|
57
55
|
{ type: 'hash', value: { name: 'onConnect', desc: 'connection lifecycle hook' } },
|
|
58
56
|
];
|
|
59
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
|
+
|
|
60
130
|
const colors = { key: '#f1f0ee', muted: '#a0a0a0', border: '#a0a0a066', accent: '#3b8eea' };
|
|
61
131
|
|
|
62
132
|
export const Schemes: Story = {
|
|
@@ -108,119 +178,56 @@ export const Undescribed: Story = {
|
|
|
108
178
|
},
|
|
109
179
|
play: async ({ canvasElement }) => {
|
|
110
180
|
await expect(canvasElement.textContent).toContain('legacy_flag');
|
|
181
|
+
|
|
182
|
+
// A field name renders on ONE line. The label column is measured from the
|
|
183
|
+
// longest name, and the label carries `overflow-wrap: anywhere` — so an
|
|
184
|
+
// under-measured column does not ellipsise, it breaks the name in half.
|
|
185
|
+
// That is exactly what happened when the labels became uppercase and the
|
|
186
|
+
// measurement was still in `ch`, which knows nothing about the tracking:
|
|
187
|
+
// `LEGACY_FLAG` came out over two lines.
|
|
188
|
+
//
|
|
189
|
+
// Asserted as geometry rather than as a width, because the width is the
|
|
190
|
+
// thing that keeps being wrong — the invariant is that the name fits.
|
|
191
|
+
const label = [...canvasElement.querySelectorAll<HTMLElement>('.schema-view-fields > *')].find(
|
|
192
|
+
(element) => (element.textContent ?? '').trim().toLowerCase() === 'legacy_flag'
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await expect(label).toBeTruthy();
|
|
196
|
+
|
|
197
|
+
const lineHeight = parseFloat(getComputedStyle(label!).lineHeight) || 16;
|
|
198
|
+
|
|
199
|
+
await expect(label!.offsetHeight).toBeLessThan(lineHeight * 1.6);
|
|
111
200
|
},
|
|
112
201
|
};
|
|
113
202
|
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* The identifying value is the thing a reader scans for, and size alone was not
|
|
118
|
-
* carrying it far enough. These render the SAME data under candidate
|
|
119
|
-
* treatments, side by side, so the choice is made by looking rather than by
|
|
120
|
-
* describing.
|
|
203
|
+
/**
|
|
204
|
+
* The deeper case: hash values holding hash values.
|
|
121
205
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const StyledVariant = styled.div`
|
|
130
|
-
/* B — tinted background: the heading sits on its own surface. */
|
|
131
|
-
&.variant-background .schema-view-item-title {
|
|
132
|
-
display: inline-block;
|
|
133
|
-
background: #ffffff0f;
|
|
134
|
-
border-radius: 4px;
|
|
135
|
-
padding: 1px 8px;
|
|
136
|
-
margin-left: -8px;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/* C — indented rows: the heading keeps the left edge to itself, and the
|
|
140
|
-
supporting fields step in under it. Costs no colour at all. */
|
|
141
|
-
&.variant-indent .schema-view-fields {
|
|
142
|
-
padding-left: 14px;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* D — accent bar: a rule of its own beside the heading, in the app's accent.
|
|
146
|
-
Sanctioned emphasis (BRAND_DESIGN §1 names accent bars explicitly). */
|
|
147
|
-
&.variant-accent .schema-view-item-title {
|
|
148
|
-
border-left: 3px solid #7c5cff;
|
|
149
|
-
padding-left: 8px;
|
|
150
|
-
margin-left: -11px;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/* E — everything: accent bar + tinted surface + indented rows. */
|
|
154
|
-
&.variant-all .schema-view-item-title {
|
|
155
|
-
display: inline-block;
|
|
156
|
-
background: #7c5cff1f;
|
|
157
|
-
border-left: 3px solid #7c5cff;
|
|
158
|
-
border-radius: 0 4px 4px 0;
|
|
159
|
-
padding: 1px 8px;
|
|
160
|
-
margin-left: -11px;
|
|
161
|
-
}
|
|
162
|
-
&.variant-all .schema-view-fields {
|
|
163
|
-
padding-left: 14px;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/* F — colour only, applied as a FILL behind the text rather than to the text
|
|
167
|
-
itself: an intent used as text fails contrast (a muted label reads at
|
|
168
|
-
1.3:1), which is why this is a chip and not a coloured word. */
|
|
169
|
-
&.variant-tint .schema-view-item-title {
|
|
170
|
-
display: inline-block;
|
|
171
|
-
background: #2e7d5b33;
|
|
172
|
-
border-radius: 4px;
|
|
173
|
-
padding: 1px 8px;
|
|
174
|
-
margin-left: -8px;
|
|
175
|
-
}
|
|
176
|
-
`;
|
|
177
|
-
|
|
178
|
-
const VARIANTS: { key: string; label: string; note: string }[] = [
|
|
179
|
-
{ key: 'variant-none', label: 'A — shipped treatment', note: 'accent bar + tinted fill + indented rows' },
|
|
180
|
-
{ key: 'variant-indent', label: 'B — indent the rows', note: 'no colour; heading owns the edge' },
|
|
181
|
-
{ key: 'variant-background', label: 'C — neutral fill', note: 'white 6% chip behind the name' },
|
|
182
|
-
{ key: 'variant-accent', label: 'D — accent bar', note: 'brand bar beside the name' },
|
|
183
|
-
{ key: 'variant-tint', label: 'E — coloured fill', note: 'intent as a FILL, never as text' },
|
|
184
|
-
{ key: 'variant-all', label: 'F — bar + fill + indent', note: 'all three together' },
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
export const HeadingEmphasisPrototypes: Story = {
|
|
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 = {
|
|
188
212
|
parameters: {
|
|
189
|
-
// Six variants stacked do not fit the default 720px capture, and a
|
|
190
|
-
// comparison you have to scroll is not a comparison. Qlip's capture viewport
|
|
191
|
-
// is a separate setting from Storybook's preview viewport, so it is pinned
|
|
192
|
-
// here explicitly — setting only the preview one yields a full-height story
|
|
193
|
-
// and a cropped snapshot.
|
|
194
|
-
qlip: { viewport: { width: 1280, height: 1100 } },
|
|
195
213
|
docs: {
|
|
196
214
|
description: {
|
|
197
215
|
story:
|
|
198
|
-
'
|
|
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.',
|
|
199
217
|
},
|
|
200
218
|
},
|
|
201
219
|
},
|
|
202
|
-
|
|
203
|
-
// screen or they cannot be compared at a glance, which is the whole point.
|
|
204
|
-
args: { value: methodValue.slice(0, 2), schema: MethodSchema, colors },
|
|
205
|
-
render: (args) => (
|
|
206
|
-
<ReqoreControlGroup vertical fluid gapSize='big'>
|
|
207
|
-
{VARIANTS.map((variant) => (
|
|
208
|
-
<ReqorePanel key={variant.key} label={variant.label} minimal flat padded size='small'>
|
|
209
|
-
<ReqoreP size='small' effect={{ opacity: 0.6 }} style={{ marginBottom: 6 }}>
|
|
210
|
-
{variant.note}
|
|
211
|
-
</ReqoreP>
|
|
212
|
-
<StyledVariant className={variant.key}>
|
|
213
|
-
<SchemaDataView {...args} />
|
|
214
|
-
</StyledVariant>
|
|
215
|
-
</ReqorePanel>
|
|
216
|
-
))}
|
|
217
|
-
</ReqoreControlGroup>
|
|
218
|
-
),
|
|
220
|
+
args: { value: nestedValue, schema: NestedSchema, colors },
|
|
219
221
|
play: async ({ canvasElement }) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await expect(canvasElement.querySelectorAll('.schema-view-item-title')).toHaveLength(
|
|
223
|
-
VARIANTS.length * 2
|
|
222
|
+
const titles = [...canvasElement.querySelectorAll('.schema-view-item-title')].map((element) =>
|
|
223
|
+
(element.textContent ?? '').trim()
|
|
224
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]');
|
|
225
232
|
},
|
|
226
233
|
};
|