@qoretechnologies/reqraft 0.10.46-pr.95.ge595a13 → 0.10.46
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 +14 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +199 -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 +65 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +276 -108
- 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.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +15 -3
- 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 +81 -27
- 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/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +15 -2
- package/dist/helpers/expressions.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 +279 -33
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +995 -12
- package/src/components/form/engine/FormEngine.tsx +377 -94
- 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 +55 -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 +389 -1
- package/src/components/form/fields/template/TemplateField.tsx +100 -35
- 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/expressions.ts +16 -2
- 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>;
|
|
@@ -762,6 +835,24 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
762
835
|
* (non-compact) mode.
|
|
763
836
|
*/
|
|
764
837
|
initialExpandedOptions?: string[];
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Cap how many rows a status box shows before the rest fold behind a
|
|
841
|
+
* "N more fields" control. Applies to every box — Needs attention, Set and
|
|
842
|
+
* Optional alike — so a long form stays scannable instead of making the
|
|
843
|
+
* reader page past one box to reach the next.
|
|
844
|
+
*
|
|
845
|
+
* Optional keeps its own rule on top of this: a box that would have
|
|
846
|
+
* collapsed but holds preselected rows shows those rows rather than an
|
|
847
|
+
* arbitrary first-N. The cap then applies to that set, so
|
|
848
|
+
* `maxFieldsShown: 5` over 10 preselected fields shows 5 — and opening the
|
|
849
|
+
* control reveals EVERY row in the box, not just the other 5 preselected
|
|
850
|
+
* ones, because once the reader has asked to see more, hiding the rest is
|
|
851
|
+
* the same defect one level down.
|
|
852
|
+
*
|
|
853
|
+
* Unset (the default) leaves every box uncapped.
|
|
854
|
+
*/
|
|
855
|
+
maxFieldsShown?: number;
|
|
765
856
|
}
|
|
766
857
|
|
|
767
858
|
// Option types rendered full-width (IDE Options parity, commit 8e6b7781).
|
|
@@ -792,6 +883,8 @@ const FormEngineImpl = ({
|
|
|
792
883
|
compactFlush = false,
|
|
793
884
|
compactNested = false,
|
|
794
885
|
compactPanelProps,
|
|
886
|
+
compactCollapsedGroups = ['optional'],
|
|
887
|
+
compactToolbar = true,
|
|
795
888
|
commitMode = 'immediate',
|
|
796
889
|
expandMode = 'single',
|
|
797
890
|
onCommit,
|
|
@@ -812,6 +905,7 @@ const FormEngineImpl = ({
|
|
|
812
905
|
autoFocusFirstRequired,
|
|
813
906
|
expandFirstRequired,
|
|
814
907
|
initialExpandedOptions,
|
|
908
|
+
maxFieldsShown,
|
|
815
909
|
...rest
|
|
816
910
|
}: IFormEngineProps) => {
|
|
817
911
|
// Built-ins + whatever the consumer declared for its own injected editors.
|
|
@@ -859,6 +953,9 @@ const FormEngineImpl = ({
|
|
|
859
953
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
860
954
|
// Which options are expanded into their editor (several can be open at once).
|
|
861
955
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
956
|
+
/** Status boxes whose "N more fields" control the reader has opened. Keyed by
|
|
957
|
+
box so opening Optional does not also unfold Set. */
|
|
958
|
+
const [expandedMoreBoxes, setExpandedMoreBoxes] = useState<TFormEngineBoxKey[]>([]);
|
|
862
959
|
|
|
863
960
|
// --- Reveal a status box's content when it is opened ----------------------
|
|
864
961
|
//
|
|
@@ -1129,7 +1226,10 @@ const FormEngineImpl = ({
|
|
|
1129
1226
|
setOptions({});
|
|
1130
1227
|
return;
|
|
1131
1228
|
}
|
|
1132
|
-
setLocalValue({
|
|
1229
|
+
setLocalValue({
|
|
1230
|
+
fields: fixOptions(value, data.data, undefined, isRendererOnly),
|
|
1231
|
+
meta: undefined,
|
|
1232
|
+
});
|
|
1133
1233
|
if (!operatorsUrl) {
|
|
1134
1234
|
setLoading(false);
|
|
1135
1235
|
}
|
|
@@ -1174,7 +1274,10 @@ const FormEngineImpl = ({
|
|
|
1174
1274
|
}
|
|
1175
1275
|
setOptions(data.data);
|
|
1176
1276
|
onOptionsLoaded?.(data.data);
|
|
1177
|
-
setLocalValue({
|
|
1277
|
+
setLocalValue({
|
|
1278
|
+
fields: fixOptions({}, data.data, undefined, isRendererOnly),
|
|
1279
|
+
meta: undefined,
|
|
1280
|
+
});
|
|
1178
1281
|
})();
|
|
1179
1282
|
}
|
|
1180
1283
|
}, [url, customUrl]);
|
|
@@ -1231,12 +1334,14 @@ const FormEngineImpl = ({
|
|
|
1231
1334
|
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
1232
1335
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
1233
1336
|
const resolvedSchemaType =
|
|
1234
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1337
|
+
isAnyLike && (fields[optionName] as IQorusFormField)?.type ?
|
|
1338
|
+
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
1339
|
+
: (
|
|
1340
|
+
(fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
|
|
1341
|
+
isRendererOnly((fields[optionName] as IQorusFormField)?.type)
|
|
1342
|
+
) ?
|
|
1343
|
+
schemaType
|
|
1344
|
+
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
1240
1345
|
const type =
|
|
1241
1346
|
_type ||
|
|
1242
1347
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
@@ -1310,11 +1415,7 @@ const FormEngineImpl = ({
|
|
|
1310
1415
|
|
|
1311
1416
|
onSingleOptionsChange?.(optionName, updatedValue[optionName]);
|
|
1312
1417
|
|
|
1313
|
-
if (
|
|
1314
|
-
compact &&
|
|
1315
|
-
!readOnly &&
|
|
1316
|
-
shouldAutoCollapseCompactOption(options?.[optionName], val)
|
|
1317
|
-
) {
|
|
1418
|
+
if (compact && !readOnly && shouldAutoCollapseCompactOption(options?.[optionName], val)) {
|
|
1318
1419
|
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1319
1420
|
}
|
|
1320
1421
|
|
|
@@ -1757,7 +1858,11 @@ const FormEngineImpl = ({
|
|
|
1757
1858
|
isRendererOnly
|
|
1758
1859
|
);
|
|
1759
1860
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1760
|
-
|
|
1861
|
+
// A schema-declared hash whose every entry is a materialised-but-unset
|
|
1862
|
+
// field holds nothing, so it is not "set" — without this the row showed a
|
|
1863
|
+
// green set-dot and counted toward the completion meter while the
|
|
1864
|
+
// sub-form it summarises said 0/3 set.
|
|
1865
|
+
const empty = isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema);
|
|
1761
1866
|
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
1762
1867
|
const required = !!(schema?.required || reqGroups.length);
|
|
1763
1868
|
const covered =
|
|
@@ -2060,7 +2165,10 @@ const FormEngineImpl = ({
|
|
|
2060
2165
|
next[optionName] = value as IQorusFormField;
|
|
2061
2166
|
}
|
|
2062
2167
|
});
|
|
2063
|
-
return {
|
|
2168
|
+
return {
|
|
2169
|
+
fields: fixOptions(next, options || {}, operators, isRendererOnly),
|
|
2170
|
+
meta: undefined,
|
|
2171
|
+
};
|
|
2064
2172
|
});
|
|
2065
2173
|
setRequiredOnly(false);
|
|
2066
2174
|
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
@@ -2108,11 +2216,10 @@ const FormEngineImpl = ({
|
|
|
2108
2216
|
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
2109
2217
|
const uiTypeIsAnyLike = schemaUiType === 'any' || schemaUiType === 'auto';
|
|
2110
2218
|
const resolvedType =
|
|
2111
|
-
(isFixedCompactAllowedValueOption(optionSchema)
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
: undefined) ||
|
|
2219
|
+
(isFixedCompactAllowedValueOption(optionSchema) ?
|
|
2220
|
+
getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
2221
|
+
: schemaUiType && !uiTypeIsAnyLike ? schemaUiType
|
|
2222
|
+
: undefined) ||
|
|
2116
2223
|
type ||
|
|
2117
2224
|
schemaUiType ||
|
|
2118
2225
|
(optionSchema?.type as TQorusType) ||
|
|
@@ -2307,6 +2414,11 @@ const FormEngineImpl = ({
|
|
|
2307
2414
|
allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
|
|
2308
2415
|
disabled={
|
|
2309
2416
|
options?.[optionName]?.disabled ||
|
|
2417
|
+
// a system-owned field states that as `readonly`; only the older
|
|
2418
|
+
// compat projection also sent `disabled`, so honouring only
|
|
2419
|
+
// `disabled` rendered an editable control for a value the user
|
|
2420
|
+
// cannot change (see CompactRow's `fieldReadonly`)
|
|
2421
|
+
(options?.[optionName] as { readonly?: boolean } | undefined)?.readonly ||
|
|
2310
2422
|
readOnly ||
|
|
2311
2423
|
!hasAllDependenciesFullfilled(
|
|
2312
2424
|
options?.[optionName]?.depends_on,
|
|
@@ -2400,6 +2512,9 @@ const FormEngineImpl = ({
|
|
|
2400
2512
|
const cBg = (theme as { main?: string } | undefined)?.main || '#181818';
|
|
2401
2513
|
const cRowBg = changeDarkness(getMainBackgroundColor(theme), 0.01);
|
|
2402
2514
|
const cGroupLine = `${cText}1f`;
|
|
2515
|
+
// The "N more fields" row's dashed edge. Well above the divider tint: this
|
|
2516
|
+
// one is an affordance the reader has to notice, not a hairline between rows.
|
|
2517
|
+
const cMoreBorder = `${cText}4d`;
|
|
2403
2518
|
|
|
2404
2519
|
// The closure surface the extracted CompactRow reads through context. Refs and
|
|
2405
2520
|
// setters are stable; the state/memo/handler fields change identity as they do
|
|
@@ -2503,6 +2618,14 @@ const FormEngineImpl = ({
|
|
|
2503
2618
|
]
|
|
2504
2619
|
);
|
|
2505
2620
|
|
|
2621
|
+
// `compactToolbar` resolved to a full object, so neither the context type nor
|
|
2622
|
+
// the toolbar itself has to re-default anything. `null` means no toolbar.
|
|
2623
|
+
const compactToolbarParts = useMemo(() => {
|
|
2624
|
+
if (compactToolbar === false) return null;
|
|
2625
|
+
const all = { completion: true, search: true, fields: true, help: true };
|
|
2626
|
+
return compactToolbar === true ? all : { ...all, ...compactToolbar };
|
|
2627
|
+
}, [JSON.stringify(compactToolbar)]);
|
|
2628
|
+
|
|
2506
2629
|
const compactToolbarContextValue = useMemo<ICompactToolbarContext>(
|
|
2507
2630
|
() => ({
|
|
2508
2631
|
readOnly,
|
|
@@ -2511,6 +2634,12 @@ const FormEngineImpl = ({
|
|
|
2511
2634
|
completion: readFirstCompletion,
|
|
2512
2635
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
2513
2636
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
2637
|
+
parts: compactToolbarParts ?? {
|
|
2638
|
+
completion: false,
|
|
2639
|
+
search: false,
|
|
2640
|
+
fields: false,
|
|
2641
|
+
help: false,
|
|
2642
|
+
},
|
|
2514
2643
|
hasMultipleOptions: size(availableOptions) > 1,
|
|
2515
2644
|
compactQuery,
|
|
2516
2645
|
setCompactQuery,
|
|
@@ -2561,10 +2690,14 @@ const FormEngineImpl = ({
|
|
|
2561
2690
|
|
|
2562
2691
|
// The engine's own toolbar action, plus whatever the consumer added through
|
|
2563
2692
|
// `compactPanelProps.actions` — appended, so an outside action can never
|
|
2564
|
-
// knock the form's toolbar out of the header.
|
|
2693
|
+
// knock the form's toolbar out of the header. Only `compactToolbar={false}`
|
|
2694
|
+
// removes it, and then the header carries just the consumer's own actions.
|
|
2565
2695
|
const compactHeaderActions = useMemo(
|
|
2566
|
-
() => [
|
|
2567
|
-
|
|
2696
|
+
() => [
|
|
2697
|
+
...(compactToolbarParts ? [{ as: CompactToolbar, responsive: false }] : []),
|
|
2698
|
+
...(compactPanelProps?.actions || []),
|
|
2699
|
+
],
|
|
2700
|
+
[compactPanelProps?.actions, compactToolbarParts]
|
|
2568
2701
|
);
|
|
2569
2702
|
const renderCompact = () => {
|
|
2570
2703
|
const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
|
|
@@ -2699,17 +2832,20 @@ const FormEngineImpl = ({
|
|
|
2699
2832
|
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
2700
2833
|
// unmet one-of group's members all land in attention and still rail up.
|
|
2701
2834
|
type TRowEntry = { name: string; hidden: boolean };
|
|
2702
|
-
|
|
2703
|
-
const buckets: Record<TBucketKey, Record<string, TRowEntry[]>> = {
|
|
2835
|
+
const buckets: Record<TFormEngineBoxKey, Record<string, TRowEntry[]>> = {
|
|
2704
2836
|
attention: {},
|
|
2705
2837
|
set: {},
|
|
2706
2838
|
optional: {},
|
|
2707
2839
|
};
|
|
2708
|
-
const bucketGroups: Record<
|
|
2840
|
+
const bucketGroups: Record<TFormEngineBoxKey, string[]> = {
|
|
2841
|
+
attention: [],
|
|
2842
|
+
set: [],
|
|
2843
|
+
optional: [],
|
|
2844
|
+
};
|
|
2709
2845
|
// Freeze the box of any field currently being edited (or whose one-of group
|
|
2710
2846
|
// has an edited member) to its last settled box — so finishing an edit that
|
|
2711
2847
|
// flips its status doesn't remount it in another box and steal focus.
|
|
2712
|
-
const stableBucketOf = (entry: TRowEntry):
|
|
2848
|
+
const stableBucketOf = (entry: TRowEntry): TFormEngineBoxKey => {
|
|
2713
2849
|
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
2714
2850
|
const groupBeingEdited =
|
|
2715
2851
|
!entry.hidden &&
|
|
@@ -2733,7 +2869,7 @@ const FormEngineImpl = ({
|
|
|
2733
2869
|
buckets[b][groupName].push(entry);
|
|
2734
2870
|
});
|
|
2735
2871
|
});
|
|
2736
|
-
const bucketCount = (b:
|
|
2872
|
+
const bucketCount = (b: TFormEngineBoxKey) =>
|
|
2737
2873
|
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2738
2874
|
|
|
2739
2875
|
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
@@ -2748,8 +2884,32 @@ const FormEngineImpl = ({
|
|
|
2748
2884
|
// other and DOES get its sub-label.
|
|
2749
2885
|
const showGroupSubLabel = (groupName: string) =>
|
|
2750
2886
|
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
2887
|
+
|
|
2888
|
+
// `preselected` means "show this option by default when a new form is
|
|
2889
|
+
// loaded, so it's visible to the user". An empty non-required preselected
|
|
2890
|
+
// field buckets to 'optional' (nothing is wrong with it, and it has no
|
|
2891
|
+
// value), and the Optional box is collapsed by default — so the very
|
|
2892
|
+
// fields the schema asked to put in front of the author were the ones
|
|
2893
|
+
// nobody could see. A preselected row therefore forces its box open.
|
|
2894
|
+
//
|
|
2895
|
+
// A hidden row is an addable the author has not put in the form yet, so it
|
|
2896
|
+
// is never what `preselected` is talking about, however its schema is
|
|
2897
|
+
// flagged.
|
|
2898
|
+
const isPreselectedRow = (entry: TRowEntry) =>
|
|
2899
|
+
!entry.hidden &&
|
|
2900
|
+
!!(options?.[entry.name] as { preselected?: boolean } | undefined)?.preselected;
|
|
2901
|
+
// ONLY the Optional box. A preselected field that HAS a value buckets to
|
|
2902
|
+
// 'set' and one that fails validation to 'attention'; in both boxes the
|
|
2903
|
+
// field is already accounted for, and whether those boxes start collapsed
|
|
2904
|
+
// is the consumer's deliberate `compactCollapsedGroups` choice. Overriding
|
|
2905
|
+
// them here forced the Set box open against an explicit request.
|
|
2906
|
+
const groupHasPreselectedRow = (boxKey: TFormEngineBoxKey, groupName: string) =>
|
|
2907
|
+
boxKey === 'optional' && (buckets[boxKey][groupName] || []).some(isPreselectedRow);
|
|
2908
|
+
const boxHasPreselectedRows = (boxKey: TFormEngineBoxKey) =>
|
|
2909
|
+
boxKey === 'optional' &&
|
|
2910
|
+
bucketGroups[boxKey].some((groupName) => groupHasPreselectedRow(boxKey, groupName));
|
|
2751
2911
|
const STATUS_BOXES: Array<{
|
|
2752
|
-
key:
|
|
2912
|
+
key: TFormEngineBoxKey;
|
|
2753
2913
|
label: string;
|
|
2754
2914
|
intent?: 'warning' | 'success';
|
|
2755
2915
|
icon: IReqoreIconName;
|
|
@@ -2836,6 +2996,52 @@ const FormEngineImpl = ({
|
|
|
2836
2996
|
});
|
|
2837
2997
|
};
|
|
2838
2998
|
|
|
2999
|
+
// One schema group inside a status box: its thin sub-label, the group's
|
|
3000
|
+
// subtitle, then its rows. Factored out of the box body so the SAME markup
|
|
3001
|
+
// renders both the groups a box shows directly and the ones it defers
|
|
3002
|
+
// behind the inner collapse.
|
|
3003
|
+
/** @param only when given, render just these rows of the group — the group
|
|
3004
|
+
can be split across the shown and folded halves of a capped box, and its
|
|
3005
|
+
label must appear above whichever half holds its first row. */
|
|
3006
|
+
const renderBoxGroup = (
|
|
3007
|
+
boxKey: TFormEngineBoxKey,
|
|
3008
|
+
groupName: string,
|
|
3009
|
+
only?: Set<string>
|
|
3010
|
+
) => {
|
|
3011
|
+
const groupConfig = groups?.[groupName];
|
|
3012
|
+
const rows =
|
|
3013
|
+
only ?
|
|
3014
|
+
(buckets[boxKey][groupName] || []).filter((entry) => only.has(entry.name))
|
|
3015
|
+
: buckets[boxKey][groupName];
|
|
3016
|
+
if (only && !rows.length) {
|
|
3017
|
+
return null;
|
|
3018
|
+
}
|
|
3019
|
+
return (
|
|
3020
|
+
<React.Fragment key={groupName}>
|
|
3021
|
+
{showGroupSubLabel(groupName) ?
|
|
3022
|
+
<StyledStatusBoxGroupLabel>
|
|
3023
|
+
{getOptionGroupLabel(groupName, groups)}
|
|
3024
|
+
</StyledStatusBoxGroupLabel>
|
|
3025
|
+
: null}
|
|
3026
|
+
{showGroupSubLabel(groupName) && groupConfig?.subtitle ?
|
|
3027
|
+
<ReqoreP
|
|
3028
|
+
size='small'
|
|
3029
|
+
effect={{ opacity: 0.6 }}
|
|
3030
|
+
style={{
|
|
3031
|
+
marginTop: 2,
|
|
3032
|
+
marginBottom: 8,
|
|
3033
|
+
marginLeft: GROUP_INDENT,
|
|
3034
|
+
paddingRight: 10,
|
|
3035
|
+
}}
|
|
3036
|
+
>
|
|
3037
|
+
{groupConfig.subtitle}
|
|
3038
|
+
</ReqoreP>
|
|
3039
|
+
: null}
|
|
3040
|
+
{renderGroupRows(rows)}
|
|
3041
|
+
</React.Fragment>
|
|
3042
|
+
);
|
|
3043
|
+
};
|
|
3044
|
+
|
|
2839
3045
|
return (
|
|
2840
3046
|
<OptionsContext.Provider value={{ schema: options, value: availableOptions }}>
|
|
2841
3047
|
<CompactRowContext.Provider value={compactRowContextValue}>
|
|
@@ -2855,6 +3061,10 @@ const FormEngineImpl = ({
|
|
|
2855
3061
|
$flush={compactFlush || compactNested}
|
|
2856
3062
|
>
|
|
2857
3063
|
<StyledCompactPanel
|
|
3064
|
+
// Mirrors the toolbar's own search-row gate: when no search
|
|
3065
|
+
// row renders, the header is a thin strip and sits tight to
|
|
3066
|
+
// the first status box (see StyledCompactPanel).
|
|
3067
|
+
$tightHeader={!(compactToolbarParts?.search && size(availableOptions) > 1)}
|
|
2858
3068
|
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
2859
3069
|
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
2860
3070
|
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
@@ -2893,6 +3103,54 @@ const FormEngineImpl = ({
|
|
|
2893
3103
|
const groupsInBox = bucketGroups[box.key];
|
|
2894
3104
|
const count = bucketCount(box.key);
|
|
2895
3105
|
if (!count) return null;
|
|
3106
|
+
const hasPreselectedRows = boxHasPreselectedRows(box.key);
|
|
3107
|
+
const wouldCollapse =
|
|
3108
|
+
compactCollapsedGroups.includes(box.key) &&
|
|
3109
|
+
!query &&
|
|
3110
|
+
!(box.key === 'optional' && onlyOptionalRows);
|
|
3111
|
+
// Only a box that was opened SOLELY to reveal a preselected
|
|
3112
|
+
// row splits its body; a box that renders open anyway shows
|
|
3113
|
+
// all of its groups as before.
|
|
3114
|
+
const splitForPreselected = wouldCollapse && hasPreselectedRows;
|
|
3115
|
+
|
|
3116
|
+
// Which rows this box shows, and which fold away, decided at
|
|
3117
|
+
// ROW level because `maxFieldsShown` counts fields — a
|
|
3118
|
+
// group-level split can only ever cut at a group boundary.
|
|
3119
|
+
// Group order is preserved so the visible rows read in the
|
|
3120
|
+
// same order they would if nothing were folded.
|
|
3121
|
+
const orderedRows: TRowEntry[] = groupsInBox.flatMap(
|
|
3122
|
+
(groupName) => buckets[box.key][groupName] || []
|
|
3123
|
+
);
|
|
3124
|
+
// Optional's own rule first: a box opened solely to reveal
|
|
3125
|
+
// preselected rows offers those, not an arbitrary first-N.
|
|
3126
|
+
const candidateRows =
|
|
3127
|
+
splitForPreselected ? orderedRows.filter(isPreselectedRow) : orderedRows;
|
|
3128
|
+
// …then the cap applies to whatever that left.
|
|
3129
|
+
const capped =
|
|
3130
|
+
maxFieldsShown !== undefined && candidateRows.length > maxFieldsShown;
|
|
3131
|
+
const shownRows = capped ? candidateRows.slice(0, maxFieldsShown) : candidateRows;
|
|
3132
|
+
const shownRowSet = new Set(shownRows.map((entry) => entry.name));
|
|
3133
|
+
// Everything the box holds that is not on show. Once the
|
|
3134
|
+
// reader asks for more they get the whole box — withholding
|
|
3135
|
+
// the non-preselected remainder behind a second control is
|
|
3136
|
+
// the same defect one level down.
|
|
3137
|
+
const deferredRowSet = new Set(
|
|
3138
|
+
orderedRows
|
|
3139
|
+
.filter((entry) => !shownRowSet.has(entry.name))
|
|
3140
|
+
.map((entry) => entry.name)
|
|
3141
|
+
);
|
|
3142
|
+
const deferredCount = deferredRowSet.size;
|
|
3143
|
+
const shownGroups = groupsInBox.filter((groupName) =>
|
|
3144
|
+
(buckets[box.key][groupName] || []).some((entry) =>
|
|
3145
|
+
shownRowSet.has(entry.name)
|
|
3146
|
+
)
|
|
3147
|
+
);
|
|
3148
|
+
const deferredGroups = groupsInBox.filter((groupName) =>
|
|
3149
|
+
(buckets[box.key][groupName] || []).some(
|
|
3150
|
+
(entry) => !shownRowSet.has(entry.name)
|
|
3151
|
+
)
|
|
3152
|
+
);
|
|
3153
|
+
const moreOpen = Boolean(query) || expandedMoreBoxes.includes(box.key);
|
|
2896
3154
|
const accent =
|
|
2897
3155
|
box.key === 'attention' ? cWarning
|
|
2898
3156
|
: box.key === 'set' ? cSuccess
|
|
@@ -2921,24 +3179,33 @@ const FormEngineImpl = ({
|
|
|
2921
3179
|
minimal
|
|
2922
3180
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
2923
3181
|
collapsible
|
|
2924
|
-
//
|
|
2925
|
-
//
|
|
2926
|
-
//
|
|
2927
|
-
//
|
|
2928
|
-
//
|
|
2929
|
-
//
|
|
2930
|
-
//
|
|
2931
|
-
//
|
|
2932
|
-
//
|
|
2933
|
-
//
|
|
2934
|
-
//
|
|
2935
|
-
//
|
|
2936
|
-
//
|
|
2937
|
-
//
|
|
2938
|
-
//
|
|
2939
|
-
//
|
|
2940
|
-
//
|
|
2941
|
-
|
|
3182
|
+
// Which boxes start closed is the consumer's call
|
|
3183
|
+
// (`compactCollapsedGroups`, default `['optional']`: the
|
|
3184
|
+
// Optional box holds every not-yet-added field, so a form
|
|
3185
|
+
// opens on what is in use). Two rules override it:
|
|
3186
|
+
//
|
|
3187
|
+
// A SEARCH forces every box open — ReqorePanel unmounts
|
|
3188
|
+
// collapsed content, so a match inside a closed box would
|
|
3189
|
+
// be unreachable. (isCollapsed is the panel's controllable
|
|
3190
|
+
// state; manual toggling still works with no query set.)
|
|
3191
|
+
//
|
|
3192
|
+
// The Optional box stays open when it is the ONLY box,
|
|
3193
|
+
// because collapsing it renders a card that looks empty and
|
|
3194
|
+
// broken: an auth profile's Authorization block, whose nine
|
|
3195
|
+
// requirement fields are all optional, showed as a titled
|
|
3196
|
+
// card containing one collapsed "Optional 9" strip and
|
|
3197
|
+
// nothing else. This does not generalise to the other two —
|
|
3198
|
+
// a lone Set box collapsed is a deliberate ask, and its
|
|
3199
|
+
// header still reports what is in there.
|
|
3200
|
+
//
|
|
3201
|
+
// A PRESELECTED row overrides it too: the schema asked
|
|
3202
|
+
// for that field to be visible on a new form, so the
|
|
3203
|
+
// box it lands in opens. The groups it does NOT appear
|
|
3204
|
+
// in stay behind an inner collapse below, so opening
|
|
3205
|
+
// the box reveals the preselected fields without also
|
|
3206
|
+
// dumping every not-yet-added optional field on the
|
|
3207
|
+
// author.
|
|
3208
|
+
isCollapsed={wouldCollapse && !hasPreselectedRows}
|
|
2942
3209
|
label={
|
|
2943
3210
|
<StyledGroupHeader>
|
|
2944
3211
|
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
@@ -2969,35 +3236,51 @@ const FormEngineImpl = ({
|
|
|
2969
3236
|
$success={cSuccess}
|
|
2970
3237
|
$rowBg={cRowBg}
|
|
2971
3238
|
$lineColor={cGroupLine}
|
|
3239
|
+
$moreBorder={cMoreBorder}
|
|
2972
3240
|
className={compactNarrow ? 'readfirst-narrow' : undefined}
|
|
2973
3241
|
>
|
|
2974
|
-
{
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3242
|
+
{shownGroups.map((groupName) =>
|
|
3243
|
+
renderBoxGroup(box.key, groupName, shownRowSet)
|
|
3244
|
+
)}
|
|
3245
|
+
{/* The control lives INSIDE the group body, as the last
|
|
3246
|
+
row, so it inherits the rows' grid, padding and
|
|
3247
|
+
hover and lines up with them. Outside it was a bare
|
|
3248
|
+
line of text in the box's corner, which is what the
|
|
3249
|
+
build #154 review rejected. */}
|
|
3250
|
+
{deferredCount ?
|
|
3251
|
+
<button
|
|
3252
|
+
type='button'
|
|
3253
|
+
className='readfirst-row readfirst-more-row options-readfirst-more'
|
|
3254
|
+
aria-expanded={moreOpen}
|
|
3255
|
+
onClick={() =>
|
|
3256
|
+
setExpandedMoreBoxes((prev) =>
|
|
3257
|
+
prev.includes(box.key) ?
|
|
3258
|
+
prev.filter((k) => k !== box.key)
|
|
3259
|
+
: [...prev, box.key]
|
|
3260
|
+
)
|
|
3261
|
+
}
|
|
3262
|
+
>
|
|
3263
|
+
<ReqoreP size='small' effect={{ opacity: 0.8 }}>
|
|
3264
|
+
{moreOpen ?
|
|
3265
|
+
'Show fewer'
|
|
3266
|
+
: `${deferredCount} more ${box.key === 'optional' ? 'optional ' : ''}field${deferredCount === 1 ? '' : 's'}`
|
|
3267
|
+
}
|
|
3268
|
+
</ReqoreP>
|
|
3269
|
+
<ReqoreIcon
|
|
3270
|
+
icon='ArrowDownSLine'
|
|
3271
|
+
size='small'
|
|
3272
|
+
className='readfirst-more-chevron'
|
|
3273
|
+
effect={{ opacity: 0.8 }}
|
|
3274
|
+
/>
|
|
3275
|
+
</button>
|
|
3276
|
+
: null}
|
|
3277
|
+
{/* A search forces the fold open for the same reason
|
|
3278
|
+
the boxes open: an unmounted row cannot be found. */}
|
|
3279
|
+
{deferredCount && moreOpen ?
|
|
3280
|
+
deferredGroups.map((groupName) =>
|
|
3281
|
+
renderBoxGroup(box.key, groupName, deferredRowSet)
|
|
3282
|
+
)
|
|
3283
|
+
: null}
|
|
3001
3284
|
</StyledGroupBody>
|
|
3002
3285
|
</StyledStatusBox>
|
|
3003
3286
|
);
|