@qoretechnologies/reqraft 0.10.2 → 0.10.4
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/design/COMPACT_ENGINE_REDESIGN.md +156 -0
- package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +153 -94
- 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 +130 -94
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +181 -45
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +6 -3
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +70 -48
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -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 +19 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +22 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.js +80 -0
- package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.js +138 -0
- package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
- package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.js +139 -0
- package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
- package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantModel.js +133 -0
- package/dist/components/form/engine/variants/variantModel.js.map +1 -0
- package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
- package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantParts.js +191 -0
- package/dist/components/form/engine/variants/variantParts.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +2 -2
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +256 -234
- package/src/components/form/engine/CompactToolbar.tsx +108 -68
- package/src/components/form/engine/FormEngine.stories.tsx +127 -110
- package/src/components/form/engine/FormEngine.tsx +248 -67
- package/src/components/form/engine/compactRowStyles.ts +207 -134
- package/src/components/form/engine/compactToolbarContext.ts +1 -0
- package/src/components/form/engine/readFirst.ts +35 -0
- package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +119 -0
- package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
- package/src/components/form/engine/variants/VariantCards.tsx +212 -0
- package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
- package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
- package/src/components/form/engine/variants/focusDemo.ts +145 -0
- package/src/components/form/engine/variants/variantModel.ts +216 -0
- package/src/components/form/engine/variants/variantParts.tsx +313 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +5 -0
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
ReqoreCollection,
|
|
4
4
|
ReqoreControlGroup,
|
|
5
5
|
ReqoreErrorBoundary,
|
|
6
|
+
ReqoreIcon,
|
|
6
7
|
ReqoreMessage,
|
|
7
8
|
ReqoreP,
|
|
8
|
-
ReqorePanel,
|
|
9
9
|
ReqoreSkeleton,
|
|
10
10
|
ReqoreTag,
|
|
11
11
|
ReqoreTagGroup,
|
|
@@ -86,15 +86,21 @@ import {
|
|
|
86
86
|
StyledCompactPanel,
|
|
87
87
|
StyledGroupBody,
|
|
88
88
|
StyledGroupHeader,
|
|
89
|
-
|
|
89
|
+
StyledRequiredClusterBox,
|
|
90
|
+
StyledRequiredClusterHeader,
|
|
91
|
+
StyledStatusBox,
|
|
92
|
+
StyledStatusBoxGroupLabel,
|
|
90
93
|
} from './compactRowStyles';
|
|
91
94
|
import { OptionFieldMessages } from './OptionFieldMessages';
|
|
92
95
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
93
96
|
import {
|
|
94
97
|
getOptionGroup,
|
|
95
98
|
getOptionGroupLabel,
|
|
99
|
+
getReadFirstBucket,
|
|
96
100
|
getReadFirstCompletion,
|
|
101
|
+
getReadFirstStatus,
|
|
97
102
|
isOptionValueEmpty,
|
|
103
|
+
TReadFirstStatus,
|
|
98
104
|
} from './readFirst';
|
|
99
105
|
|
|
100
106
|
// Re-export types for consumers
|
|
@@ -560,6 +566,10 @@ export const FormEngine = ({
|
|
|
560
566
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
561
567
|
// Which options are expanded into their editor (several can be open at once).
|
|
562
568
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
569
|
+
// Remembers each row's last settled status box, so an actively-edited field
|
|
570
|
+
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
571
|
+
// another box mid-edit and stealing focus. Keyed by option name.
|
|
572
|
+
const settledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
563
573
|
// Measured form width (not viewport — the form lives in drawers/panels of
|
|
564
574
|
// arbitrary width) drives the stacked narrow layout.
|
|
565
575
|
const [compactWrapRef, { width: compactWrapWidth }] = useMeasure<HTMLDivElement>();
|
|
@@ -1269,6 +1279,73 @@ export const FormEngine = ({
|
|
|
1269
1279
|
);
|
|
1270
1280
|
}, [showInvalidOptionsOnly, JSON.stringify(availableOptions)]);
|
|
1271
1281
|
|
|
1282
|
+
// Read-first STATUS / BOX for one option — lifted to component scope so the
|
|
1283
|
+
// status boxes (renderCompact) and the header's "needs attention" count share
|
|
1284
|
+
// exactly one definition. One-of group members travel together (bucket by the
|
|
1285
|
+
// group's satisfaction); everything else by its own status.
|
|
1286
|
+
const schemaMsgIntent = useCallback(
|
|
1287
|
+
(name: string): 'danger' | 'warning' | undefined => {
|
|
1288
|
+
const msgs = ((options?.[name] as { messages?: Array<{ intent?: string }> } | undefined)
|
|
1289
|
+
?.messages || []) as Array<{ intent?: string }>;
|
|
1290
|
+
if (msgs.some((m) => m.intent === 'danger')) return 'danger';
|
|
1291
|
+
if (msgs.some((m) => m.intent === 'warning')) return 'warning';
|
|
1292
|
+
return undefined;
|
|
1293
|
+
},
|
|
1294
|
+
[JSON.stringify(options)]
|
|
1295
|
+
);
|
|
1296
|
+
const getOptionStatus = useCallback(
|
|
1297
|
+
(name: string, hidden = false): TReadFirstStatus => {
|
|
1298
|
+
if (hidden) return 'optional';
|
|
1299
|
+
const schema = options?.[name];
|
|
1300
|
+
const type = (schema?.ui_type || schema?.type) as TQorusType;
|
|
1301
|
+
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1302
|
+
const empty = isOptionValueEmpty(value);
|
|
1303
|
+
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
1304
|
+
const required = !!(schema?.required || reqGroups.length);
|
|
1305
|
+
const covered =
|
|
1306
|
+
empty &&
|
|
1307
|
+
reqGroups.some((g) => {
|
|
1308
|
+
const by = requiredGroupsInfo.satisfiedBy[g];
|
|
1309
|
+
return !!by && by !== name;
|
|
1310
|
+
});
|
|
1311
|
+
const msgIntent = schemaMsgIntent(name);
|
|
1312
|
+
const invalid = (!empty && !isOptionValid(name, type, value)) || msgIntent === 'danger';
|
|
1313
|
+
return getReadFirstStatus({
|
|
1314
|
+
empty,
|
|
1315
|
+
required,
|
|
1316
|
+
covered,
|
|
1317
|
+
invalid,
|
|
1318
|
+
warned: msgIntent === 'warning',
|
|
1319
|
+
});
|
|
1320
|
+
},
|
|
1321
|
+
[
|
|
1322
|
+
JSON.stringify(options),
|
|
1323
|
+
JSON.stringify(availableOptions),
|
|
1324
|
+
isOptionValid,
|
|
1325
|
+
requiredGroupsInfo,
|
|
1326
|
+
schemaMsgIntent,
|
|
1327
|
+
]
|
|
1328
|
+
);
|
|
1329
|
+
const getOptionBucket = useCallback(
|
|
1330
|
+
(name: string, hidden = false): 'attention' | 'set' | 'optional' => {
|
|
1331
|
+
if (!hidden) {
|
|
1332
|
+
const reqGroups = (options?.[name]?.required_groups as string[] | undefined) || [];
|
|
1333
|
+
if (reqGroups.length) {
|
|
1334
|
+
return reqGroups.some((g) => !requiredGroupsInfo.satisfiedBy[g]) ? 'attention' : 'set';
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return getReadFirstBucket(getOptionStatus(name, hidden));
|
|
1338
|
+
},
|
|
1339
|
+
[JSON.stringify(options), requiredGroupsInfo, getOptionStatus]
|
|
1340
|
+
);
|
|
1341
|
+
// How many fields are in the "Needs attention" box — drives the header link.
|
|
1342
|
+
const readFirstAttentionCount = useMemo(
|
|
1343
|
+
() =>
|
|
1344
|
+
Object.keys(availableOptions || {}).filter((name) => getOptionBucket(name) === 'attention')
|
|
1345
|
+
.length,
|
|
1346
|
+
[JSON.stringify(availableOptions), getOptionBucket]
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1272
1349
|
const getIntent = useCallback(
|
|
1273
1350
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1274
1351
|
(optName: string, type: TQorusType, optValue: any, _op: any): TReqoreIntent => {
|
|
@@ -1430,20 +1507,37 @@ export const FormEngine = ({
|
|
|
1430
1507
|
const operatorParts = fixOperatorValue(other.op);
|
|
1431
1508
|
return (
|
|
1432
1509
|
<>
|
|
1433
|
-
{(
|
|
1434
|
-
|
|
1510
|
+
{(() => {
|
|
1511
|
+
const schemaMsgs = (
|
|
1512
|
+
suppressSchemaMessages ? [] : (options?.[optionName] as any)?.messages || []
|
|
1513
|
+
) as { intent?: string; title?: string; content?: string }[];
|
|
1514
|
+
if (!schemaMsgs.length) return null;
|
|
1515
|
+
const items = schemaMsgs.map(({ intent, title, content }, index) => (
|
|
1435
1516
|
<ReqoreMessage
|
|
1436
|
-
intent={intent}
|
|
1517
|
+
intent={intent as never}
|
|
1437
1518
|
title={title}
|
|
1438
1519
|
key={title || index}
|
|
1439
1520
|
opaque={false}
|
|
1440
1521
|
size='small'
|
|
1441
|
-
|
|
1522
|
+
// Compact: flat (no border) to match the read-row info panels;
|
|
1523
|
+
// classic forms keep the bordered, bottom-margined message.
|
|
1524
|
+
flat={compact || undefined}
|
|
1525
|
+
margin={compact ? undefined : 'bottom'}
|
|
1442
1526
|
>
|
|
1443
1527
|
{content}
|
|
1444
1528
|
</ReqoreMessage>
|
|
1445
|
-
)
|
|
1446
|
-
|
|
1529
|
+
));
|
|
1530
|
+
// Compact: stack them in a 4px-gap panel so a field's messages look
|
|
1531
|
+
// identical whether the row is collapsed (read panel) or expanded.
|
|
1532
|
+
return compact ?
|
|
1533
|
+
<div
|
|
1534
|
+
className='options-readfirst-info-panel'
|
|
1535
|
+
style={{ display: 'flex', flexFlow: 'column', gap: 4, marginBottom: 8 }}
|
|
1536
|
+
>
|
|
1537
|
+
{items}
|
|
1538
|
+
</div>
|
|
1539
|
+
: <>{items}</>;
|
|
1540
|
+
})()}
|
|
1447
1541
|
{operators && size(operators) ?
|
|
1448
1542
|
<>
|
|
1449
1543
|
<ReqoreControlGroup fill wrap className='operators'>
|
|
@@ -1498,6 +1592,9 @@ export const FormEngine = ({
|
|
|
1498
1592
|
<TemplateField
|
|
1499
1593
|
fluid
|
|
1500
1594
|
{...(options?.[optionName] as any)}
|
|
1595
|
+
// Propagate compact so an arg_schema field renders a COMPACT sub-form
|
|
1596
|
+
// (consistent with the parent) rather than the classic FormEngine.
|
|
1597
|
+
compact={compact}
|
|
1501
1598
|
// SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
|
|
1502
1599
|
// which renders consumer-injected editors by field type/ui_type.
|
|
1503
1600
|
componentOverrides={componentOverrides}
|
|
@@ -1726,6 +1823,7 @@ export const FormEngine = ({
|
|
|
1726
1823
|
() => ({
|
|
1727
1824
|
readOnly,
|
|
1728
1825
|
invalidCount: size(validityData.invalidFields),
|
|
1826
|
+
attentionCount: readFirstAttentionCount,
|
|
1729
1827
|
completion: readFirstCompletion,
|
|
1730
1828
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
1731
1829
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
@@ -1751,6 +1849,7 @@ export const FormEngine = ({
|
|
|
1751
1849
|
[
|
|
1752
1850
|
readOnly,
|
|
1753
1851
|
validityData,
|
|
1852
|
+
readFirstAttentionCount,
|
|
1754
1853
|
readFirstCompletion,
|
|
1755
1854
|
showInvalidOptionsOnly,
|
|
1756
1855
|
handleToggleInvalidOnly,
|
|
@@ -1863,6 +1962,67 @@ export const FormEngine = ({
|
|
|
1863
1962
|
return groupOrder.indexOf(a) - groupOrder.indexOf(b);
|
|
1864
1963
|
});
|
|
1865
1964
|
|
|
1965
|
+
// Read-first STATUS per row → one of three boxes (Needs attention / Set /
|
|
1966
|
+
// Optional), via the component-scope getOptionBucket (shared with the header
|
|
1967
|
+
// count and the row dot, so box ↔ dot ↔ header always agree). Buckets keep
|
|
1968
|
+
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
1969
|
+
// unmet one-of group's members all land in attention and still rail up.
|
|
1970
|
+
type TRowEntry = { name: string; hidden: boolean };
|
|
1971
|
+
type TBucketKey = 'attention' | 'set' | 'optional';
|
|
1972
|
+
const buckets: Record<TBucketKey, Record<string, TRowEntry[]>> = {
|
|
1973
|
+
attention: {},
|
|
1974
|
+
set: {},
|
|
1975
|
+
optional: {},
|
|
1976
|
+
};
|
|
1977
|
+
const bucketGroups: Record<TBucketKey, string[]> = { attention: [], set: [], optional: [] };
|
|
1978
|
+
// Freeze the box of any field currently being edited (or whose one-of group
|
|
1979
|
+
// has an edited member) to its last settled box — so finishing an edit that
|
|
1980
|
+
// flips its status doesn't remount it in another box and steal focus.
|
|
1981
|
+
const stableBucketOf = (entry: TRowEntry): TBucketKey => {
|
|
1982
|
+
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
1983
|
+
const groupBeingEdited =
|
|
1984
|
+
!entry.hidden &&
|
|
1985
|
+
((options?.[entry.name]?.required_groups as string[] | undefined) || []).some((g) =>
|
|
1986
|
+
(requiredGroupsInfo.members[g] || []).some((m) => expandedOptions.includes(m))
|
|
1987
|
+
);
|
|
1988
|
+
if (!entry.hidden && (expandedOptions.includes(entry.name) || groupBeingEdited)) {
|
|
1989
|
+
const memo = settledBucket.current[entry.name];
|
|
1990
|
+
if (memo) return memo;
|
|
1991
|
+
}
|
|
1992
|
+
settledBucket.current[entry.name] = fresh;
|
|
1993
|
+
return fresh;
|
|
1994
|
+
};
|
|
1995
|
+
groupKeys.forEach((groupName) => {
|
|
1996
|
+
grouped[groupName].forEach((entry) => {
|
|
1997
|
+
const b = stableBucketOf(entry);
|
|
1998
|
+
if (!buckets[b][groupName]) {
|
|
1999
|
+
buckets[b][groupName] = [];
|
|
2000
|
+
bucketGroups[b].push(groupName);
|
|
2001
|
+
}
|
|
2002
|
+
buckets[b][groupName].push(entry);
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
const bucketCount = (b: TBucketKey) =>
|
|
2006
|
+
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2007
|
+
// 'general' / 'optional' are the SYNTHETIC fallback group keys getOptionGroup
|
|
2008
|
+
// assigns to fields with no explicit `group` — printing a "General"/"Optional"
|
|
2009
|
+
// sub-label for those is just noise, so suppress it. BUT a consumer may also
|
|
2010
|
+
// use 'general' as a REAL group (defining it in the `groups` prop and tagging
|
|
2011
|
+
// fields with `group: 'general'`); in that case it's a named group like any
|
|
2012
|
+
// other and DOES get its sub-label.
|
|
2013
|
+
const showGroupSubLabel = (groupName: string) =>
|
|
2014
|
+
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
2015
|
+
const STATUS_BOXES: Array<{
|
|
2016
|
+
key: TBucketKey;
|
|
2017
|
+
label: string;
|
|
2018
|
+
intent?: 'warning' | 'success';
|
|
2019
|
+
icon: IReqoreIconName;
|
|
2020
|
+
}> = [
|
|
2021
|
+
{ key: 'attention', label: 'Needs attention', intent: 'warning', icon: 'ErrorWarningLine' },
|
|
2022
|
+
{ key: 'set', label: 'Set', intent: 'success', icon: 'CheckLine' },
|
|
2023
|
+
{ key: 'optional', label: 'Optional', icon: 'CheckboxBlankCircleLine' },
|
|
2024
|
+
];
|
|
2025
|
+
|
|
1866
2026
|
// Build the rows for one group: contiguous required-group members are pulled
|
|
1867
2027
|
// together at the first member's slot and rendered as a connected rail (flat
|
|
1868
2028
|
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
@@ -1891,7 +2051,9 @@ export const FormEngine = ({
|
|
|
1891
2051
|
clusterLast={clusterLast}
|
|
1892
2052
|
/>
|
|
1893
2053
|
);
|
|
1894
|
-
|
|
2054
|
+
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
2055
|
+
// required" box wraps the members regardless of width; it no longer relies
|
|
2056
|
+
// on a contiguous rail.)
|
|
1895
2057
|
const emitted = new Set<string>();
|
|
1896
2058
|
const groupOf = (name: string) =>
|
|
1897
2059
|
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
@@ -1902,9 +2064,27 @@ export const FormEngine = ({
|
|
|
1902
2064
|
const memberEntries = names.filter((e) => !e.hidden && groupOf(e.name) === grp);
|
|
1903
2065
|
if (memberEntries.length < 2) return renderRow(entry, false);
|
|
1904
2066
|
emitted.add(grp);
|
|
1905
|
-
|
|
2067
|
+
const railed = memberEntries.map((e, idx) =>
|
|
1906
2068
|
renderRow(e, true, idx === 0, idx === memberEntries.length - 1)
|
|
1907
2069
|
);
|
|
2070
|
+
// An UNMET one-of group gets the explicit "One of the below is required"
|
|
2071
|
+
// box (the Focus cluster). A met group needs no banner — the rail + the
|
|
2072
|
+
// "Covers"/"Covered by" chips already say which member satisfies it.
|
|
2073
|
+
if (requiredGroupsInfo.satisfiedBy[grp]) return railed;
|
|
2074
|
+
return (
|
|
2075
|
+
<StyledRequiredClusterBox
|
|
2076
|
+
key={grp}
|
|
2077
|
+
className='options-readfirst-required-cluster'
|
|
2078
|
+
$border={`${cWarning}33`}
|
|
2079
|
+
$tint={`${cWarning}0d`}
|
|
2080
|
+
>
|
|
2081
|
+
<StyledRequiredClusterHeader $color={cWarning}>
|
|
2082
|
+
<ReqoreIcon icon='LinkM' size='11px' style={{ color: cWarning }} />
|
|
2083
|
+
One of the below is required
|
|
2084
|
+
</StyledRequiredClusterHeader>
|
|
2085
|
+
{railed}
|
|
2086
|
+
</StyledRequiredClusterBox>
|
|
2087
|
+
);
|
|
1908
2088
|
});
|
|
1909
2089
|
};
|
|
1910
2090
|
|
|
@@ -1943,78 +2123,53 @@ export const FormEngine = ({
|
|
|
1943
2123
|
</ReqoreMessage>
|
|
1944
2124
|
: null}
|
|
1945
2125
|
|
|
1946
|
-
{
|
|
1947
|
-
const
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2126
|
+
{STATUS_BOXES.map((box) => {
|
|
2127
|
+
const groupsInBox = bucketGroups[box.key];
|
|
2128
|
+
const count = bucketCount(box.key);
|
|
2129
|
+
if (!count) return null;
|
|
2130
|
+
const accent =
|
|
2131
|
+
box.key === 'attention' ? cWarning
|
|
2132
|
+
: box.key === 'set' ? cSuccess
|
|
2133
|
+
: cMuted;
|
|
2134
|
+
// The muted "Optional" box reads as a quieter, recessed
|
|
2135
|
+
// surface — a touch darker than the page rather than the
|
|
2136
|
+
// faint grey tint the accent would give.
|
|
2137
|
+
const boxBg =
|
|
2138
|
+
box.key === 'optional' ?
|
|
2139
|
+
changeDarkness(getMainBackgroundColor(theme), 0.06)
|
|
2140
|
+
: undefined;
|
|
1960
2141
|
return (
|
|
1961
|
-
<
|
|
1962
|
-
|
|
2142
|
+
<StyledStatusBox
|
|
2143
|
+
$accent={accent}
|
|
2144
|
+
$bg={boxBg}
|
|
2145
|
+
key={box.key}
|
|
1963
2146
|
flat
|
|
1964
2147
|
minimal
|
|
1965
2148
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
1966
2149
|
collapsible
|
|
1967
2150
|
label={
|
|
1968
2151
|
<StyledGroupHeader>
|
|
1969
|
-
<ReqoreP effect={{ weight: 'bold' }} size='
|
|
1970
|
-
{
|
|
2152
|
+
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
2153
|
+
{box.label}
|
|
1971
2154
|
</ReqoreP>
|
|
1972
|
-
<
|
|
1973
|
-
|
|
1974
|
-
readOnly
|
|
1975
|
-
size='tiny'
|
|
2155
|
+
<ReqoreTag
|
|
2156
|
+
size='small'
|
|
1976
2157
|
minimal
|
|
1977
|
-
flat
|
|
1978
2158
|
compact
|
|
1979
|
-
|
|
1980
|
-
{
|
|
1981
|
-
: invalidCount ?
|
|
1982
|
-
{
|
|
1983
|
-
label: `${invalidCount} to resolve`,
|
|
1984
|
-
intent: 'warning' as const,
|
|
1985
|
-
icon: 'ErrorWarningLine' as const,
|
|
1986
|
-
}
|
|
1987
|
-
: {
|
|
1988
|
-
label: 'All set',
|
|
1989
|
-
intent: 'success' as const,
|
|
1990
|
-
icon: 'CheckLine' as const,
|
|
1991
|
-
})}
|
|
2159
|
+
intent={box.intent}
|
|
2160
|
+
label={String(count)}
|
|
1992
2161
|
/>
|
|
1993
2162
|
</StyledGroupHeader>
|
|
1994
2163
|
}
|
|
1995
|
-
icon={
|
|
2164
|
+
icon={box.icon}
|
|
1996
2165
|
className='options-readfirst-group'
|
|
1997
2166
|
padded={false}
|
|
1998
2167
|
contentStyle={{ padding: '4px 4px 6px' }}
|
|
1999
2168
|
>
|
|
2000
|
-
{
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
// Indent to the same content line as the rows (StyledGroupBody's
|
|
2005
|
-
// `margin-left` clamp) so the subtitle sits tucked under the group
|
|
2006
|
-
// name instead of at the panel edge — and clears the group's
|
|
2007
|
-
// vertical rule (left:16px) rather than crossing it.
|
|
2008
|
-
style={{
|
|
2009
|
-
marginTop: 2,
|
|
2010
|
-
marginBottom: 8,
|
|
2011
|
-
marginLeft: GROUP_INDENT,
|
|
2012
|
-
paddingRight: 10,
|
|
2013
|
-
}}
|
|
2014
|
-
>
|
|
2015
|
-
{groupConfig.subtitle}
|
|
2016
|
-
</ReqoreP>
|
|
2017
|
-
: null}
|
|
2169
|
+
{/* ONE group body per box: every field block (and the
|
|
2170
|
+
thin schema sub-labels) is a direct flex child, so the
|
|
2171
|
+
inter-field gap is identical everywhere — including
|
|
2172
|
+
across schema-group boundaries. */}
|
|
2018
2173
|
<StyledGroupBody
|
|
2019
2174
|
$divider={cDivider}
|
|
2020
2175
|
$hover={cHover}
|
|
@@ -2024,9 +2179,35 @@ export const FormEngine = ({
|
|
|
2024
2179
|
$lineColor={cGroupLine}
|
|
2025
2180
|
className={compactNarrow ? 'readfirst-narrow' : undefined}
|
|
2026
2181
|
>
|
|
2027
|
-
{
|
|
2182
|
+
{groupsInBox.map((groupName) => {
|
|
2183
|
+
const groupConfig = groups?.[groupName];
|
|
2184
|
+
return (
|
|
2185
|
+
<React.Fragment key={groupName}>
|
|
2186
|
+
{showGroupSubLabel(groupName) ?
|
|
2187
|
+
<StyledStatusBoxGroupLabel>
|
|
2188
|
+
{getOptionGroupLabel(groupName, groups)}
|
|
2189
|
+
</StyledStatusBoxGroupLabel>
|
|
2190
|
+
: null}
|
|
2191
|
+
{showGroupSubLabel(groupName) && groupConfig?.subtitle ?
|
|
2192
|
+
<ReqoreP
|
|
2193
|
+
size='small'
|
|
2194
|
+
effect={{ opacity: 0.6 }}
|
|
2195
|
+
style={{
|
|
2196
|
+
marginTop: 2,
|
|
2197
|
+
marginBottom: 8,
|
|
2198
|
+
marginLeft: GROUP_INDENT,
|
|
2199
|
+
paddingRight: 10,
|
|
2200
|
+
}}
|
|
2201
|
+
>
|
|
2202
|
+
{groupConfig.subtitle}
|
|
2203
|
+
</ReqoreP>
|
|
2204
|
+
: null}
|
|
2205
|
+
{renderGroupRows(buckets[box.key][groupName])}
|
|
2206
|
+
</React.Fragment>
|
|
2207
|
+
);
|
|
2208
|
+
})}
|
|
2028
2209
|
</StyledGroupBody>
|
|
2029
|
-
</
|
|
2210
|
+
</StyledStatusBox>
|
|
2030
2211
|
);
|
|
2031
2212
|
})}
|
|
2032
2213
|
</StyledCompactPanel>
|