@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g124fc92
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/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +130 -25
- 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 +78 -15
- 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 +260 -99
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +22 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +5 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +72 -39
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +3 -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/formAppearance.d.ts +48 -0
- package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
- package/dist/components/form/engine/formAppearance.js +50 -0
- package/dist/components/form/engine/formAppearance.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +14 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +6 -2
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +9 -1
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
- package/dist/components/form/engine/rowFocus.js +17 -5
- package/dist/components/form/engine/rowFocus.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +21 -0
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +13 -11
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +20 -7
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/allowed-values/savedValues.d.ts +30 -0
- package/dist/components/form/fields/allowed-values/savedValues.d.ts.map +1 -0
- package/dist/components/form/fields/allowed-values/savedValues.js +18 -0
- package/dist/components/form/fields/allowed-values/savedValues.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +18 -8
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +7 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +9 -5
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js +6 -5
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
- package/dist/components/form/fields/number/Number.d.ts +4 -1
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +13 -5
- package/dist/components/form/fields/number/Number.js.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
- package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +71 -42
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +2 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -1
- package/dist/components/imageLightbox/ImageLightbox.js +2 -0
- package/dist/components/imageLightbox/ImageLightbox.js.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +3 -2
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts +2 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.js +19 -13
- package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketHeader.js +4 -1
- package/dist/components/supportTicket/TicketHeader.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js +29 -10
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/dist/components/supportTicket/useLoadedImages.d.ts +10 -0
- package/dist/components/supportTicket/useLoadedImages.d.ts.map +1 -1
- package/dist/components/supportTicket/useLoadedImages.js +63 -1
- package/dist/components/supportTicket/useLoadedImages.js.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.js +6 -2
- package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +10 -16
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/allowedValues.d.ts +12 -0
- package/dist/helpers/allowedValues.d.ts.map +1 -1
- package/dist/helpers/allowedValues.js +20 -1
- package/dist/helpers/allowedValues.js.map +1 -1
- package/dist/helpers/fieldDescription.d.ts +16 -0
- package/dist/helpers/fieldDescription.d.ts.map +1 -0
- package/dist/helpers/fieldDescription.js +40 -0
- package/dist/helpers/fieldDescription.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -1
- package/dist/helpers/singleLineString.js +1 -0
- package/dist/helpers/singleLineString.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +143 -23
- package/src/components/form/engine/CompactToolbar.tsx +85 -16
- package/src/components/form/engine/FormEngine.stories.tsx +25 -13
- package/src/components/form/engine/FormEngine.tsx +367 -81
- package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
- package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
- package/src/components/form/engine/compactRowContext.ts +5 -0
- package/src/components/form/engine/compactRowStyles.ts +218 -38
- package/src/components/form/engine/compactToolbarContext.ts +3 -0
- package/src/components/form/engine/formAppearance.ts +78 -0
- package/src/components/form/engine/readFirst.ts +16 -3
- package/src/components/form/engine/rowFocus.ts +17 -6
- package/src/components/form/expressions/renderExpressionToText.ts +15 -0
- package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
- package/src/components/form/fields/Field.tsx +11 -9
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +21 -9
- package/src/components/form/fields/allowed-values/savedValues.ts +41 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +17 -4
- package/src/components/form/fields/long-string/LongString.stories.tsx +27 -0
- package/src/components/form/fields/long-string/LongString.tsx +14 -3
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +5 -4
- package/src/components/form/fields/number/Number.tsx +19 -4
- package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
- package/src/components/form/fields/template/TemplateField.tsx +32 -4
- package/src/components/form/index.tsx +2 -0
- package/src/components/imageLightbox/ImageLightbox.tsx +2 -0
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +3 -2
- package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -4
- package/src/components/supportTicket/ReferencePicker.tsx +22 -12
- package/src/components/supportTicket/TicketConversation.stories.tsx +0 -1
- package/src/components/supportTicket/TicketHeader.tsx +2 -1
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +24 -21
- package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
- package/src/components/supportTicket/useLoadedImages.ts +20 -0
- package/src/components/ticketReferences/TicketReferences.stories.tsx +26 -0
- package/src/components/ticketReferences/TicketReferences.tsx +12 -4
- package/src/components/ticketThread/TicketThread.stories.tsx +71 -2
- package/src/components/ticketThread/TicketThread.tsx +15 -28
- package/src/helpers/allowedValues.ts +19 -0
- package/src/helpers/fieldDescription.ts +39 -0
- package/src/helpers/singleLineString.ts +1 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ReqoreIcon,
|
|
7
7
|
ReqoreMessage,
|
|
8
8
|
ReqoreP,
|
|
9
|
+
ReqoreSpan,
|
|
9
10
|
ReqoreTag,
|
|
10
11
|
ReqoreTagGroup,
|
|
11
12
|
ReqoreVerticalSpacer,
|
|
@@ -43,6 +44,7 @@ import { offersTypeChoices } from './typeChoices';
|
|
|
43
44
|
import { optionRowActions, resolveOptionActions, TOptionActions } from './optionActions';
|
|
44
45
|
import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
|
|
45
46
|
import { revealRow } from './revealRow';
|
|
47
|
+
import { findRowFocusTarget } from './rowFocus';
|
|
46
48
|
import { useEmittedValues } from '../fields/emittedValues';
|
|
47
49
|
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last, uniq } from 'lodash';
|
|
48
50
|
import map from 'lodash/map';
|
|
@@ -51,6 +53,8 @@ import size from 'lodash/size';
|
|
|
51
53
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
52
54
|
import { useMeasure, useMount, useUpdateEffect } from 'react-use';
|
|
53
55
|
import styled, { css } from 'styled-components';
|
|
56
|
+
import { allowedValueOf } from '../../../helpers/allowedValues';
|
|
57
|
+
import { fieldHasLongDescription } from '../../../helpers/fieldDescription';
|
|
54
58
|
import {
|
|
55
59
|
fixOperatorValue,
|
|
56
60
|
getDefaultValue,
|
|
@@ -84,6 +88,7 @@ import { CompactRow } from './CompactRow';
|
|
|
84
88
|
import { FormFieldsSkeleton } from './FormFieldsSkeleton';
|
|
85
89
|
import { CompactRowContext, ICompactRowContext, TCodePreviewRenderer } from './compactRowContext';
|
|
86
90
|
import {
|
|
91
|
+
CALM_SECTION_INSET,
|
|
87
92
|
GROUP_INDENT,
|
|
88
93
|
LABEL_AFFORDANCE_WIDTH,
|
|
89
94
|
LABEL_COL_MAX,
|
|
@@ -96,6 +101,7 @@ import {
|
|
|
96
101
|
StyledRequiredClusterHeader,
|
|
97
102
|
StyledStatusBox,
|
|
98
103
|
StyledStatusBoxGroupLabel,
|
|
104
|
+
StyledStatusDot,
|
|
99
105
|
} from './compactRowStyles';
|
|
100
106
|
import { CompactToolbar } from './CompactToolbar';
|
|
101
107
|
import {
|
|
@@ -114,6 +120,14 @@ import {
|
|
|
114
120
|
TMarkdownRenderer,
|
|
115
121
|
} from '../../Description/markdownRendererContext';
|
|
116
122
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
123
|
+
import {
|
|
124
|
+
FormAppearanceProvider,
|
|
125
|
+
getCalmPanelHairline,
|
|
126
|
+
getCalmPanelSurface,
|
|
127
|
+
TFormEngineAppearance,
|
|
128
|
+
useFormAppearance,
|
|
129
|
+
useIsCalmForm,
|
|
130
|
+
} from './formAppearance';
|
|
117
131
|
import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
118
132
|
import {
|
|
119
133
|
TReadFirstStatus,
|
|
@@ -197,6 +211,14 @@ export interface IFormValidityData {
|
|
|
197
211
|
invalidFields: IFormFieldValidityData[];
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
/** The two numbers the readiness line shows ("2 need attention · 1 to fill in"). */
|
|
215
|
+
export interface IFormAttention {
|
|
216
|
+
/** Fields in the first box: still to fill in, and faults. */
|
|
217
|
+
count: number;
|
|
218
|
+
/** How many of those are faults (amber) rather than only still to fill in. */
|
|
219
|
+
faults: number;
|
|
220
|
+
}
|
|
221
|
+
|
|
200
222
|
/**
|
|
201
223
|
* Resolve a field's `inherit_props` map against the current available
|
|
202
224
|
* options (plus any values inherited from an outer FormEngine scope),
|
|
@@ -848,6 +870,21 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
848
870
|
* value; clicking expands the real editor inline, Done collapses it.
|
|
849
871
|
*/
|
|
850
872
|
compact?: boolean;
|
|
873
|
+
/**
|
|
874
|
+
* How the form LOOKS — never what it does. `'default'` keeps the established
|
|
875
|
+
* look; `'calm'` draws the status boxes as plain panels on the theme's panel
|
|
876
|
+
* surface with label/value rows split by hairlines, grey labels, an amber
|
|
877
|
+
* "Required · add a value" in place of red asterisks and messages, quiet
|
|
878
|
+
* counts, no dot on a healthy row, Yes/No as words, and a readiness meter
|
|
879
|
+
* whose done part is grey. See {@link TFormEngineAppearance}.
|
|
880
|
+
*
|
|
881
|
+
* Opt-in, so a consumer that does not pass it is unchanged. Inherited: a
|
|
882
|
+
* nested (`arg_schema`) sub-form and every field editor the form mounts
|
|
883
|
+
* follow the form's appearance, and a form with no `appearance` of its own
|
|
884
|
+
* follows a `FormAppearanceProvider` above it. Applies to compact mode; the
|
|
885
|
+
* classic collection layout only passes it on to its field editors.
|
|
886
|
+
*/
|
|
887
|
+
appearance?: TFormEngineAppearance;
|
|
851
888
|
/**
|
|
852
889
|
* Compact mode only: drop the read-first wrap's horizontal gutter (the 12px
|
|
853
890
|
* breathing room) so the form sits flush to its container's edges. For embeds
|
|
@@ -928,8 +965,32 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
928
965
|
* `Optional` names a category rather than the things in it, and
|
|
929
966
|
* `Optional settings` is what the reader is looking at. Only the labels
|
|
930
967
|
* change; bucketing, icons and intents are untouched.
|
|
968
|
+
*
|
|
969
|
+
* `pending` names the first box while nothing in it is a fault yet — a new form's
|
|
970
|
+
* empty required fields, which are still to fill in (default `To fill in`).
|
|
971
|
+
*/
|
|
972
|
+
compactBoxLabels?: Partial<Record<TFormEngineBoxKey | 'pending', string>>;
|
|
973
|
+
/**
|
|
974
|
+
* The host attempted a submit. Until then an empty required field the person has not
|
|
975
|
+
* been to yet is only "to fill in": a new form opens neutral rather than with every
|
|
976
|
+
* required field in amber, and a field turns amber once someone opens it and leaves
|
|
977
|
+
* it empty. Pass `true` after a failed submit to show every empty required field as
|
|
978
|
+
* needing attention. Invalid values are faults from the start either way.
|
|
979
|
+
*/
|
|
980
|
+
revealRequired?: boolean;
|
|
981
|
+
/**
|
|
982
|
+
* Bump it to move the caret to the first field that needs attention: the answer to a
|
|
983
|
+
* submit that could not go is where to type. Each new value opens that field and puts
|
|
984
|
+
* the caret in it once, even though the host's submit button holds the caret (an open
|
|
985
|
+
* row otherwise never takes a caret held outside it). Pair it with `revealRequired`.
|
|
986
|
+
*/
|
|
987
|
+
focusAttentionRequest?: number;
|
|
988
|
+
/**
|
|
989
|
+
* The readiness line's two numbers, reported whenever they change. A host that draws
|
|
990
|
+
* its own submit control draws it from these, so the control and the form can never
|
|
991
|
+
* disagree about what is left. `{ count: 0, faults: 0 }` is a form with nothing left.
|
|
931
992
|
*/
|
|
932
|
-
|
|
993
|
+
onAttentionChange?: (attention: IFormAttention) => void;
|
|
933
994
|
/**
|
|
934
995
|
* Compact mode only: the three status-box headers stay pinned to the top of
|
|
935
996
|
* whatever scrolls while their own box is in view. Default `true`.
|
|
@@ -1235,6 +1296,9 @@ const FormEngineImpl = ({
|
|
|
1235
1296
|
forceDropdown,
|
|
1236
1297
|
showTypeToggle = true,
|
|
1237
1298
|
compact,
|
|
1299
|
+
// Read through the context `FormEngine` provides (resolved there against an
|
|
1300
|
+
// inherited appearance); destructured only to keep it out of `rest`.
|
|
1301
|
+
appearance: _appearance, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
1238
1302
|
// Destructured only to keep it out of `rest` (and so off the DOM). The gutter
|
|
1239
1303
|
// this prop documents was never implemented: it was passed to the wrap as
|
|
1240
1304
|
// `$flush`, whose styled template had no interpolation slot, so it has always
|
|
@@ -1247,6 +1311,9 @@ const FormEngineImpl = ({
|
|
|
1247
1311
|
compactPanelProps,
|
|
1248
1312
|
compactCollapsedGroups: compactCollapsedGroupsProp,
|
|
1249
1313
|
compactBoxLabels,
|
|
1314
|
+
revealRequired = false,
|
|
1315
|
+
focusAttentionRequest,
|
|
1316
|
+
onAttentionChange,
|
|
1250
1317
|
compactStickyBoxHeaders = true,
|
|
1251
1318
|
compactStickyOffset = 0,
|
|
1252
1319
|
compactToolbar = true,
|
|
@@ -1318,6 +1385,7 @@ const FormEngineImpl = ({
|
|
|
1318
1385
|
const [loading, setLoading] = useState<boolean>(!!(url || customUrl || operatorsUrl));
|
|
1319
1386
|
const confirmAction = useReqoreProperty('confirmAction');
|
|
1320
1387
|
const theme = useReqoreTheme();
|
|
1388
|
+
const calm = useIsCalmForm();
|
|
1321
1389
|
const [focusedEditing, setFocusedEditing] = useState<string>();
|
|
1322
1390
|
const [showFieldTypes, setShowFieldTypes] = useState<boolean>(false);
|
|
1323
1391
|
// Global toggle (toolbar ⓘ): reveal the short-description info panel on every
|
|
@@ -1330,6 +1398,21 @@ const FormEngineImpl = ({
|
|
|
1330
1398
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
1331
1399
|
// Which options are expanded into their editor (several can be open at once).
|
|
1332
1400
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
1401
|
+
// The fields the person has opened and left. An empty required field becomes a
|
|
1402
|
+
// fault only then (or after a submit attempt, `revealRequired`): a form nobody has
|
|
1403
|
+
// touched is not already wrong.
|
|
1404
|
+
const [visitedOptions, setVisitedOptions] = useState<string[]>([]);
|
|
1405
|
+
const lastExpandedOptions = useRef<string[]>([]);
|
|
1406
|
+
useEffect(() => {
|
|
1407
|
+
const left = lastExpandedOptions.current.filter((name) => !expandedOptions.includes(name));
|
|
1408
|
+
lastExpandedOptions.current = expandedOptions;
|
|
1409
|
+
if (left.length) {
|
|
1410
|
+
setVisitedOptions((prev) => {
|
|
1411
|
+
const added = left.filter((name) => !prev.includes(name));
|
|
1412
|
+
return added.length ? [...prev, ...added] : prev;
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}, [expandedOptions]);
|
|
1333
1416
|
/** Status boxes whose "N more fields" control the reader has opened. Keyed by
|
|
1334
1417
|
box so opening Optional does not also unfold Set. */
|
|
1335
1418
|
const [expandedMoreBoxes, setExpandedMoreBoxes] = useState<TFormEngineBoxKey[]>([]);
|
|
@@ -2270,7 +2353,7 @@ const FormEngineImpl = ({
|
|
|
2270
2353
|
forEach(options || {}, (optionSchema, name) => {
|
|
2271
2354
|
forEach(optionSchema?.allowed_values || [], (allowedValue) => {
|
|
2272
2355
|
if (!getAllowedValueAvailability(allowedValue, availableOptions, options || {}).available) {
|
|
2273
|
-
locked.push(`${name}\u0000${JSON.stringify(allowedValue
|
|
2356
|
+
locked.push(`${name}\u0000${JSON.stringify(allowedValueOf(allowedValue))}`);
|
|
2274
2357
|
}
|
|
2275
2358
|
});
|
|
2276
2359
|
});
|
|
@@ -2458,6 +2541,11 @@ const FormEngineImpl = ({
|
|
|
2458
2541
|
(newValue, option, optionName) => {
|
|
2459
2542
|
if (
|
|
2460
2543
|
showInvalidOptionsOnly &&
|
|
2544
|
+
// An open row stays while the reader is in it, as its box does
|
|
2545
|
+
// (`stableBucketOf`): the first character typed into an empty required
|
|
2546
|
+
// field makes it valid, and hiding it then would pull the field out
|
|
2547
|
+
// from under the caret mid-word.
|
|
2548
|
+
!expandedOptions.includes(optionName) &&
|
|
2461
2549
|
isOptionValid(
|
|
2462
2550
|
optionName,
|
|
2463
2551
|
getOptionFieldStorageType(
|
|
@@ -2480,6 +2568,7 @@ const FormEngineImpl = ({
|
|
|
2480
2568
|
);
|
|
2481
2569
|
}, [
|
|
2482
2570
|
showInvalidOptionsOnly,
|
|
2571
|
+
expandedOptions,
|
|
2483
2572
|
isRendererOnly,
|
|
2484
2573
|
JSON.stringify(availableOptions),
|
|
2485
2574
|
JSON.stringify(options),
|
|
@@ -2548,9 +2637,12 @@ const FormEngineImpl = ({
|
|
|
2548
2637
|
covered,
|
|
2549
2638
|
invalid,
|
|
2550
2639
|
warned: msgIntent === 'warning',
|
|
2640
|
+
visited: revealRequired || visitedOptions.includes(name),
|
|
2551
2641
|
});
|
|
2552
2642
|
},
|
|
2553
2643
|
[
|
|
2644
|
+
revealRequired,
|
|
2645
|
+
visitedOptions,
|
|
2554
2646
|
isRendererOnly,
|
|
2555
2647
|
JSON.stringify(options),
|
|
2556
2648
|
JSON.stringify(availableOptions),
|
|
@@ -2583,6 +2675,24 @@ const FormEngineImpl = ({
|
|
|
2583
2675
|
.length,
|
|
2584
2676
|
[JSON.stringify(availableOptions), getOptionBucket]
|
|
2585
2677
|
);
|
|
2678
|
+
// How many of those are faults rather than only still to fill in (`pending`). Zero
|
|
2679
|
+
// on a new form, so its first box reads "To fill in" without amber.
|
|
2680
|
+
const readFirstAttentionFaultCount = useMemo(
|
|
2681
|
+
() =>
|
|
2682
|
+
Object.keys(availableOptions || {}).filter(
|
|
2683
|
+
(name) => getOptionBucket(name) === 'attention' && getOptionStatus(name) !== 'pending'
|
|
2684
|
+
).length,
|
|
2685
|
+
[JSON.stringify(availableOptions), getOptionBucket, getOptionStatus]
|
|
2686
|
+
);
|
|
2687
|
+
// The same two numbers, to a host that draws its own submit control from them.
|
|
2688
|
+
const onAttentionChangeRef = useRef(onAttentionChange);
|
|
2689
|
+
onAttentionChangeRef.current = onAttentionChange;
|
|
2690
|
+
useEffect(() => {
|
|
2691
|
+
onAttentionChangeRef.current?.({
|
|
2692
|
+
count: readFirstAttentionCount,
|
|
2693
|
+
faults: readFirstAttentionFaultCount,
|
|
2694
|
+
});
|
|
2695
|
+
}, [readFirstAttentionCount, readFirstAttentionFaultCount]);
|
|
2586
2696
|
|
|
2587
2697
|
const getIntent = useCallback(
|
|
2588
2698
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -2708,6 +2818,39 @@ const FormEngineImpl = ({
|
|
|
2708
2818
|
// field update or an in-place schema reload will NOT re-grab focus; only a
|
|
2709
2819
|
// genuine remount (a fresh instance) auto-focuses again, which is the intended
|
|
2710
2820
|
// on-mount behaviour.
|
|
2821
|
+
// What the "first field to fix" scans read: the opener on arrival below and the
|
|
2822
|
+
// caret move on a submit attempt (`focusAttentionRequest`) pick the same field.
|
|
2823
|
+
const getAttentionFieldMeta = useCallback(
|
|
2824
|
+
(fieldName: string) => {
|
|
2825
|
+
const schema = options?.[fieldName];
|
|
2826
|
+
if (!schema) {
|
|
2827
|
+
return undefined;
|
|
2828
|
+
}
|
|
2829
|
+
return {
|
|
2830
|
+
focusable:
|
|
2831
|
+
!readOnly &&
|
|
2832
|
+
!schema.disabled &&
|
|
2833
|
+
!(schema as { readonly?: boolean }).readonly &&
|
|
2834
|
+
!dependencyLockedNames.includes(fieldName),
|
|
2835
|
+
// Single source of truth: the same bucket the read-first status boxes
|
|
2836
|
+
// show, so a filled-but-invalid required row is a target too.
|
|
2837
|
+
needsAttention: getOptionBucket(fieldName) === 'attention',
|
|
2838
|
+
};
|
|
2839
|
+
},
|
|
2840
|
+
[options, readOnly, dependencyLockedNames, getOptionBucket]
|
|
2841
|
+
);
|
|
2842
|
+
// Opening a row by name: `expandMode: 'single'` replaces only what a click could
|
|
2843
|
+
// (see the note in the opener below), never a row the host named.
|
|
2844
|
+
const openOption = useCallback(
|
|
2845
|
+
(target: string) =>
|
|
2846
|
+
setExpandedOptions((prev) =>
|
|
2847
|
+
prev.includes(target) ? prev
|
|
2848
|
+
: expandMode === 'multi' ? [...prev, target]
|
|
2849
|
+
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2850
|
+
),
|
|
2851
|
+
[expandMode, initialExpandedOptions]
|
|
2852
|
+
);
|
|
2853
|
+
|
|
2711
2854
|
const hasAutoFocusedRef = useRef(false);
|
|
2712
2855
|
useEffect(() => {
|
|
2713
2856
|
// Two decisions, one scan. Both open the row that needs attention;
|
|
@@ -2746,22 +2889,7 @@ const FormEngineImpl = ({
|
|
|
2746
2889
|
return;
|
|
2747
2890
|
}
|
|
2748
2891
|
|
|
2749
|
-
const target = getFirstAttentionOptionName(orderedNames,
|
|
2750
|
-
const schema = options[fieldName];
|
|
2751
|
-
if (!schema) {
|
|
2752
|
-
return undefined;
|
|
2753
|
-
}
|
|
2754
|
-
return {
|
|
2755
|
-
focusable:
|
|
2756
|
-
!readOnly &&
|
|
2757
|
-
!schema.disabled &&
|
|
2758
|
-
!(schema as { readonly?: boolean }).readonly &&
|
|
2759
|
-
!dependencyLockedNames.includes(fieldName),
|
|
2760
|
-
// Single source of truth: the same bucket the read-first status boxes
|
|
2761
|
-
// show, so a filled-but-invalid required row is a target too.
|
|
2762
|
-
needsAttention: getOptionBucket(fieldName) === 'attention',
|
|
2763
|
-
};
|
|
2764
|
-
});
|
|
2892
|
+
const target = getFirstAttentionOptionName(orderedNames, getAttentionFieldMeta);
|
|
2765
2893
|
|
|
2766
2894
|
// Mark done even when nothing needs attention, so later value edits or an
|
|
2767
2895
|
// in-place schema reload never re-scan or re-focus. Only a remount arms it
|
|
@@ -2769,17 +2897,13 @@ const FormEngineImpl = ({
|
|
|
2769
2897
|
hasAutoFocusedRef.current = true;
|
|
2770
2898
|
|
|
2771
2899
|
if (target) {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
// the reader did anything — and this scan is a convenience layered on
|
|
2780
|
-
// top of it. Keep the named rows; replace only what a click could.
|
|
2781
|
-
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2782
|
-
);
|
|
2900
|
+
// `expandMode: 'single'` is a rule about what a CLICK does: opening a row
|
|
2901
|
+
// closes the one the reader last opened. It is not a licence to close a row
|
|
2902
|
+
// the HOST named through `initialExpandedOptions`. That is an address — a
|
|
2903
|
+
// statement about what the page is FOR, made before the reader did anything
|
|
2904
|
+
// — and this scan is a convenience layered on top of it. `openOption` keeps
|
|
2905
|
+
// the named rows and replaces only what a click could.
|
|
2906
|
+
openOption(target);
|
|
2783
2907
|
}
|
|
2784
2908
|
}, [
|
|
2785
2909
|
autoFocusFirstRequired,
|
|
@@ -2787,13 +2911,52 @@ const FormEngineImpl = ({
|
|
|
2787
2911
|
compact,
|
|
2788
2912
|
options,
|
|
2789
2913
|
availableOptions,
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
getOptionBucket,
|
|
2793
|
-
expandMode,
|
|
2794
|
-
initialExpandedOptions,
|
|
2914
|
+
getAttentionFieldMeta,
|
|
2915
|
+
openOption,
|
|
2795
2916
|
]);
|
|
2796
2917
|
|
|
2918
|
+
// A submit that could not go is answered with where to type: each new
|
|
2919
|
+
// `focusAttentionRequest` opens the first field that needs attention and puts the
|
|
2920
|
+
// caret in it. The host's submit button holds the caret at that moment, and an open
|
|
2921
|
+
// row never takes a caret held outside it (CompactRow), so the button lets go of it
|
|
2922
|
+
// here first; the person asked to submit, and this is the answer to that.
|
|
2923
|
+
const lastFocusRequest = useRef(focusAttentionRequest);
|
|
2924
|
+
useEffect(() => {
|
|
2925
|
+
if (focusAttentionRequest === lastFocusRequest.current) {
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
lastFocusRequest.current = focusAttentionRequest;
|
|
2929
|
+
if (!focusAttentionRequest || !compact || !options) {
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
const target = getFirstAttentionOptionName(
|
|
2933
|
+
Object.keys(availableOptions),
|
|
2934
|
+
getAttentionFieldMeta
|
|
2935
|
+
);
|
|
2936
|
+
if (!target) {
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
const wrap = compactWrapNodeRef.current;
|
|
2940
|
+
const active = document.activeElement as HTMLElement | null;
|
|
2941
|
+
if (active && active !== document.body && !wrap?.contains(active)) {
|
|
2942
|
+
active.blur();
|
|
2943
|
+
}
|
|
2944
|
+
if (!expandedOptions.includes(target)) {
|
|
2945
|
+
// The row's own open effect reveals it and takes the caret.
|
|
2946
|
+
openOption(target);
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
// Already open (the field the form opened on arrival): its open effect has run,
|
|
2950
|
+
// so the caret goes to its control here.
|
|
2951
|
+
const row = wrap?.querySelector<HTMLElement>(`[data-field="${target}"]`);
|
|
2952
|
+
if (row) {
|
|
2953
|
+
revealRow(row, 'auto');
|
|
2954
|
+
findRowFocusTarget(row.querySelector<HTMLElement>('.options-readfirst-row-editor'), {
|
|
2955
|
+
textEntryOnly: row.classList.contains('options-readfirst-card'),
|
|
2956
|
+
})?.focus({ preventScroll: true });
|
|
2957
|
+
}
|
|
2958
|
+
}, [focusAttentionRequest]);
|
|
2959
|
+
|
|
2797
2960
|
// Read-first completion summary (how many shown options have a value set),
|
|
2798
2961
|
// surfaced as a progress meter at the top of the compact form.
|
|
2799
2962
|
const readFirstCompletion = useMemo(
|
|
@@ -3191,6 +3354,9 @@ const FormEngineImpl = ({
|
|
|
3191
3354
|
option={{ type: resolvedType, ...other }}
|
|
3192
3355
|
getType={getTypeForOption}
|
|
3193
3356
|
isRendererOnly={isRendererOnly}
|
|
3357
|
+
// Open is not left: a field the form opened for typing (the first empty
|
|
3358
|
+
// one, on a new form) says what it needs without already being wrong.
|
|
3359
|
+
requirementPending={!(revealRequired || visitedOptions.includes(optionName))}
|
|
3194
3360
|
/>
|
|
3195
3361
|
{operators && size(operators) && size(other.op) ?
|
|
3196
3362
|
<>
|
|
@@ -3242,6 +3408,8 @@ const FormEngineImpl = ({
|
|
|
3242
3408
|
getTypeForOption,
|
|
3243
3409
|
inheritedScopes,
|
|
3244
3410
|
inheritedFromParent,
|
|
3411
|
+
revealRequired,
|
|
3412
|
+
visitedOptions,
|
|
3245
3413
|
]
|
|
3246
3414
|
);
|
|
3247
3415
|
|
|
@@ -3266,6 +3434,10 @@ const FormEngineImpl = ({
|
|
|
3266
3434
|
// The "N more fields" row's dashed edge. Well above the divider tint: this
|
|
3267
3435
|
// one is an affordance the reader has to notice, not a hairline between rows.
|
|
3268
3436
|
const cMoreBorder = `${cText}4d`;
|
|
3437
|
+
// Calm: the section surface and its hairline, from the theme's panel (the
|
|
3438
|
+
// same surface and edge Reqore gives the section panels themselves).
|
|
3439
|
+
const cPanel = getCalmPanelSurface(theme);
|
|
3440
|
+
const cHairline = getCalmPanelHairline(theme);
|
|
3269
3441
|
|
|
3270
3442
|
// The closure surface the extracted CompactRow reads through context. Refs and
|
|
3271
3443
|
// setters are stable; the state/memo/handler fields change identity as they do
|
|
@@ -3284,6 +3456,8 @@ const FormEngineImpl = ({
|
|
|
3284
3456
|
codePreviewRenderer,
|
|
3285
3457
|
showAllDescriptions,
|
|
3286
3458
|
expandedOptions,
|
|
3459
|
+
visitedOptions,
|
|
3460
|
+
revealRequired,
|
|
3287
3461
|
highlightedOptions,
|
|
3288
3462
|
flashedOptions,
|
|
3289
3463
|
infoPanelOverrides,
|
|
@@ -3332,6 +3506,8 @@ const FormEngineImpl = ({
|
|
|
3332
3506
|
codePreviewRenderer,
|
|
3333
3507
|
showAllDescriptions,
|
|
3334
3508
|
expandedOptions,
|
|
3509
|
+
visitedOptions,
|
|
3510
|
+
revealRequired,
|
|
3335
3511
|
highlightedOptions,
|
|
3336
3512
|
flashedOptions,
|
|
3337
3513
|
infoPanelOverrides,
|
|
@@ -3383,6 +3559,7 @@ const FormEngineImpl = ({
|
|
|
3383
3559
|
readOnly,
|
|
3384
3560
|
invalidCount: size(validityData.invalidFields),
|
|
3385
3561
|
attentionCount: readFirstAttentionCount,
|
|
3562
|
+
attentionFaultCount: readFirstAttentionFaultCount,
|
|
3386
3563
|
completion: readFirstCompletion,
|
|
3387
3564
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
3388
3565
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
@@ -3430,6 +3607,7 @@ const FormEngineImpl = ({
|
|
|
3430
3607
|
onReadOnlyActivate,
|
|
3431
3608
|
validityData,
|
|
3432
3609
|
readFirstAttentionCount,
|
|
3610
|
+
readFirstAttentionFaultCount,
|
|
3433
3611
|
readFirstCompletion,
|
|
3434
3612
|
showInvalidOptionsOnly,
|
|
3435
3613
|
handleToggleInvalidOnly,
|
|
@@ -3711,12 +3889,20 @@ const FormEngineImpl = ({
|
|
|
3711
3889
|
intent?: 'warning' | 'success';
|
|
3712
3890
|
icon: IReqoreIconName;
|
|
3713
3891
|
}> = [
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3892
|
+
// The first box is amber only once something in it is a fault. On a new form it
|
|
3893
|
+
// holds the required fields still to fill in, which is not yet anything wrong.
|
|
3894
|
+
readFirstAttentionFaultCount ?
|
|
3895
|
+
{
|
|
3896
|
+
key: 'attention',
|
|
3897
|
+
label: compactBoxLabels?.attention ?? 'Needs attention',
|
|
3898
|
+
intent: 'warning',
|
|
3899
|
+
icon: 'ErrorWarningLine',
|
|
3900
|
+
}
|
|
3901
|
+
: {
|
|
3902
|
+
key: 'attention',
|
|
3903
|
+
label: compactBoxLabels?.pending ?? 'To fill in',
|
|
3904
|
+
icon: 'CheckboxBlankCircleLine',
|
|
3905
|
+
},
|
|
3720
3906
|
{ key: 'set', label: compactBoxLabels?.set ?? 'Set', intent: 'success', icon: 'CheckLine' },
|
|
3721
3907
|
{
|
|
3722
3908
|
key: 'optional',
|
|
@@ -3785,15 +3971,28 @@ const FormEngineImpl = ({
|
|
|
3785
3971
|
// box (the Focus cluster). A met group needs no banner — the rail + the
|
|
3786
3972
|
// "Covers"/"Covered by" chips already say which member satisfies it.
|
|
3787
3973
|
if (requiredGroupsInfo.satisfiedBy[grp]) return railed;
|
|
3974
|
+
// Amber once someone has been to the group (or a submit was attempted); until
|
|
3975
|
+
// then it is still to fill in, in the quiet colour, like its members.
|
|
3976
|
+
const clusterColor =
|
|
3977
|
+
memberEntries.some((e) => getOptionStatus(e.name) !== 'pending') ? cWarning : cMuted;
|
|
3788
3978
|
return (
|
|
3789
3979
|
<StyledRequiredClusterBox
|
|
3790
3980
|
key={grp}
|
|
3791
3981
|
className='options-readfirst-required-cluster'
|
|
3792
|
-
$border={`${
|
|
3793
|
-
$tint={`${
|
|
3982
|
+
$border={`${clusterColor}33`}
|
|
3983
|
+
$tint={`${clusterColor}0d`}
|
|
3984
|
+
$calm={calm}
|
|
3794
3985
|
>
|
|
3795
|
-
<StyledRequiredClusterHeader
|
|
3796
|
-
|
|
3986
|
+
<StyledRequiredClusterHeader
|
|
3987
|
+
className='options-readfirst-required-cluster-header'
|
|
3988
|
+
$color={clusterColor}
|
|
3989
|
+
$calm={calm}
|
|
3990
|
+
>
|
|
3991
|
+
<ReqoreIcon
|
|
3992
|
+
icon='LinkM'
|
|
3993
|
+
size={calm ? '13px' : '11px'}
|
|
3994
|
+
style={{ color: clusterColor }}
|
|
3995
|
+
/>
|
|
3797
3996
|
One of the below is required
|
|
3798
3997
|
</StyledRequiredClusterHeader>
|
|
3799
3998
|
{railed}
|
|
@@ -3821,20 +4020,31 @@ const FormEngineImpl = ({
|
|
|
3821
4020
|
return (
|
|
3822
4021
|
<React.Fragment key={groupName}>
|
|
3823
4022
|
{showGroupSubLabel(groupName) ?
|
|
3824
|
-
<StyledStatusBoxGroupLabel
|
|
4023
|
+
<StyledStatusBoxGroupLabel
|
|
4024
|
+
className='options-readfirst-group-label'
|
|
4025
|
+
$calm={calm}
|
|
4026
|
+
$color={cKey}
|
|
4027
|
+
>
|
|
3825
4028
|
{getOptionGroupLabel(groupName, groups)}
|
|
3826
4029
|
</StyledStatusBoxGroupLabel>
|
|
3827
4030
|
: null}
|
|
3828
4031
|
{showGroupSubLabel(groupName) && groupConfig?.subtitle ?
|
|
3829
4032
|
<ReqoreP
|
|
4033
|
+
className='options-readfirst-group-subtitle'
|
|
3830
4034
|
size='small'
|
|
3831
4035
|
effect={{ opacity: 0.6 }}
|
|
3832
|
-
style={
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
4036
|
+
style={
|
|
4037
|
+
calm ?
|
|
4038
|
+
// Under the calm heading, at the rows' inset; the space below
|
|
4039
|
+
// it is the next row's own padding.
|
|
4040
|
+
{ margin: 0, padding: `2px ${CALM_SECTION_INSET}px 4px` }
|
|
4041
|
+
: {
|
|
4042
|
+
marginTop: 2,
|
|
4043
|
+
marginBottom: 8,
|
|
4044
|
+
marginLeft: GROUP_INDENT,
|
|
4045
|
+
paddingRight: 10,
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
3838
4048
|
>
|
|
3839
4049
|
{groupConfig.subtitle}
|
|
3840
4050
|
</ReqoreP>
|
|
@@ -3888,8 +4098,15 @@ const FormEngineImpl = ({
|
|
|
3888
4098
|
)
|
|
3889
4099
|
}
|
|
3890
4100
|
flat
|
|
3891
|
-
|
|
4101
|
+
// Calm: no surface and no lift at all. A theme that gives
|
|
4102
|
+
// panels a surface of their own (Reqore's `theme.panel`) would
|
|
4103
|
+
// otherwise paint this wrapper as a card around the section
|
|
4104
|
+
// cards inside it. Transparent and un-raised, Reqore also drops
|
|
4105
|
+
// the wrapper's horizontal padding, so the readiness line, the
|
|
4106
|
+
// search and the sections share one left edge with the host.
|
|
4107
|
+
raised={!calm}
|
|
3892
4108
|
minimal
|
|
4109
|
+
transparent={calm || undefined}
|
|
3893
4110
|
// No panel background: the form sits transparently on whatever
|
|
3894
4111
|
// hosts it (page, drawer, or — for an arg_schema field — the
|
|
3895
4112
|
// parent's edit card) instead of stacking its own dark surface.
|
|
@@ -3967,8 +4184,13 @@ const FormEngineImpl = ({
|
|
|
3967
4184
|
the reader's record withheld, and a page scrolls. */}
|
|
3968
4185
|
{readOnly ?
|
|
3969
4186
|
bucketCount('set') ?
|
|
3970
|
-
<div
|
|
4187
|
+
<div
|
|
4188
|
+
className='options-readfirst-read'
|
|
4189
|
+
style={{ padding: calm ? 0 : '4px 4px 6px' }}
|
|
4190
|
+
>
|
|
3971
4191
|
<StyledGroupBody
|
|
4192
|
+
$calm={calm}
|
|
4193
|
+
$hairline={cHairline}
|
|
3972
4194
|
$divider={cDivider}
|
|
3973
4195
|
$hover={cHover}
|
|
3974
4196
|
$focus={cWarning}
|
|
@@ -3976,7 +4198,7 @@ const FormEngineImpl = ({
|
|
|
3976
4198
|
$rowBg={cRowBg}
|
|
3977
4199
|
$lineColor={cGroupLine}
|
|
3978
4200
|
$moreBorder={cMoreBorder}
|
|
3979
|
-
className={compactNarrow ? 'readfirst-narrow' :
|
|
4201
|
+
className={`readfirst-read-list${compactNarrow ? ' readfirst-narrow' : ''}`}
|
|
3980
4202
|
>
|
|
3981
4203
|
{bucketGroups.set.map((groupName) => renderBoxGroup('set', groupName))}
|
|
3982
4204
|
</StyledGroupBody>
|
|
@@ -4004,6 +4226,17 @@ const FormEngineImpl = ({
|
|
|
4004
4226
|
const orderedRows: TRowEntry[] = groupsInBox.flatMap(
|
|
4005
4227
|
(groupName) => buckets[box.key][groupName] || []
|
|
4006
4228
|
);
|
|
4229
|
+
// The number in the box's header counts the rows whose value
|
|
4230
|
+
// belongs in it NOW. A field being edited keeps its box until it
|
|
4231
|
+
// is left (so it does not jump away under the caret), and it used
|
|
4232
|
+
// to keep its count too: a box read "To fill in 1" while its last
|
|
4233
|
+
// field already had a value and the host's submit read ready.
|
|
4234
|
+
const headerCount =
|
|
4235
|
+
readOnly ? count : (
|
|
4236
|
+
orderedRows.filter(
|
|
4237
|
+
(entry) => getOptionBucket(entry.name, entry.hidden) === box.key
|
|
4238
|
+
).length
|
|
4239
|
+
);
|
|
4007
4240
|
// Optional's own rule first: a box opened solely to reveal
|
|
4008
4241
|
// preselected rows offers those, not an arbitrary first-N.
|
|
4009
4242
|
const candidateRows =
|
|
@@ -4036,24 +4269,32 @@ const FormEngineImpl = ({
|
|
|
4036
4269
|
);
|
|
4037
4270
|
const moreOpen = Boolean(query) || expandedMoreBoxes.includes(box.key);
|
|
4038
4271
|
const accent =
|
|
4039
|
-
box.key === 'attention' ? cWarning
|
|
4272
|
+
box.key === 'attention' && readFirstAttentionFaultCount ? cWarning
|
|
4040
4273
|
: box.key === 'set' ? cSuccess
|
|
4041
4274
|
: cMuted;
|
|
4042
4275
|
// The muted "Optional" box reads as a quieter, recessed
|
|
4043
4276
|
// surface — a touch darker than the page rather than the
|
|
4044
4277
|
// faint grey tint the accent would give.
|
|
4278
|
+
// Calm: every box is the same panel — no recessed one.
|
|
4045
4279
|
const boxBg =
|
|
4046
|
-
box.key === 'optional' ?
|
|
4280
|
+
box.key === 'optional' && !calm ?
|
|
4047
4281
|
changeDarkness(getMainBackgroundColor(theme), 0.06)
|
|
4048
4282
|
: undefined;
|
|
4049
4283
|
return (
|
|
4050
4284
|
<StyledStatusBox
|
|
4051
4285
|
$accent={accent}
|
|
4052
4286
|
$bg={boxBg}
|
|
4287
|
+
$calm={calm}
|
|
4053
4288
|
// Only a box that PINS needs an opaque header (see
|
|
4054
4289
|
// StyledStatusBox); passing it unconditionally would
|
|
4055
|
-
// repaint every form's boxes for nothing.
|
|
4056
|
-
|
|
4290
|
+
// repaint every form's boxes for nothing. A calm box
|
|
4291
|
+
// pins on its own panel surface.
|
|
4292
|
+
$surface={
|
|
4293
|
+
stickyBoxHeaders ?
|
|
4294
|
+
calm ? cPanel
|
|
4295
|
+
: getMainBackgroundColor(theme)
|
|
4296
|
+
: undefined
|
|
4297
|
+
}
|
|
4057
4298
|
key={box.key}
|
|
4058
4299
|
ref={(node: HTMLDivElement | null) => {
|
|
4059
4300
|
statusBoxRefs.current[box.key] = node;
|
|
@@ -4063,7 +4304,11 @@ const FormEngineImpl = ({
|
|
|
4063
4304
|
onCollapseChange={(collapsed) =>
|
|
4064
4305
|
setOpenedStatusBox(collapsed ? undefined : box.key)
|
|
4065
4306
|
}
|
|
4066
|
-
|
|
4307
|
+
// Calm: an ordinary panel — Reqore paints the theme's
|
|
4308
|
+
// panel surface and its hairline edge. The header stays
|
|
4309
|
+
// minimal (no surface or rule of its own); the first
|
|
4310
|
+
// row's hairline is the line under it.
|
|
4311
|
+
flat={!calm}
|
|
4067
4312
|
minimal
|
|
4068
4313
|
/* The header stays on screen while its own box is
|
|
4069
4314
|
being read — see `compactStickyBoxHeaders`. Sticky
|
|
@@ -4101,29 +4346,57 @@ const FormEngineImpl = ({
|
|
|
4101
4346
|
// author.
|
|
4102
4347
|
isCollapsed={wouldCollapse && !hasPreselectedRows}
|
|
4103
4348
|
label={
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4349
|
+
calm ?
|
|
4350
|
+
// One status dot and a word — and only where a
|
|
4351
|
+
// person is needed. The count is a quiet number.
|
|
4352
|
+
<StyledGroupHeader $calm>
|
|
4353
|
+
{box.key === 'attention' && readFirstAttentionFaultCount ?
|
|
4354
|
+
<StyledStatusDot
|
|
4355
|
+
className='options-readfirst-group-dot'
|
|
4356
|
+
$color={cWarning}
|
|
4357
|
+
aria-hidden
|
|
4358
|
+
/>
|
|
4359
|
+
: null}
|
|
4360
|
+
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
4361
|
+
{box.label}
|
|
4362
|
+
</ReqoreP>
|
|
4363
|
+
{headerCount ?
|
|
4364
|
+
<ReqoreSpan
|
|
4365
|
+
className='options-readfirst-group-count'
|
|
4366
|
+
size='small'
|
|
4367
|
+
effect={{ opacity: 0.65 }}
|
|
4368
|
+
>
|
|
4369
|
+
{headerCount}
|
|
4370
|
+
</ReqoreSpan>
|
|
4371
|
+
: null}
|
|
4372
|
+
</StyledGroupHeader>
|
|
4373
|
+
: <StyledGroupHeader>
|
|
4374
|
+
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
4375
|
+
{box.label}
|
|
4376
|
+
</ReqoreP>
|
|
4377
|
+
{headerCount ?
|
|
4378
|
+
<ReqoreTag
|
|
4379
|
+
size='small'
|
|
4380
|
+
minimal
|
|
4381
|
+
compact
|
|
4382
|
+
intent={box.intent}
|
|
4383
|
+
label={String(headerCount)}
|
|
4384
|
+
/>
|
|
4385
|
+
: null}
|
|
4386
|
+
</StyledGroupHeader>
|
|
4116
4387
|
}
|
|
4117
|
-
icon={box.icon}
|
|
4388
|
+
icon={calm ? undefined : box.icon}
|
|
4118
4389
|
className='options-readfirst-group'
|
|
4119
4390
|
padded={false}
|
|
4120
|
-
contentStyle={{ padding: '4px 4px 6px' }}
|
|
4391
|
+
contentStyle={{ padding: calm ? 0 : '4px 4px 6px' }}
|
|
4121
4392
|
>
|
|
4122
4393
|
{/* ONE group body per box: every field block (and the
|
|
4123
4394
|
thin schema sub-labels) is a direct flex child, so the
|
|
4124
4395
|
inter-field gap is identical everywhere — including
|
|
4125
4396
|
across schema-group boundaries. */}
|
|
4126
4397
|
<StyledGroupBody
|
|
4398
|
+
$calm={calm}
|
|
4399
|
+
$hairline={cHairline}
|
|
4127
4400
|
$divider={cDivider}
|
|
4128
4401
|
$hover={cHover}
|
|
4129
4402
|
$focus={cWarning}
|
|
@@ -4379,14 +4652,16 @@ const FormEngineImpl = ({
|
|
|
4379
4652
|
}
|
|
4380
4653
|
: undefined,
|
|
4381
4654
|
onClick:
|
|
4382
|
-
options[optionName]
|
|
4655
|
+
fieldHasLongDescription(options[optionName]) ?
|
|
4656
|
+
() => handleOptionLabelClick(optionName)
|
|
4657
|
+
: undefined,
|
|
4383
4658
|
},
|
|
4384
4659
|
showLabelTooltip: !!options[optionName]?.short_desc,
|
|
4385
4660
|
customLabelTooltip: {
|
|
4386
4661
|
delay: 300,
|
|
4387
4662
|
content: (
|
|
4388
4663
|
<Description
|
|
4389
|
-
longDescription={`${options[optionName]?.short_desc}${options[optionName]
|
|
4664
|
+
longDescription={`${options[optionName]?.short_desc}${fieldHasLongDescription(options[optionName]) ? '. Click to learn more.' : ''}`}
|
|
4390
4665
|
margin='none'
|
|
4391
4666
|
/>
|
|
4392
4667
|
),
|
|
@@ -4398,7 +4673,8 @@ const FormEngineImpl = ({
|
|
|
4398
4673
|
descriptionEffect: {
|
|
4399
4674
|
textSize: '11px',
|
|
4400
4675
|
opacity: 0.3,
|
|
4401
|
-
underline:
|
|
4676
|
+
underline:
|
|
4677
|
+
fieldHasLongDescription(options[optionName]) ? 'underline dotted #878787' : undefined,
|
|
4402
4678
|
},
|
|
4403
4679
|
customTheme: {
|
|
4404
4680
|
main: 'main:darken:1',
|
|
@@ -4560,10 +4836,20 @@ const FormEngineImpl = ({
|
|
|
4560
4836
|
* a child of the row it belongs to. Threading a renderer to each of them by
|
|
4561
4837
|
* hand is how one of them gets missed.
|
|
4562
4838
|
*/
|
|
4563
|
-
export const FormEngine = (props: IFormEngineProps) =>
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
);
|
|
4839
|
+
export const FormEngine = (props: IFormEngineProps) => {
|
|
4840
|
+
// The form's own `appearance` wins; without one it follows the form (or the
|
|
4841
|
+
// host's provider) it sits in, so a nested sub-form never changes look
|
|
4842
|
+
// halfway down its parent.
|
|
4843
|
+
const inherited = useFormAppearance();
|
|
4844
|
+
const appearance = props.appearance ?? inherited;
|
|
4845
|
+
|
|
4846
|
+
return (
|
|
4847
|
+
<MarkdownRendererContext.Provider value={props.markdownRenderer}>
|
|
4848
|
+
<FormAppearanceProvider value={appearance}>
|
|
4849
|
+
<FormEngineImpl {...props} />
|
|
4850
|
+
</FormAppearanceProvider>
|
|
4851
|
+
</MarkdownRendererContext.Provider>
|
|
4852
|
+
);
|
|
4853
|
+
};
|
|
4568
4854
|
|
|
4569
4855
|
export default FormEngine;
|