@qoretechnologies/reqraft 0.10.39-pr.99.gdbafa34 → 0.10.39
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/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 +365 -30
- 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 +6 -8
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +24 -45
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +211 -84
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +37 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +335 -0
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -0
- package/dist/components/form/engine/compactRowStyles.d.ts +13 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +30 -5
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +0 -8
- 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 +73 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +175 -5
- 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 +51 -21
- 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 +4 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +29 -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/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +12 -2
- package/dist/components/form/fields/template/TemplateField.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/validations.d.ts +85 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +802 -76
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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 +450 -31
- package/src/components/form/engine/CompactToolbar.tsx +16 -21
- package/src/components/form/engine/FormEngine.stories.tsx +804 -17
- package/src/components/form/engine/FormEngine.tsx +207 -105
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +233 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +630 -0
- package/src/components/form/engine/compactRowStyles.ts +44 -2
- package/src/components/form/engine/compactToolbarContext.ts +0 -8
- package/src/components/form/engine/readFirst.ts +201 -5
- 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 +35 -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 +30 -1
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/template/TemplateField.tsx +16 -1
- 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/validations.ts +1105 -80
- 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,
|
|
@@ -107,6 +112,7 @@ import {
|
|
|
107
112
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
108
113
|
import {
|
|
109
114
|
TReadFirstStatus,
|
|
115
|
+
findAllowedValueOption,
|
|
110
116
|
getFirstAttentionOptionName,
|
|
111
117
|
getOptionGroup,
|
|
112
118
|
getOptionGroupLabel,
|
|
@@ -119,21 +125,6 @@ import {
|
|
|
119
125
|
} from './readFirst';
|
|
120
126
|
|
|
121
127
|
// Re-export types for consumers
|
|
122
|
-
/** The compact form's three status boxes. */
|
|
123
|
-
export type TFormEngineBoxKey = 'attention' | 'set' | 'optional';
|
|
124
|
-
|
|
125
|
-
/** Which parts of the compact toolbar to render — see `compactToolbar`. */
|
|
126
|
-
export interface IFormEngineToolbarParts {
|
|
127
|
-
/** The "Ready / 4 of 6 set / 67%" line and its meter. */
|
|
128
|
-
completion?: boolean;
|
|
129
|
-
/** The "Filter fields…" input. */
|
|
130
|
-
search?: boolean;
|
|
131
|
-
/** The field-picker dropdown (add optional fields, reset, revert). */
|
|
132
|
-
fields?: boolean;
|
|
133
|
-
/** The help button that opens the options dialog. */
|
|
134
|
-
help?: boolean;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
128
|
export type IOptions = TQorusForm;
|
|
138
129
|
export type TFlatOptions = TQorusFlatForm;
|
|
139
130
|
export type TOption = IQorusFormField;
|
|
@@ -306,9 +297,10 @@ const getTypeFromOperator = (
|
|
|
306
297
|
return (operators[last(operatorData) as string]?.type as TQorusType) || null;
|
|
307
298
|
};
|
|
308
299
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
300
|
+
/** Re-exported from `helpers/common` so existing importers keep working; it
|
|
301
|
+
* lives there because `helpers/validations` needs it and cannot import this
|
|
302
|
+
* module (this module imports the validators). */
|
|
303
|
+
export { fixOperatorValue } from '../../../helpers/common';
|
|
312
304
|
|
|
313
305
|
/**
|
|
314
306
|
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
@@ -387,6 +379,23 @@ export const fixOptions = (
|
|
|
387
379
|
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
388
380
|
) {
|
|
389
381
|
obj = option.default_value as IQorusFormField;
|
|
382
|
+
} else if (
|
|
383
|
+
option.default_view === 'expression' &&
|
|
384
|
+
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
385
|
+
) {
|
|
386
|
+
// A field the schema declares as opening in expression mode starts as an
|
|
387
|
+
// EMPTY expression, not as its raw default. `isDefaultFunction` already
|
|
388
|
+
// makes the renderer open that way, so without this the data disagreed
|
|
389
|
+
// with what the operator was looking at: an expression editor whose
|
|
390
|
+
// value was still the plain default, and no `is_expression` on the field
|
|
391
|
+
// until the first edit.
|
|
392
|
+
obj = {
|
|
393
|
+
type,
|
|
394
|
+
value: {
|
|
395
|
+
args: [],
|
|
396
|
+
},
|
|
397
|
+
is_expression: true,
|
|
398
|
+
};
|
|
390
399
|
} else {
|
|
391
400
|
obj = {
|
|
392
401
|
type,
|
|
@@ -444,13 +453,28 @@ export const fixOptions = (
|
|
|
444
453
|
};
|
|
445
454
|
}
|
|
446
455
|
|
|
456
|
+
// A value that is not one of the declared choices is dropped. What counts
|
|
457
|
+
// as "one of the choices" is `findAllowedValueOption` — the same predicate
|
|
458
|
+
// the read-first row uses to LABEL a value — because a value the row can
|
|
459
|
+
// name is by definition a value the form must keep. Inlining a narrower
|
|
460
|
+
// test here (envelope and `name`, but not a bare `value`) silently erased
|
|
461
|
+
// every value declared the bare way: the collapsed row still showed its
|
|
462
|
+
// display name while the editor showed "—" and the value never reached
|
|
463
|
+
// the submitted data.
|
|
464
|
+
// `allowed_values: []` is NOT "no value is permitted" — it is the server
|
|
465
|
+
// saying it has no reference values to offer right now (the app-action
|
|
466
|
+
// catalogue ships exactly that, with an
|
|
467
|
+
// `option_reference_values_unavailable` message attached). An empty array
|
|
468
|
+
// is truthy, so testing the array itself made every such option erase the
|
|
469
|
+
// operator's stored value on the first render, before the
|
|
470
|
+
// connection-refreshed schema arrived to say otherwise — and the emptied
|
|
471
|
+
// form then autosaved over the draft, so the value was gone for good.
|
|
472
|
+
// Only an option that actually declares choices can have a value that is
|
|
473
|
+
// not one of them.
|
|
447
474
|
if (
|
|
448
475
|
newOption.value !== undefined &&
|
|
449
|
-
options?.[optionName]?.allowed_values &&
|
|
450
|
-
!options?.[optionName]
|
|
451
|
-
(allowedValue: any) =>
|
|
452
|
-
allowedValue.value?.value === newOption.value || allowedValue.name === newOption.value
|
|
453
|
-
) &&
|
|
476
|
+
options?.[optionName]?.allowed_values?.length &&
|
|
477
|
+
!findAllowedValueOption(newOption.value, options?.[optionName]) &&
|
|
454
478
|
!isValueTemplate(newOption.value) &&
|
|
455
479
|
!options?.[optionName]?.multiselect &&
|
|
456
480
|
!options?.[optionName]?.allowed_values_creatable
|
|
@@ -635,37 +659,6 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
635
659
|
* No-op in classic (non-compact) mode.
|
|
636
660
|
*/
|
|
637
661
|
compactPanelProps?: IReqorePanelProps;
|
|
638
|
-
/**
|
|
639
|
-
* Compact mode only: which status boxes start collapsed. Defaults to
|
|
640
|
-
* `['optional']` — a form opens on what is in use.
|
|
641
|
-
*
|
|
642
|
-
* Pass `['set', 'optional']` where the form is one item in a longer list and
|
|
643
|
-
* the reader is choosing WHICH to open rather than reading this one, e.g. a
|
|
644
|
-
* template's per-action option forms stacked in a drawer.
|
|
645
|
-
*
|
|
646
|
-
* Two escapes override this and are not negotiable: a box always opens while
|
|
647
|
-
* a search query is running (`ReqorePanel` unmounts collapsed content, so a
|
|
648
|
-
* match inside a closed box would be unreachable), and the Optional box stays
|
|
649
|
-
* open when every row is optional, because collapsing it would render a card
|
|
650
|
-
* that looks empty and broken.
|
|
651
|
-
*/
|
|
652
|
-
compactCollapsedGroups?: TFormEngineBoxKey[];
|
|
653
|
-
/**
|
|
654
|
-
* Compact mode only: which parts of the form's toolbar to show. `true`
|
|
655
|
-
* (default) shows all of it; `false` hides the whole thing.
|
|
656
|
-
*
|
|
657
|
-
* The parts already hide themselves when they have nothing to say — the
|
|
658
|
-
* completion meter needs at least one counted option, and the search and
|
|
659
|
-
* field-picker need more than one option to pick between — so reach for this
|
|
660
|
-
* only to hide something that WOULD have rendered. A per-action form asking
|
|
661
|
-
* for one value, say, has a meter reading 0/1 that tells the reader nothing
|
|
662
|
-
* they cannot see in the row below it.
|
|
663
|
-
*
|
|
664
|
-
* Hiding `search` removes the only way to run a query, which is what forces
|
|
665
|
-
* a collapsed box open; `compactCollapsedGroups` then fully determines what
|
|
666
|
-
* is open.
|
|
667
|
-
*/
|
|
668
|
-
compactToolbar?: boolean | IFormEngineToolbarParts;
|
|
669
662
|
/** Compact mode only: per-group display metadata (label / icon / subtitle /
|
|
670
663
|
* order) — the server only sends the bare group key. */
|
|
671
664
|
groups?: Record<string, IFormEngineGroup>;
|
|
@@ -761,6 +754,27 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
761
754
|
*/
|
|
762
755
|
autoFocusFirstRequired?: boolean;
|
|
763
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Opt-in: on mount, OPEN the first row that needs attention — without taking
|
|
759
|
+
* focus.
|
|
760
|
+
*
|
|
761
|
+
* `autoFocusFirstRequired` fuses two decisions that are not the same: which
|
|
762
|
+
* row is open (layout) and where the caret is (focus). Focus is the half that
|
|
763
|
+
* can be stolen, so that flag waits for focus to be free — and a form mounted
|
|
764
|
+
* BY a click never sees free focus, because the button that mounted it still
|
|
765
|
+
* has it. Adding a list item is exactly that case: the row the author must
|
|
766
|
+
* fill stayed shut behind a second click, and the flag that exists to prevent
|
|
767
|
+
* it could not fire.
|
|
768
|
+
*
|
|
769
|
+
* Opening a row takes nothing from the user, so this half needs no guard. Use
|
|
770
|
+
* it where the mount is already the answer to a deliberate action; use
|
|
771
|
+
* `autoFocusFirstRequired` where the form is the destination and the caret
|
|
772
|
+
* should land in it. Setting both keeps the focusing behaviour. Same one-shot
|
|
773
|
+
* contract: it fires on the first render with focusable content and then never
|
|
774
|
+
* again for the life of the instance. No-op in classic mode. Default: off.
|
|
775
|
+
*/
|
|
776
|
+
expandFirstRequired?: boolean;
|
|
777
|
+
|
|
764
778
|
/**
|
|
765
779
|
* Names of read-first rows to open on mount, in addition to whatever the
|
|
766
780
|
* user opens afterwards.
|
|
@@ -811,8 +825,6 @@ const FormEngineImpl = ({
|
|
|
811
825
|
compactFlush = false,
|
|
812
826
|
compactNested = false,
|
|
813
827
|
compactPanelProps,
|
|
814
|
-
compactCollapsedGroups = ['optional'],
|
|
815
|
-
compactToolbar = true,
|
|
816
828
|
commitMode = 'immediate',
|
|
817
829
|
expandMode = 'single',
|
|
818
830
|
onCommit,
|
|
@@ -831,6 +843,7 @@ const FormEngineImpl = ({
|
|
|
831
843
|
rendererOnlyUiTypes,
|
|
832
844
|
inheritedFromParent,
|
|
833
845
|
autoFocusFirstRequired,
|
|
846
|
+
expandFirstRequired,
|
|
834
847
|
initialExpandedOptions,
|
|
835
848
|
...rest
|
|
836
849
|
}: IFormEngineProps) => {
|
|
@@ -879,6 +892,47 @@ const FormEngineImpl = ({
|
|
|
879
892
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
880
893
|
// Which options are expanded into their editor (several can be open at once).
|
|
881
894
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
895
|
+
|
|
896
|
+
// --- Reveal a status box's content when it is opened ----------------------
|
|
897
|
+
//
|
|
898
|
+
// The status boxes stack, so the last one ("Optional", which holds every
|
|
899
|
+
// not-yet-added field) sits at the bottom of the form — frequently at the
|
|
900
|
+
// bottom of the scroll container too. Opening it mounts its rows BELOW the
|
|
901
|
+
// fold: the box's own header is all that stays on screen, the click appears to
|
|
902
|
+
// have done nothing, and the fields it just revealed have to be hunted for by
|
|
903
|
+
// scrolling.
|
|
904
|
+
//
|
|
905
|
+
// The box that was just opened, read by the layout effect below. Held as state
|
|
906
|
+
// rather than acted on inside the collapse handler because the handler runs
|
|
907
|
+
// BEFORE React re-renders — at that moment the rows do not exist yet, so the
|
|
908
|
+
// box has not grown and there is nothing to scroll to. A layout effect runs
|
|
909
|
+
// after the DOM is updated and before paint, which makes the reveal
|
|
910
|
+
// deterministic instead of a guessed delay.
|
|
911
|
+
const [openedStatusBox, setOpenedStatusBox] = useState<string | undefined>(undefined);
|
|
912
|
+
const statusBoxRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
913
|
+
useLayoutEffect(() => {
|
|
914
|
+
if (!openedStatusBox) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const element = statusBoxRefs.current[openedStatusBox];
|
|
918
|
+
// Consume it either way: a box that has gone (a search narrowed it out of
|
|
919
|
+
// existence) must not leave a pending reveal armed for a later render.
|
|
920
|
+
setOpenedStatusBox(undefined);
|
|
921
|
+
// `scrollIntoView` is not implemented in jsdom, and a missing reveal must
|
|
922
|
+
// never break a form.
|
|
923
|
+
if (!element?.scrollIntoView) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// 'nearest' scrolls the MINIMUM needed: nothing at all when the box is
|
|
927
|
+
// already fully visible, and a top-alignment when it is taller than the
|
|
928
|
+
// viewport (header plus the first rows — the right answer for a long
|
|
929
|
+
// Optional list). Anything else moves the page when it did not need to.
|
|
930
|
+
element.scrollIntoView({
|
|
931
|
+
block: 'nearest',
|
|
932
|
+
behavior:
|
|
933
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth',
|
|
934
|
+
});
|
|
935
|
+
}, [openedStatusBox]);
|
|
882
936
|
// Remembers each row's last settled status box, so an actively-edited field
|
|
883
937
|
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
884
938
|
// another box mid-edit and stealing focus. Keyed by option name.
|
|
@@ -1911,7 +1965,11 @@ const FormEngineImpl = ({
|
|
|
1911
1965
|
// (or below-the-fold) form is never scrolled into view on mount.
|
|
1912
1966
|
const autoFocusNameRef = useRef<string | undefined>(undefined);
|
|
1913
1967
|
useEffect(() => {
|
|
1914
|
-
|
|
1968
|
+
// Two decisions, one scan. `autoFocusFirstRequired` opens the row AND puts
|
|
1969
|
+
// the caret in it; `expandFirstRequired` only opens it. Setting both keeps
|
|
1970
|
+
// the focusing behaviour, since focusing implies opening.
|
|
1971
|
+
const wantsFocus = !!autoFocusFirstRequired;
|
|
1972
|
+
if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
|
|
1915
1973
|
return;
|
|
1916
1974
|
}
|
|
1917
1975
|
if (hasAutoFocusedRef.current) {
|
|
@@ -1929,8 +1987,14 @@ const FormEngineImpl = ({
|
|
|
1929
1987
|
// clean mount focus rests on the body, so the intended "drop into the first
|
|
1930
1988
|
// field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
|
|
1931
1989
|
// retries once focus is free.
|
|
1990
|
+
//
|
|
1991
|
+
// Only the FOCUSING variant waits: opening a row moves no caret, so there is
|
|
1992
|
+
// nothing to steal and nothing to wait for. Making the expand-only variant
|
|
1993
|
+
// wait here would make it a no-op in its main use — a sub-form mounted by a
|
|
1994
|
+
// click, where the button that mounted it still holds focus and never gives
|
|
1995
|
+
// it up.
|
|
1932
1996
|
const active = document.activeElement as HTMLElement | null;
|
|
1933
|
-
if (active && active !== document.body) {
|
|
1997
|
+
if (wantsFocus && active && active !== document.body) {
|
|
1934
1998
|
return;
|
|
1935
1999
|
}
|
|
1936
2000
|
|
|
@@ -1958,7 +2022,10 @@ const FormEngineImpl = ({
|
|
|
1958
2022
|
|
|
1959
2023
|
if (target) {
|
|
1960
2024
|
// Set the ref before the state update so CompactRow's focus timer sees it.
|
|
1961
|
-
|
|
2025
|
+
// Expand-only leaves it unset, which is what keeps the caret where it is.
|
|
2026
|
+
if (wantsFocus) {
|
|
2027
|
+
autoFocusNameRef.current = target;
|
|
2028
|
+
}
|
|
1962
2029
|
setExpandedOptions((prev) =>
|
|
1963
2030
|
prev.includes(target) ? prev
|
|
1964
2031
|
: expandMode === 'multi' ? [...prev, target]
|
|
@@ -1967,6 +2034,7 @@ const FormEngineImpl = ({
|
|
|
1967
2034
|
}
|
|
1968
2035
|
}, [
|
|
1969
2036
|
autoFocusFirstRequired,
|
|
2037
|
+
expandFirstRequired,
|
|
1970
2038
|
compact,
|
|
1971
2039
|
options,
|
|
1972
2040
|
availableOptions,
|
|
@@ -2468,14 +2536,6 @@ const FormEngineImpl = ({
|
|
|
2468
2536
|
]
|
|
2469
2537
|
);
|
|
2470
2538
|
|
|
2471
|
-
// `compactToolbar` resolved to a full object, so neither the context type nor
|
|
2472
|
-
// the toolbar itself has to re-default anything. `null` means no toolbar.
|
|
2473
|
-
const compactToolbarParts = useMemo(() => {
|
|
2474
|
-
if (compactToolbar === false) return null;
|
|
2475
|
-
const all = { completion: true, search: true, fields: true, help: true };
|
|
2476
|
-
return compactToolbar === true ? all : { ...all, ...compactToolbar };
|
|
2477
|
-
}, [JSON.stringify(compactToolbar)]);
|
|
2478
|
-
|
|
2479
2539
|
const compactToolbarContextValue = useMemo<ICompactToolbarContext>(
|
|
2480
2540
|
() => ({
|
|
2481
2541
|
readOnly,
|
|
@@ -2484,7 +2544,6 @@ const FormEngineImpl = ({
|
|
|
2484
2544
|
completion: readFirstCompletion,
|
|
2485
2545
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
2486
2546
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
2487
|
-
parts: compactToolbarParts ?? { completion: false, search: false, fields: false, help: false },
|
|
2488
2547
|
hasMultipleOptions: size(availableOptions) > 1,
|
|
2489
2548
|
compactQuery,
|
|
2490
2549
|
setCompactQuery,
|
|
@@ -2535,14 +2594,10 @@ const FormEngineImpl = ({
|
|
|
2535
2594
|
|
|
2536
2595
|
// The engine's own toolbar action, plus whatever the consumer added through
|
|
2537
2596
|
// `compactPanelProps.actions` — appended, so an outside action can never
|
|
2538
|
-
// knock the form's toolbar out of the header.
|
|
2539
|
-
// removes it, and then the header carries just the consumer's own actions.
|
|
2597
|
+
// knock the form's toolbar out of the header.
|
|
2540
2598
|
const compactHeaderActions = useMemo(
|
|
2541
|
-
() => [
|
|
2542
|
-
|
|
2543
|
-
...(compactPanelProps?.actions || []),
|
|
2544
|
-
],
|
|
2545
|
-
[compactPanelProps?.actions, compactToolbarParts]
|
|
2599
|
+
() => [{ as: CompactToolbar, responsive: false }, ...(compactPanelProps?.actions || [])],
|
|
2600
|
+
[compactPanelProps?.actions]
|
|
2546
2601
|
);
|
|
2547
2602
|
const renderCompact = () => {
|
|
2548
2603
|
const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
|
|
@@ -2555,9 +2610,40 @@ const FormEngineImpl = ({
|
|
|
2555
2610
|
if (requiredOnly && !(schema?.required || schema?.required_groups)) {
|
|
2556
2611
|
return false;
|
|
2557
2612
|
}
|
|
2558
|
-
|
|
2613
|
+
// A field that absorbs a sibling also answers for it: searching
|
|
2614
|
+
// "language" has to surface the row that now holds the language control,
|
|
2615
|
+
// or the field becomes unreachable through the filter.
|
|
2616
|
+
if (matchesQuery(optionName)) return true;
|
|
2617
|
+
const absorb = (schema as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2618
|
+
return !!absorb?.some((absorbedName) => matchesQuery(absorbedName));
|
|
2559
2619
|
};
|
|
2560
2620
|
|
|
2621
|
+
// Fields a sibling absorbs into its own container.
|
|
2622
|
+
//
|
|
2623
|
+
// A field declaring `absorb_fields: ['language']` renders that sibling's
|
|
2624
|
+
// control inside its own row instead of letting it have a row of its own.
|
|
2625
|
+
// The pair this exists for is a code editor and its language: they are one
|
|
2626
|
+
// decision — "what is this code, and in what language" — and reading them
|
|
2627
|
+
// as two unrelated rows one above the other is what the layout used to say.
|
|
2628
|
+
//
|
|
2629
|
+
// Absorbed fields stay in `grouped`, and therefore in the Set / Optional /
|
|
2630
|
+
// Needs-attention counts and in the completion meter. Only the ROW is
|
|
2631
|
+
// skipped. Dropping them from the buckets instead would leave the meter
|
|
2632
|
+
// ("13/18 set", computed from `availableOptions`) disagreeing with the box
|
|
2633
|
+
// badge beside it, which counts row entries.
|
|
2634
|
+
const absorbedBy: Record<string, string> = {};
|
|
2635
|
+
forEach(options, (option, optionName) => {
|
|
2636
|
+
const absorb = (option as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
2637
|
+
if (!absorb?.length) return;
|
|
2638
|
+
absorb.forEach((absorbedName) => {
|
|
2639
|
+
// Only absorb a sibling that exists and is not already spoken for; a
|
|
2640
|
+
// schema naming a missing field must not make the field disappear.
|
|
2641
|
+
if (options?.[absorbedName] && !absorbedBy[absorbedName]) {
|
|
2642
|
+
absorbedBy[absorbedName] = optionName;
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2561
2647
|
// Group the options by their raw `group` key, remembering the order groups
|
|
2562
2648
|
// first appear in the schema. Each row carries a `hidden` flag.
|
|
2563
2649
|
const groupOrder: string[] = [];
|
|
@@ -2646,16 +2732,17 @@ const FormEngineImpl = ({
|
|
|
2646
2732
|
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
2647
2733
|
// unmet one-of group's members all land in attention and still rail up.
|
|
2648
2734
|
type TRowEntry = { name: string; hidden: boolean };
|
|
2649
|
-
|
|
2735
|
+
type TBucketKey = 'attention' | 'set' | 'optional';
|
|
2736
|
+
const buckets: Record<TBucketKey, Record<string, TRowEntry[]>> = {
|
|
2650
2737
|
attention: {},
|
|
2651
2738
|
set: {},
|
|
2652
2739
|
optional: {},
|
|
2653
2740
|
};
|
|
2654
|
-
const bucketGroups: Record<
|
|
2741
|
+
const bucketGroups: Record<TBucketKey, string[]> = { attention: [], set: [], optional: [] };
|
|
2655
2742
|
// Freeze the box of any field currently being edited (or whose one-of group
|
|
2656
2743
|
// has an edited member) to its last settled box — so finishing an edit that
|
|
2657
2744
|
// flips its status doesn't remount it in another box and steal focus.
|
|
2658
|
-
const stableBucketOf = (entry: TRowEntry):
|
|
2745
|
+
const stableBucketOf = (entry: TRowEntry): TBucketKey => {
|
|
2659
2746
|
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
2660
2747
|
const groupBeingEdited =
|
|
2661
2748
|
!entry.hidden &&
|
|
@@ -2679,7 +2766,7 @@ const FormEngineImpl = ({
|
|
|
2679
2766
|
buckets[b][groupName].push(entry);
|
|
2680
2767
|
});
|
|
2681
2768
|
});
|
|
2682
|
-
const bucketCount = (b:
|
|
2769
|
+
const bucketCount = (b: TBucketKey) =>
|
|
2683
2770
|
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2684
2771
|
|
|
2685
2772
|
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
@@ -2695,7 +2782,7 @@ const FormEngineImpl = ({
|
|
|
2695
2782
|
const showGroupSubLabel = (groupName: string) =>
|
|
2696
2783
|
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
2697
2784
|
const STATUS_BOXES: Array<{
|
|
2698
|
-
key:
|
|
2785
|
+
key: TBucketKey;
|
|
2699
2786
|
label: string;
|
|
2700
2787
|
intent?: 'warning' | 'success';
|
|
2701
2788
|
icon: IReqoreIconName;
|
|
@@ -2709,6 +2796,11 @@ const FormEngineImpl = ({
|
|
|
2709
2796
|
// together at the first member's slot and rendered as a connected rail (flat
|
|
2710
2797
|
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
2711
2798
|
// are drawn per member). Narrow stacks fall back to flat rows.
|
|
2799
|
+
// The absorbed siblings a host row must render, in schema order. Empty for
|
|
2800
|
+
// every row that absorbs nothing, which is all of them by default.
|
|
2801
|
+
const absorbedNamesFor = (hostName: string): string[] =>
|
|
2802
|
+
Object.keys(absorbedBy).filter((absorbedName) => absorbedBy[absorbedName] === hostName);
|
|
2803
|
+
|
|
2712
2804
|
const renderGroupRows = (names: Array<{ name: string; hidden: boolean }>) => {
|
|
2713
2805
|
const renderRow = (
|
|
2714
2806
|
entry: { name: string; hidden?: boolean },
|
|
@@ -2731,6 +2823,9 @@ const FormEngineImpl = ({
|
|
|
2731
2823
|
clustered={clustered}
|
|
2732
2824
|
clusterFirst={clusterFirst}
|
|
2733
2825
|
clusterLast={clusterLast}
|
|
2826
|
+
// The siblings this row renders inside itself instead of leaving
|
|
2827
|
+
// them a row of their own — see `absorbedBy`.
|
|
2828
|
+
absorbedFields={absorbedNamesFor(entry.name)}
|
|
2734
2829
|
/>
|
|
2735
2830
|
);
|
|
2736
2831
|
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
@@ -2739,6 +2834,10 @@ const FormEngineImpl = ({
|
|
|
2739
2834
|
const emitted = new Set<string>();
|
|
2740
2835
|
const groupOf = (name: string) =>
|
|
2741
2836
|
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
2837
|
+
// An absorbed field keeps its place in the buckets — so the Set /
|
|
2838
|
+
// Optional counts and the completion meter still see it — but its host
|
|
2839
|
+
// renders its control, so it gets no row of its own here.
|
|
2840
|
+
names = names.filter((entry) => !absorbedBy[entry.name]);
|
|
2742
2841
|
return names.map((entry) => {
|
|
2743
2842
|
const grp = groupOf(entry.name);
|
|
2744
2843
|
if (!grp) return renderRow(entry, false);
|
|
@@ -2843,33 +2942,36 @@ const FormEngineImpl = ({
|
|
|
2843
2942
|
$accent={accent}
|
|
2844
2943
|
$bg={boxBg}
|
|
2845
2944
|
key={box.key}
|
|
2945
|
+
ref={(node: HTMLDivElement | null) => {
|
|
2946
|
+
statusBoxRefs.current[box.key] = node;
|
|
2947
|
+
}}
|
|
2948
|
+
// Opening arms the reveal; collapsing disarms it, so a
|
|
2949
|
+
// close never scrolls.
|
|
2950
|
+
onCollapseChange={(collapsed) =>
|
|
2951
|
+
setOpenedStatusBox(collapsed ? undefined : box.key)
|
|
2952
|
+
}
|
|
2846
2953
|
flat
|
|
2847
2954
|
minimal
|
|
2848
2955
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
2849
2956
|
collapsible
|
|
2850
|
-
//
|
|
2851
|
-
//
|
|
2852
|
-
//
|
|
2853
|
-
//
|
|
2854
|
-
//
|
|
2855
|
-
//
|
|
2856
|
-
//
|
|
2857
|
-
//
|
|
2858
|
-
//
|
|
2859
|
-
//
|
|
2860
|
-
//
|
|
2861
|
-
//
|
|
2862
|
-
//
|
|
2863
|
-
//
|
|
2864
|
-
//
|
|
2865
|
-
//
|
|
2866
|
-
//
|
|
2867
|
-
|
|
2868
|
-
isCollapsed={
|
|
2869
|
-
compactCollapsedGroups.includes(box.key) &&
|
|
2870
|
-
!query &&
|
|
2871
|
-
!(box.key === 'optional' && onlyOptionalRows)
|
|
2872
|
-
}
|
|
2957
|
+
// The Optional box now holds every not-yet-added field, so
|
|
2958
|
+
// it starts COLLAPSED to keep the form focused on what's in
|
|
2959
|
+
// use. But a SEARCH must surface matching addable fields —
|
|
2960
|
+
// and ReqorePanel unmounts collapsed content — so force it
|
|
2961
|
+
// open whenever a query is active. (isCollapsed is the
|
|
2962
|
+
// panel's controllable state; manual toggling still works
|
|
2963
|
+
// when no query is set.)
|
|
2964
|
+
// The Optional box starts collapsed so a form opens on what
|
|
2965
|
+
// is in use — but only when there IS something else to
|
|
2966
|
+
// open on. When every field is optional it is the whole
|
|
2967
|
+
// form, and collapsing it renders a card that looks empty
|
|
2968
|
+
// and broken: an auth profile's Authorization block, whose
|
|
2969
|
+
// nine requirement fields are all optional, showed as a
|
|
2970
|
+
// titled card containing one collapsed "Optional 9" strip
|
|
2971
|
+
// and nothing else. A search still forces it open
|
|
2972
|
+
// (ReqorePanel unmounts collapsed content, so a match
|
|
2973
|
+
// inside it would be unreachable otherwise).
|
|
2974
|
+
isCollapsed={box.key === 'optional' && !query && !onlyOptionalRows}
|
|
2873
2975
|
label={
|
|
2874
2976
|
<StyledGroupHeader>
|
|
2875
2977
|
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|