@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
|
@@ -35,17 +35,16 @@ import {
|
|
|
35
35
|
StyledActionSlot,
|
|
36
36
|
StyledCardHeading,
|
|
37
37
|
StyledCardLabel,
|
|
38
|
-
StyledClusterNode,
|
|
39
38
|
StyledColorSwatch,
|
|
40
39
|
StyledColumn,
|
|
41
40
|
StyledEditCard,
|
|
42
|
-
StyledInfoPanel,
|
|
43
41
|
StyledLabelBlock,
|
|
44
42
|
StyledLabelDesc,
|
|
45
43
|
StyledRowActions,
|
|
46
44
|
StyledRowInset,
|
|
47
45
|
StyledRowLabel,
|
|
48
46
|
StyledRowValue,
|
|
47
|
+
StyledStatusDot,
|
|
49
48
|
} from './compactRowStyles';
|
|
50
49
|
import { getOptionFieldMessages } from './OptionFieldMessages';
|
|
51
50
|
import {
|
|
@@ -55,6 +54,7 @@ import {
|
|
|
55
54
|
getAllowedValueImage,
|
|
56
55
|
getFileSize,
|
|
57
56
|
getHashEntries,
|
|
57
|
+
getReadFirstStatus,
|
|
58
58
|
getValueType,
|
|
59
59
|
isOptionValueEmpty,
|
|
60
60
|
optionHasImages,
|
|
@@ -148,18 +148,10 @@ export const CompactRow = memo(
|
|
|
148
148
|
const isFlashed = useContextSelector(CompactRowContext, (v) =>
|
|
149
149
|
v.flashedOptions.includes(optionName)
|
|
150
150
|
);
|
|
151
|
-
const infoPanelOverride = useContextSelector(
|
|
152
|
-
CompactRowContext,
|
|
153
|
-
(v) => v.infoPanelOverrides[optionName]
|
|
154
|
-
);
|
|
155
151
|
const setHighlightedOptions = useContextSelector(
|
|
156
152
|
CompactRowContext,
|
|
157
153
|
(v) => v.setHighlightedOptions
|
|
158
154
|
);
|
|
159
|
-
const setInfoPanelOverrides = useContextSelector(
|
|
160
|
-
CompactRowContext,
|
|
161
|
-
(v) => v.setInfoPanelOverrides
|
|
162
|
-
);
|
|
163
155
|
const setFocusedEditing = useContextSelector(CompactRowContext, (v) => v.setFocusedEditing);
|
|
164
156
|
const readRowHeights = useContextSelector(CompactRowContext, (v) => v.readRowHeights);
|
|
165
157
|
const originalValue = useContextSelector(CompactRowContext, (v) => v.originalValue);
|
|
@@ -190,14 +182,12 @@ export const CompactRow = memo(
|
|
|
190
182
|
const theme = useContextSelector(CompactRowContext, (v) => v.theme);
|
|
191
183
|
const cMuted = useContextSelector(CompactRowContext, (v) => v.cMuted);
|
|
192
184
|
const templates = useContextSelector(CompactRowContext, (v) => v.templates);
|
|
193
|
-
const cFaint = useContextSelector(CompactRowContext, (v) => v.cFaint);
|
|
194
185
|
const cKey = useContextSelector(CompactRowContext, (v) => v.cKey);
|
|
195
186
|
const cDivider = useContextSelector(CompactRowContext, (v) => v.cDivider);
|
|
196
187
|
const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
|
|
197
188
|
const cDanger = useContextSelector(CompactRowContext, (v) => v.cDanger);
|
|
198
189
|
const cWarning = useContextSelector(CompactRowContext, (v) => v.cWarning);
|
|
199
190
|
const cInfo = useContextSelector(CompactRowContext, (v) => v.cInfo);
|
|
200
|
-
const cBg = useContextSelector(CompactRowContext, (v) => v.cBg);
|
|
201
191
|
|
|
202
192
|
// Value-cell content: colour adds a swatch, file an icon + size; hash keeps
|
|
203
193
|
// its "N fields" summary (sub-fields reveal beneath the row).
|
|
@@ -420,6 +410,21 @@ export const CompactRow = memo(
|
|
|
420
410
|
// A choice with per-option logos (e.g. language) reads better collapsed.
|
|
421
411
|
!optionHasImages(schema) &&
|
|
422
412
|
!COMPACT_COMPLEX_TYPES.has(editType);
|
|
413
|
+
|
|
414
|
+
// Auto-focus the editor's first input when a field is opened, so you can type
|
|
415
|
+
// straight away (matches the prototype's tap-to-edit feel).
|
|
416
|
+
const editorRef = React.useRef<HTMLDivElement>(null);
|
|
417
|
+
React.useEffect(() => {
|
|
418
|
+
if (!isExpanded) return undefined;
|
|
419
|
+
const id = window.setTimeout(() => {
|
|
420
|
+
const el = editorRef.current?.querySelector<HTMLElement>(
|
|
421
|
+
'input:not([type="hidden"]):not([disabled]), textarea, [contenteditable="true"]'
|
|
422
|
+
);
|
|
423
|
+
el?.focus();
|
|
424
|
+
}, 60);
|
|
425
|
+
return () => window.clearTimeout(id);
|
|
426
|
+
}, [isExpanded]);
|
|
427
|
+
|
|
423
428
|
const revertButton =
|
|
424
429
|
changed ?
|
|
425
430
|
<ReqoreButton
|
|
@@ -516,10 +521,13 @@ export const CompactRow = memo(
|
|
|
516
521
|
.filter((m) => m.label !== 'This field is required')
|
|
517
522
|
.map((m) => ({ intent: m.intent as string, content: String(m.label) }))
|
|
518
523
|
: [];
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
524
|
+
// TWO display channels, matching the Focus prototype:
|
|
525
|
+
// • dedicated schema `messages` → prominent coloured PANELS below the row;
|
|
526
|
+
// • everything else (validation reasons, the unmet-dependency hint, the
|
|
527
|
+
// default-value note) → a compact INLINE reason strip — not a panel.
|
|
528
|
+
const panelMessages = schemaMessages;
|
|
529
|
+
const inlineMessages: TInfoMsg[] = [
|
|
530
|
+
...fieldMessages,
|
|
523
531
|
...(infoActive && schema?.default_value_desc ?
|
|
524
532
|
[
|
|
525
533
|
{
|
|
@@ -529,26 +537,20 @@ export const CompactRow = memo(
|
|
|
529
537
|
]
|
|
530
538
|
: []),
|
|
531
539
|
];
|
|
540
|
+
const allMsgs = [...panelMessages, ...inlineMessages];
|
|
532
541
|
const worstIntent =
|
|
533
|
-
|
|
534
|
-
:
|
|
542
|
+
allMsgs.some((m) => m.intent === 'danger') ? 'danger'
|
|
543
|
+
: allMsgs.some((m) => m.intent === 'warning') ? 'warning'
|
|
535
544
|
: undefined;
|
|
536
545
|
const intentColor =
|
|
537
546
|
worstIntent === 'danger' ? cDanger
|
|
538
547
|
: worstIntent === 'warning' ? cWarning
|
|
539
548
|
: undefined;
|
|
540
549
|
const showStripe = infoActive && !!intentColor;
|
|
541
|
-
// The short_desc renders UNDER the field name (revealed by the ⓘ toggle); the
|
|
542
|
-
// value-side panel carries only the messages (tier1/tier2).
|
|
543
550
|
const labelShortDesc = schema?.short_desc;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
// global toggle when ENGAGED (show all / hide all — overriding the message
|
|
548
|
-
// auto-open); else the default, where only critical (tier1) messages open.
|
|
549
|
-
const defaultOpen =
|
|
550
|
-
showAllDescriptions === undefined ? tier1.length > 0 : showAllDescriptions;
|
|
551
|
-
const infoPanelOpen = hasInfoPanelContent && (infoPanelOverride ?? defaultOpen);
|
|
551
|
+
// The per-row ⓘ is gone: short_desc shows under the name only when the global
|
|
552
|
+
// "descriptions" toggle is engaged (and inside the editor when expanded).
|
|
553
|
+
const showLabelDesc = !!labelShortDesc && showAllDescriptions === true;
|
|
552
554
|
|
|
553
555
|
const renderInfoStrip = (m: TInfoMsg, index: number) => (
|
|
554
556
|
<ReqoreMessage
|
|
@@ -562,33 +564,14 @@ export const CompactRow = memo(
|
|
|
562
564
|
{m.content}
|
|
563
565
|
</ReqoreMessage>
|
|
564
566
|
);
|
|
567
|
+
const reasonColor = (intent?: string) =>
|
|
568
|
+
intent === 'danger' ? cDanger
|
|
569
|
+
: intent === 'warning' ? cWarning
|
|
570
|
+
: intent === 'success' ?
|
|
571
|
+
(theme?.intents as Record<string, string> | undefined)?.success || cInfo
|
|
572
|
+
: `${cMuted}cc`;
|
|
565
573
|
|
|
566
|
-
const infoToggle =
|
|
567
|
-
hasInfoPanelContent ?
|
|
568
|
-
<ReqoreButton
|
|
569
|
-
className='options-readfirst-info-toggle'
|
|
570
|
-
size='tiny'
|
|
571
|
-
minimal
|
|
572
|
-
flat
|
|
573
|
-
compact
|
|
574
|
-
fixed
|
|
575
|
-
active={infoPanelOpen}
|
|
576
|
-
intent={worstIntent as never}
|
|
577
|
-
icon={infoPanelOpen ? 'InformationFill' : 'InformationLine'}
|
|
578
|
-
tooltip={infoPanelOpen ? 'Hide field information' : 'Show field information'}
|
|
579
|
-
onClick={(e: React.MouseEvent) => {
|
|
580
|
-
e.stopPropagation();
|
|
581
|
-
setInfoPanelOverrides((prev) => ({ ...prev, [optionName]: !infoPanelOpen }));
|
|
582
|
-
}}
|
|
583
|
-
/>
|
|
584
|
-
: null;
|
|
585
|
-
|
|
586
|
-
const infoBlock =
|
|
587
|
-
infoPanelOpen && panelHasContent ?
|
|
588
|
-
<StyledInfoPanel className='options-readfirst-info-panel'>
|
|
589
|
-
{[...tier1, ...tier2].map(renderInfoStrip)}
|
|
590
|
-
</StyledInfoPanel>
|
|
591
|
-
: null;
|
|
574
|
+
const infoToggle = null;
|
|
592
575
|
|
|
593
576
|
// Cluster (required-group connection) — shared by the read row, its block
|
|
594
577
|
// wrapper AND the inline editor, so the rail/node/highlight persist across
|
|
@@ -604,20 +587,9 @@ export const CompactRow = memo(
|
|
|
604
587
|
clustered ?
|
|
605
588
|
`readfirst-cluster-rail${clusterFirst ? ' readfirst-cluster-first' : ''}${clusterLast ? ' readfirst-cluster-last' : ''}${clusterSatisfied ? ' readfirst-cluster-satisfied' : ''}`
|
|
606
589
|
: '';
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
<StyledClusterNode
|
|
611
|
-
className='options-readfirst-node'
|
|
612
|
-
$filled={!!memberSet}
|
|
613
|
-
$color={
|
|
614
|
-
clusterSatisfied ?
|
|
615
|
-
(theme?.intents as Record<string, string> | undefined)?.success || cInfo
|
|
616
|
-
: `${cWarning}99`
|
|
617
|
-
}
|
|
618
|
-
$bg={cBg}
|
|
619
|
-
/>
|
|
620
|
-
: null;
|
|
590
|
+
// The connection rail + status nodes are gone — the "One of the below is
|
|
591
|
+
// required" box (and the Covers / Covered-by chips) carry the grouping now.
|
|
592
|
+
const clusterNode = null;
|
|
621
593
|
const clusterHoverProps =
|
|
622
594
|
clustered && clusterMembers.length ?
|
|
623
595
|
{
|
|
@@ -626,6 +598,42 @@ export const CompactRow = memo(
|
|
|
626
598
|
}
|
|
627
599
|
: {};
|
|
628
600
|
|
|
601
|
+
// Secondary edit actions tuck into a "More" (⋮) menu so the card header stays
|
|
602
|
+
// calm: Fullscreen always, plus Remove field for a removable option. Rendered
|
|
603
|
+
// just before the Done ✓ in both expanded layouts.
|
|
604
|
+
const moreMenu = (
|
|
605
|
+
<ReqoreDropdown
|
|
606
|
+
className='options-readfirst-more'
|
|
607
|
+
icon='More2Fill'
|
|
608
|
+
flat
|
|
609
|
+
minimal
|
|
610
|
+
fixed
|
|
611
|
+
size='small'
|
|
612
|
+
tooltip='More actions'
|
|
613
|
+
items={[
|
|
614
|
+
{
|
|
615
|
+
label: 'Edit fullscreen',
|
|
616
|
+
icon: 'FullscreenLine',
|
|
617
|
+
onClick: () => setFocusedEditing(optionName),
|
|
618
|
+
} as IReqoreDropdownItem,
|
|
619
|
+
...(removable && !hidden ?
|
|
620
|
+
[
|
|
621
|
+
{
|
|
622
|
+
label: 'Remove field',
|
|
623
|
+
icon: 'DeleteBinLine',
|
|
624
|
+
intent: 'danger',
|
|
625
|
+
onClick: () =>
|
|
626
|
+
confirmAction({
|
|
627
|
+
title: 'Remove field',
|
|
628
|
+
onConfirm: () => removeSelectedOption(optionName),
|
|
629
|
+
}),
|
|
630
|
+
} as IReqoreDropdownItem,
|
|
631
|
+
]
|
|
632
|
+
: []),
|
|
633
|
+
]}
|
|
634
|
+
/>
|
|
635
|
+
);
|
|
636
|
+
|
|
629
637
|
if (isExpanded) {
|
|
630
638
|
if (inlineEditable) {
|
|
631
639
|
const collapse = () => toggleExpandedOption(optionName);
|
|
@@ -641,25 +649,40 @@ export const CompactRow = memo(
|
|
|
641
649
|
}
|
|
642
650
|
{...clusterHoverProps}
|
|
643
651
|
>
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
event.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
652
|
+
<StyledLabelBlock>
|
|
653
|
+
<StyledRowLabel
|
|
654
|
+
role='button'
|
|
655
|
+
tabIndex={0}
|
|
656
|
+
aria-label={`Collapse ${label}`}
|
|
657
|
+
title={schema?.short_desc || undefined}
|
|
658
|
+
$color={cKey}
|
|
659
|
+
$pointer
|
|
660
|
+
onClick={collapse}
|
|
661
|
+
onKeyDown={(event) => {
|
|
662
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
663
|
+
event.preventDefault();
|
|
664
|
+
collapse();
|
|
665
|
+
}
|
|
666
|
+
}}
|
|
667
|
+
>
|
|
668
|
+
{label}
|
|
669
|
+
{required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
|
|
670
|
+
</StyledRowLabel>
|
|
671
|
+
{/* Keep the short_desc visible while editing inline when the global
|
|
672
|
+
descriptions toggle is on — read rows show it, so opening a field
|
|
673
|
+
shouldn't make it vanish. */}
|
|
674
|
+
{showLabelDesc ?
|
|
675
|
+
<StyledLabelDesc
|
|
676
|
+
className='options-readfirst-label-desc'
|
|
677
|
+
size='small'
|
|
678
|
+
effect={{ opacity: 0.55 }}
|
|
679
|
+
>
|
|
680
|
+
{labelShortDesc}
|
|
681
|
+
</StyledLabelDesc>
|
|
682
|
+
: null}
|
|
683
|
+
</StyledLabelBlock>
|
|
662
684
|
<div
|
|
685
|
+
ref={editorRef}
|
|
663
686
|
style={{ minWidth: 0 }}
|
|
664
687
|
onKeyDown={(event) => {
|
|
665
688
|
if (event.key === 'Escape') {
|
|
@@ -678,12 +701,12 @@ export const CompactRow = memo(
|
|
|
678
701
|
message strip is visible. */}
|
|
679
702
|
{revertButton}
|
|
680
703
|
{clearValueButton}
|
|
704
|
+
{moreMenu}
|
|
681
705
|
<ReqoreButton
|
|
682
706
|
className='options-readfirst-done'
|
|
683
707
|
size='small'
|
|
684
|
-
flat
|
|
685
|
-
minimal
|
|
686
708
|
intent='success'
|
|
709
|
+
fixed
|
|
687
710
|
icon='CheckLine'
|
|
688
711
|
tooltip='Done'
|
|
689
712
|
onClick={collapse}
|
|
@@ -749,7 +772,7 @@ export const CompactRow = memo(
|
|
|
749
772
|
}}
|
|
750
773
|
>
|
|
751
774
|
<StyledCardHeading>
|
|
752
|
-
<StyledCardLabel $color={
|
|
775
|
+
<StyledCardLabel $color={cKey}>
|
|
753
776
|
{(schema as { icon?: string } | undefined)?.icon || (schema as { image?: string } | undefined)?.image ?
|
|
754
777
|
<ReqoreIcon
|
|
755
778
|
icon={(schema as { icon?: string } | undefined)?.icon as never}
|
|
@@ -811,18 +834,8 @@ export const CompactRow = memo(
|
|
|
811
834
|
</ReqoreButton>
|
|
812
835
|
);
|
|
813
836
|
})}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
icon='FullscreenLine'
|
|
817
|
-
minimal
|
|
818
|
-
flat
|
|
819
|
-
fixed
|
|
820
|
-
className='options-readfirst-fullscreen'
|
|
821
|
-
tooltip='Edit fullscreen'
|
|
822
|
-
onClick={() => setFocusedEditing(optionName)}
|
|
823
|
-
/>
|
|
824
|
-
{/* Clear-value sits between focus-edit and Done — the card analog of
|
|
825
|
-
the inline row's Clear. Empties the value (keeps the field). */}
|
|
837
|
+
{/* Clear-value sits before the More menu — the card analog of the
|
|
838
|
+
inline row's Clear. Empties the value (keeps the field). */}
|
|
826
839
|
{hasValue && !readOnly ?
|
|
827
840
|
<ReqoreButton
|
|
828
841
|
size='small'
|
|
@@ -835,16 +848,16 @@ export const CompactRow = memo(
|
|
|
835
848
|
onClick={() => handleValueChange(optionName, undefined)}
|
|
836
849
|
/>
|
|
837
850
|
: null}
|
|
851
|
+
{moreMenu}
|
|
838
852
|
<ReqoreButton
|
|
839
853
|
size='small'
|
|
840
|
-
icon='CheckLine'
|
|
854
|
+
icon={readOnly ? 'CloseLine' : 'CheckLine'}
|
|
841
855
|
intent='success'
|
|
842
856
|
fixed
|
|
843
857
|
className='options-readfirst-done'
|
|
858
|
+
tooltip={readOnly ? 'Close' : 'Done'}
|
|
844
859
|
onClick={() => toggleExpandedOption(optionName)}
|
|
845
|
-
|
|
846
|
-
{readOnly ? 'Close' : 'Done'}
|
|
847
|
-
</ReqoreButton>
|
|
860
|
+
/>
|
|
848
861
|
</ReqoreControlGroup>
|
|
849
862
|
</div>
|
|
850
863
|
{/* Same fullscreen focused-editing affordance as the classic cards —
|
|
@@ -869,6 +882,15 @@ export const CompactRow = memo(
|
|
|
869
882
|
|
|
870
883
|
const formatted = formatOptionValue(optionField, schema);
|
|
871
884
|
const empty = formatted === '';
|
|
885
|
+
// Inline reasons shown on the value line: validation / dependency / one-of /
|
|
886
|
+
// default-value hints, plus a read-only covered-by note (editable covered rows
|
|
887
|
+
// carry that in their chip instead).
|
|
888
|
+
const valueReasons: TInfoMsg[] = [
|
|
889
|
+
...inlineMessages,
|
|
890
|
+
...(empty && coveredByLabel ?
|
|
891
|
+
[{ content: `Covered by “${coveredByLabel}”`, intent: 'success' }]
|
|
892
|
+
: []),
|
|
893
|
+
];
|
|
872
894
|
// A hash row reveals its sub-fields as read-only sub-rows under a "view
|
|
873
895
|
// more" disclosure; the row itself still expands the real editor on click.
|
|
874
896
|
const valueType = getValueType(optionField, schema);
|
|
@@ -936,8 +958,7 @@ export const CompactRow = memo(
|
|
|
936
958
|
flat
|
|
937
959
|
compact
|
|
938
960
|
icon='LockLine'
|
|
939
|
-
|
|
940
|
-
effect={{uppercase: true, spaced: 1}}
|
|
961
|
+
tooltip='Disabled — depends on other fields. Click to locate them.'
|
|
941
962
|
items={[
|
|
942
963
|
{ divider: true, label: 'Unlocked by:', dividerAlign: 'left' } as IReqoreDropdownItem,
|
|
943
964
|
...dependencyEntries.flatMap((entry): IReqoreDropdownItem[] => [
|
|
@@ -1002,8 +1023,26 @@ export const CompactRow = memo(
|
|
|
1002
1023
|
rowStripeColor ?
|
|
1003
1024
|
({ ['--readfirst-stripe']: rowStripeColor } as React.CSSProperties)
|
|
1004
1025
|
: undefined;
|
|
1005
|
-
const blockWrapped = hashEntries.length > 0 || !!infoBlock;
|
|
1006
1026
|
|
|
1027
|
+
// "Focus" trailing status dot, derived from the SHARED status helper (the
|
|
1028
|
+
// same one FormEngine uses to bucket rows into the Needs-attention/Set/Optional
|
|
1029
|
+
// boxes — so the dot and the box can never disagree). Hidden (not-yet-added)
|
|
1030
|
+
// and dependency-locked rows show no dot (the add/lock affordance speaks for
|
|
1031
|
+
// them). worstIntent already folds in schema + validation messages.
|
|
1032
|
+
const cSuccess = (theme?.intents as Record<string, string> | undefined)?.success;
|
|
1033
|
+
const rowStatus = getReadFirstStatus({
|
|
1034
|
+
empty,
|
|
1035
|
+
required,
|
|
1036
|
+
covered: !!coveredByLabel || groupResolved,
|
|
1037
|
+
invalid: worstIntent === 'danger',
|
|
1038
|
+
warned: worstIntent === 'warning',
|
|
1039
|
+
});
|
|
1040
|
+
const dotStatus = hidden || fieldDisabled ? undefined : rowStatus;
|
|
1041
|
+
const dotColor =
|
|
1042
|
+
dotStatus === 'invalid' ? cDanger
|
|
1043
|
+
: dotStatus === 'todo' ? cWarning
|
|
1044
|
+
: dotStatus === 'set' ? cSuccess || cInfo
|
|
1045
|
+
: undefined;
|
|
1007
1046
|
const row = (
|
|
1008
1047
|
<div
|
|
1009
1048
|
key={optionName}
|
|
@@ -1011,9 +1050,9 @@ export const CompactRow = memo(
|
|
|
1011
1050
|
role='button'
|
|
1012
1051
|
tabIndex={0}
|
|
1013
1052
|
aria-label={`${label}${hidden ? ' (add field)' : ''}`}
|
|
1014
|
-
className={`readfirst-row options-readfirst-value${hidden ? ' readfirst-row-hidden' : ''}${fieldDisabled ? ' readfirst-row-disabled' : ''}${isHighlighted ? ' readfirst-row-group-highlight' : ''}${isFlashed ? ' readfirst-row-flash' : ''}${
|
|
1053
|
+
className={`readfirst-row options-readfirst-value${hidden ? ' readfirst-row-hidden' : ''}${fieldDisabled ? ' readfirst-row-disabled' : ''}${isHighlighted ? ' readfirst-row-group-highlight' : ''}${isFlashed ? ' readfirst-row-flash' : ''}${showLabelDesc ? ' readfirst-row-info-open' : ''}${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
|
|
1015
1054
|
aria-disabled={fieldDisabled || undefined}
|
|
1016
|
-
style={
|
|
1055
|
+
style={stripeStyle}
|
|
1017
1056
|
onClick={activate}
|
|
1018
1057
|
onKeyDown={(event) => {
|
|
1019
1058
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -1027,31 +1066,37 @@ export const CompactRow = memo(
|
|
|
1027
1066
|
<StyledLabelBlock>
|
|
1028
1067
|
<StyledRowLabel title={schema?.short_desc || undefined} $color={cKey}>
|
|
1029
1068
|
{rowChromeIcon}
|
|
1030
|
-
{label
|
|
1031
|
-
|
|
1069
|
+
{/* label + asterisk + help flow as ONE inline run, so the asterisk
|
|
1070
|
+
stays right after the last word even when the name wraps. */}
|
|
1071
|
+
<span className='options-readfirst-label-text' style={{ minWidth: 0 }}>
|
|
1072
|
+
{label}
|
|
1073
|
+
{required ?
|
|
1074
|
+
<ReqoreIcon icon='Asterisk' color='danger' size='10px' margin='left' marginSize='tiny' />
|
|
1075
|
+
: null}
|
|
1076
|
+
{schema?.desc ?
|
|
1077
|
+
<ReqoreIcon
|
|
1078
|
+
icon='QuestionLine'
|
|
1079
|
+
size='12px'
|
|
1080
|
+
effect={{ opacity: 0.55 }}
|
|
1081
|
+
margin='left'
|
|
1082
|
+
marginSize='tiny'
|
|
1083
|
+
role='button'
|
|
1084
|
+
tabIndex={-1}
|
|
1085
|
+
aria-label='Help'
|
|
1086
|
+
className='options-readfirst-help'
|
|
1087
|
+
style={{ cursor: 'help' }}
|
|
1088
|
+
onClick={(event) => {
|
|
1089
|
+
event.stopPropagation();
|
|
1090
|
+
handleOptionLabelClick(optionName);
|
|
1091
|
+
}}
|
|
1092
|
+
/>
|
|
1093
|
+
: null}
|
|
1094
|
+
</span>
|
|
1032
1095
|
{typeLabel ?
|
|
1033
1096
|
<ReqoreTag size='tiny' minimal label={typeLabel} labelEffect={{ opacity: 0.55 }} />
|
|
1034
1097
|
: null}
|
|
1035
|
-
{schema?.desc ?
|
|
1036
|
-
<ReqoreIcon
|
|
1037
|
-
icon='QuestionLine'
|
|
1038
|
-
size='12px'
|
|
1039
|
-
effect={{ opacity: 0.55 }}
|
|
1040
|
-
margin='left'
|
|
1041
|
-
marginSize={5}
|
|
1042
|
-
role='button'
|
|
1043
|
-
tabIndex={-1}
|
|
1044
|
-
aria-label='Help'
|
|
1045
|
-
className='options-readfirst-help'
|
|
1046
|
-
style={{ cursor: 'help' }}
|
|
1047
|
-
onClick={(event) => {
|
|
1048
|
-
event.stopPropagation();
|
|
1049
|
-
handleOptionLabelClick(optionName);
|
|
1050
|
-
}}
|
|
1051
|
-
/>
|
|
1052
|
-
: null}
|
|
1053
1098
|
</StyledRowLabel>
|
|
1054
|
-
{
|
|
1099
|
+
{showLabelDesc ?
|
|
1055
1100
|
<StyledLabelDesc
|
|
1056
1101
|
className='options-readfirst-label-desc'
|
|
1057
1102
|
size='small'
|
|
@@ -1063,22 +1108,59 @@ export const CompactRow = memo(
|
|
|
1063
1108
|
</StyledLabelBlock>
|
|
1064
1109
|
<StyledRowValue
|
|
1065
1110
|
title={!empty && !hidden && typeof formatted === 'string' ? formatted : undefined}
|
|
1066
|
-
|
|
1111
|
+
// A SET value reads at full key brightness so it stands out as the
|
|
1112
|
+
// actual data — crucial when stacked under a (muted) description on
|
|
1113
|
+
// mobile, where a dim value blends into the prose. The bold name still
|
|
1114
|
+
// outranks it; the empty dash stays faint.
|
|
1115
|
+
$color={empty || hidden ? `${cMuted}66` : cKey}
|
|
1067
1116
|
$empty={empty || hidden}
|
|
1068
1117
|
>
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1118
|
+
<span className='options-readfirst-valuetext'>
|
|
1119
|
+
{hidden || empty ? '—' : renderReadFirstValue(optionField, schema, formatted)}
|
|
1120
|
+
</span>
|
|
1121
|
+
{valueReasons.map((m, i) => (
|
|
1122
|
+
<span
|
|
1123
|
+
key={`${m.content}-${i}`}
|
|
1124
|
+
className='options-readfirst-reason'
|
|
1125
|
+
style={{ color: reasonColor(m.intent) }}
|
|
1126
|
+
>
|
|
1127
|
+
{m.content}
|
|
1128
|
+
</span>
|
|
1129
|
+
))}
|
|
1130
|
+
{/* Dedicated schema message PANELS render right here, directly under the
|
|
1131
|
+
value (full width of the value column) — never pushed down by the
|
|
1132
|
+
label's short_desc. */}
|
|
1133
|
+
{panelMessages.length ?
|
|
1134
|
+
<div className='options-readfirst-info-panel'>{panelMessages.map(renderInfoStrip)}</div>
|
|
1135
|
+
: null}
|
|
1136
|
+
{/* The structured hash/list preview also lives in the value cell, so it
|
|
1137
|
+
starts directly under the value summary ("N fields"), not below the
|
|
1138
|
+
label's short_desc. */}
|
|
1139
|
+
{hashEntries.length ?
|
|
1140
|
+
// The inset lives inside the row now, so a click on a value chip would
|
|
1141
|
+
// bubble to the row's onClick and fire activate() a SECOND time (the
|
|
1142
|
+
// chip's onItemClick already calls it) — toggling the editor shut again.
|
|
1143
|
+
// Stop the bubble here; the chip's own handler still opens the editor.
|
|
1144
|
+
<StyledRowInset
|
|
1145
|
+
className='options-readfirst-inset'
|
|
1146
|
+
onClick={(e) => e.stopPropagation()}
|
|
1147
|
+
>
|
|
1148
|
+
<ReqoreCollapsibleContent
|
|
1149
|
+
maxCollapsedHeight={96}
|
|
1150
|
+
buttonProps={{ className: 'options-readfirst-viewmore' }}
|
|
1151
|
+
>
|
|
1152
|
+
<div className='options-readfirst-structured'>
|
|
1153
|
+
<StructuredDataView
|
|
1154
|
+
value={optionField?.value}
|
|
1155
|
+
collapsibleRoot={false}
|
|
1156
|
+
showTypes={showFieldTypes}
|
|
1157
|
+
defaultExpandDepth={2}
|
|
1158
|
+
onItemClick={() => activate()}
|
|
1159
|
+
/>
|
|
1160
|
+
</div>
|
|
1161
|
+
</ReqoreCollapsibleContent>
|
|
1162
|
+
</StyledRowInset>
|
|
1163
|
+
: null}
|
|
1082
1164
|
</StyledRowValue>
|
|
1083
1165
|
<StyledRowActions>
|
|
1084
1166
|
{/* Column discipline (table treatment): variable-width chips lead and
|
|
@@ -1093,46 +1175,11 @@ export const CompactRow = memo(
|
|
|
1093
1175
|
the "One of"/"Covers" chip — keep only "Covered by <X>", the one fact
|
|
1094
1176
|
the rail can't show. Non-clustered members (split-across-panels or
|
|
1095
1177
|
narrow mode, where there's no rail) keep the full chip as a fallback. */}
|
|
1096
|
-
{!hidden && !fieldDisabled &&
|
|
1178
|
+
{!hidden && !fieldDisabled && !clustered && !coveredByLabel ? requiredGroupChip : null}
|
|
1097
1179
|
{!hidden ? dependsOnChip : null}
|
|
1098
1180
|
{draftChip}
|
|
1099
|
-
{
|
|
1100
|
-
|
|
1101
|
-
className='options-readfirst-revert'
|
|
1102
|
-
size='tiny'
|
|
1103
|
-
flat
|
|
1104
|
-
minimal
|
|
1105
|
-
compact
|
|
1106
|
-
icon='HistoryLine'
|
|
1107
|
-
tooltip='Revert changes'
|
|
1108
|
-
onClick={(e: React.MouseEvent) => {
|
|
1109
|
-
e.stopPropagation();
|
|
1110
|
-
handleValueChange(
|
|
1111
|
-
optionName,
|
|
1112
|
-
originalValue.current?.[optionName]?.value,
|
|
1113
|
-
originalValue.current?.[optionName]?.type
|
|
1114
|
-
);
|
|
1115
|
-
}}
|
|
1116
|
-
/>
|
|
1117
|
-
: null}
|
|
1118
|
-
{removable && !hidden ?
|
|
1119
|
-
<ReqoreButton
|
|
1120
|
-
className='readfirst-action'
|
|
1121
|
-
size='small'
|
|
1122
|
-
flat
|
|
1123
|
-
minimal
|
|
1124
|
-
intent='danger'
|
|
1125
|
-
icon='DeleteBinLine'
|
|
1126
|
-
tooltip='Remove field'
|
|
1127
|
-
onClick={(e: React.MouseEvent) => {
|
|
1128
|
-
e.stopPropagation();
|
|
1129
|
-
confirmAction({
|
|
1130
|
-
title: 'Remove field',
|
|
1131
|
-
onConfirm: () => removeSelectedOption(optionName),
|
|
1132
|
-
});
|
|
1133
|
-
}}
|
|
1134
|
-
/>
|
|
1135
|
-
: null}
|
|
1181
|
+
{/* Remove-field lives in the expanded editor's "More" (⋮) menu now — no
|
|
1182
|
+
standalone delete button on the read row. */}
|
|
1136
1183
|
{/* Lock/add slot BEFORE the info slot so the ⓘ keeps the same far-right
|
|
1137
1184
|
x on every row — a disabled field's lock sits to the ⓘ's left rather
|
|
1138
1185
|
than pushing it inward. ADD for a hidden field; a plain LOCK for a
|
|
@@ -1157,61 +1204,36 @@ export const CompactRow = memo(
|
|
|
1157
1204
|
{infoToggle}
|
|
1158
1205
|
</StyledActionSlot>
|
|
1159
1206
|
: null}
|
|
1207
|
+
{/* The revert affordance lives in the status-dot column (a changed field
|
|
1208
|
+
swaps its dot for the revert icon) so it sits at the same fixed x as
|
|
1209
|
+
the dot — out of the value's way instead of floating over it. */}
|
|
1210
|
+
<StyledActionSlot className='options-readfirst-statusdot-slot' $width={12}>
|
|
1211
|
+
{changed && !readOnly && !hidden ?
|
|
1212
|
+
<ReqoreButton
|
|
1213
|
+
className='options-readfirst-revert'
|
|
1214
|
+
size='tiny'
|
|
1215
|
+
flat
|
|
1216
|
+
minimal
|
|
1217
|
+
compact
|
|
1218
|
+
icon='HistoryLine'
|
|
1219
|
+
tooltip='Revert changes'
|
|
1220
|
+
onClick={(e: React.MouseEvent) => {
|
|
1221
|
+
e.stopPropagation();
|
|
1222
|
+
handleValueChange(
|
|
1223
|
+
optionName,
|
|
1224
|
+
originalValue.current?.[optionName]?.value,
|
|
1225
|
+
originalValue.current?.[optionName]?.type
|
|
1226
|
+
);
|
|
1227
|
+
}}
|
|
1228
|
+
/>
|
|
1229
|
+
: dotColor ?
|
|
1230
|
+
<StyledStatusDot $color={dotColor} $ring={dotStatus !== 'set'} aria-hidden />
|
|
1231
|
+
: null}
|
|
1232
|
+
</StyledActionSlot>
|
|
1160
1233
|
</StyledRowActions>
|
|
1161
1234
|
</div>
|
|
1162
1235
|
);
|
|
1163
1236
|
|
|
1164
|
-
if (hashEntries.length) {
|
|
1165
|
-
return (
|
|
1166
|
-
<StyledColumn
|
|
1167
|
-
key={optionName}
|
|
1168
|
-
data-field={optionName}
|
|
1169
|
-
className={`options-readfirst-hash-row${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
|
|
1170
|
-
style={stripeStyle}
|
|
1171
|
-
>
|
|
1172
|
-
{row}
|
|
1173
|
-
<StyledRowInset className='options-readfirst-inset'>
|
|
1174
|
-
<ReqoreCollapsibleContent
|
|
1175
|
-
maxCollapsedHeight={96}
|
|
1176
|
-
buttonProps={{ className: 'options-readfirst-viewmore' }}
|
|
1177
|
-
>
|
|
1178
|
-
{/* The IDE workflow-orders renderer (ReqoreDataView): a nested,
|
|
1179
|
-
type-coloured tree. Section summaries own their
|
|
1180
|
-
expand/collapse clicks, but clicking a VALUE chip opens the
|
|
1181
|
-
hash's editor. The Fields-menu "Show field types" toggle also
|
|
1182
|
-
drives the per-scalar type chips here. Depth 2 = root + first
|
|
1183
|
-
level open; deeper nests start collapsed so the preview stays
|
|
1184
|
-
short before the fade's "Show more". */}
|
|
1185
|
-
<div className='options-readfirst-structured'>
|
|
1186
|
-
<StructuredDataView
|
|
1187
|
-
value={optionField?.value}
|
|
1188
|
-
collapsibleRoot={false}
|
|
1189
|
-
showTypes={showFieldTypes}
|
|
1190
|
-
defaultExpandDepth={2}
|
|
1191
|
-
onItemClick={() => activate()}
|
|
1192
|
-
/>
|
|
1193
|
-
</div>
|
|
1194
|
-
</ReqoreCollapsibleContent>
|
|
1195
|
-
</StyledRowInset>
|
|
1196
|
-
{infoBlock}
|
|
1197
|
-
</StyledColumn>
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
if (infoBlock) {
|
|
1202
|
-
return (
|
|
1203
|
-
<StyledColumn
|
|
1204
|
-
key={optionName}
|
|
1205
|
-
data-field={optionName}
|
|
1206
|
-
className={`options-readfirst-info-row${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
|
|
1207
|
-
style={stripeStyle}
|
|
1208
|
-
>
|
|
1209
|
-
{row}
|
|
1210
|
-
{infoBlock}
|
|
1211
|
-
</StyledColumn>
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
1237
|
return row;
|
|
1216
1238
|
}
|
|
1217
1239
|
);
|