@qoretechnologies/reqraft 0.10.2 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +5 -0
- 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 +158 -101
- 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 +122 -105
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +9 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +272 -82
- 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 +76 -49
- 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 +5 -2
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +273 -258
- package/src/components/form/engine/CompactToolbar.tsx +112 -85
- package/src/components/form/engine/FormEngine.stories.tsx +239 -115
- package/src/components/form/engine/FormEngine.tsx +332 -83
- package/src/components/form/engine/compactRowStyles.ts +221 -144
- 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.stories.tsx +9 -2
- package/src/components/form/fields/auto/AutoFormField.tsx +8 -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,28 +148,16 @@ 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);
|
|
166
158
|
const availableOptions = useContextSelector(CompactRowContext, (v) => v.availableOptions);
|
|
167
159
|
const requiredGroupsInfo = useContextSelector(CompactRowContext, (v) => v.requiredGroupsInfo);
|
|
168
160
|
const handleValueChange = useContextSelector(CompactRowContext, (v) => v.handleValueChange);
|
|
169
|
-
const handleAddOptionalFieldChange = useContextSelector(
|
|
170
|
-
CompactRowContext,
|
|
171
|
-
(v) => v.handleAddOptionalFieldChange
|
|
172
|
-
);
|
|
173
161
|
const toggleExpandedOption = useContextSelector(
|
|
174
162
|
CompactRowContext,
|
|
175
163
|
(v) => v.toggleExpandedOption
|
|
@@ -190,14 +178,12 @@ export const CompactRow = memo(
|
|
|
190
178
|
const theme = useContextSelector(CompactRowContext, (v) => v.theme);
|
|
191
179
|
const cMuted = useContextSelector(CompactRowContext, (v) => v.cMuted);
|
|
192
180
|
const templates = useContextSelector(CompactRowContext, (v) => v.templates);
|
|
193
|
-
const cFaint = useContextSelector(CompactRowContext, (v) => v.cFaint);
|
|
194
181
|
const cKey = useContextSelector(CompactRowContext, (v) => v.cKey);
|
|
195
182
|
const cDivider = useContextSelector(CompactRowContext, (v) => v.cDivider);
|
|
196
183
|
const cHover = useContextSelector(CompactRowContext, (v) => v.cHover);
|
|
197
184
|
const cDanger = useContextSelector(CompactRowContext, (v) => v.cDanger);
|
|
198
185
|
const cWarning = useContextSelector(CompactRowContext, (v) => v.cWarning);
|
|
199
186
|
const cInfo = useContextSelector(CompactRowContext, (v) => v.cInfo);
|
|
200
|
-
const cBg = useContextSelector(CompactRowContext, (v) => v.cBg);
|
|
201
187
|
|
|
202
188
|
// Value-cell content: colour adds a swatch, file an icon + size; hash keeps
|
|
203
189
|
// its "N fields" summary (sub-fields reveal beneath the row).
|
|
@@ -420,6 +406,21 @@ export const CompactRow = memo(
|
|
|
420
406
|
// A choice with per-option logos (e.g. language) reads better collapsed.
|
|
421
407
|
!optionHasImages(schema) &&
|
|
422
408
|
!COMPACT_COMPLEX_TYPES.has(editType);
|
|
409
|
+
|
|
410
|
+
// Auto-focus the editor's first input when a field is opened, so you can type
|
|
411
|
+
// straight away (matches the prototype's tap-to-edit feel).
|
|
412
|
+
const editorRef = React.useRef<HTMLDivElement>(null);
|
|
413
|
+
React.useEffect(() => {
|
|
414
|
+
if (!isExpanded) return undefined;
|
|
415
|
+
const id = window.setTimeout(() => {
|
|
416
|
+
const el = editorRef.current?.querySelector<HTMLElement>(
|
|
417
|
+
'input:not([type="hidden"]):not([disabled]), textarea, [contenteditable="true"]'
|
|
418
|
+
);
|
|
419
|
+
el?.focus();
|
|
420
|
+
}, 60);
|
|
421
|
+
return () => window.clearTimeout(id);
|
|
422
|
+
}, [isExpanded]);
|
|
423
|
+
|
|
423
424
|
const revertButton =
|
|
424
425
|
changed ?
|
|
425
426
|
<ReqoreButton
|
|
@@ -516,10 +517,13 @@ export const CompactRow = memo(
|
|
|
516
517
|
.filter((m) => m.label !== 'This field is required')
|
|
517
518
|
.map((m) => ({ intent: m.intent as string, content: String(m.label) }))
|
|
518
519
|
: [];
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
520
|
+
// TWO display channels, matching the Focus prototype:
|
|
521
|
+
// • dedicated schema `messages` → prominent coloured PANELS below the row;
|
|
522
|
+
// • everything else (validation reasons, the unmet-dependency hint, the
|
|
523
|
+
// default-value note) → a compact INLINE reason strip — not a panel.
|
|
524
|
+
const panelMessages = schemaMessages;
|
|
525
|
+
const inlineMessages: TInfoMsg[] = [
|
|
526
|
+
...fieldMessages,
|
|
523
527
|
...(infoActive && schema?.default_value_desc ?
|
|
524
528
|
[
|
|
525
529
|
{
|
|
@@ -529,26 +533,20 @@ export const CompactRow = memo(
|
|
|
529
533
|
]
|
|
530
534
|
: []),
|
|
531
535
|
];
|
|
536
|
+
const allMsgs = [...panelMessages, ...inlineMessages];
|
|
532
537
|
const worstIntent =
|
|
533
|
-
|
|
534
|
-
:
|
|
538
|
+
allMsgs.some((m) => m.intent === 'danger') ? 'danger'
|
|
539
|
+
: allMsgs.some((m) => m.intent === 'warning') ? 'warning'
|
|
535
540
|
: undefined;
|
|
536
541
|
const intentColor =
|
|
537
542
|
worstIntent === 'danger' ? cDanger
|
|
538
543
|
: worstIntent === 'warning' ? cWarning
|
|
539
544
|
: undefined;
|
|
540
545
|
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
546
|
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);
|
|
547
|
+
// The per-row ⓘ is gone: short_desc shows under the name only when the global
|
|
548
|
+
// "descriptions" toggle is engaged (and inside the editor when expanded).
|
|
549
|
+
const showLabelDesc = !!labelShortDesc && showAllDescriptions === true;
|
|
552
550
|
|
|
553
551
|
const renderInfoStrip = (m: TInfoMsg, index: number) => (
|
|
554
552
|
<ReqoreMessage
|
|
@@ -562,33 +560,14 @@ export const CompactRow = memo(
|
|
|
562
560
|
{m.content}
|
|
563
561
|
</ReqoreMessage>
|
|
564
562
|
);
|
|
563
|
+
const reasonColor = (intent?: string) =>
|
|
564
|
+
intent === 'danger' ? cDanger
|
|
565
|
+
: intent === 'warning' ? cWarning
|
|
566
|
+
: intent === 'success' ?
|
|
567
|
+
(theme?.intents as Record<string, string> | undefined)?.success || cInfo
|
|
568
|
+
: `${cMuted}cc`;
|
|
565
569
|
|
|
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;
|
|
570
|
+
const infoToggle = null;
|
|
592
571
|
|
|
593
572
|
// Cluster (required-group connection) — shared by the read row, its block
|
|
594
573
|
// wrapper AND the inline editor, so the rail/node/highlight persist across
|
|
@@ -604,20 +583,9 @@ export const CompactRow = memo(
|
|
|
604
583
|
clustered ?
|
|
605
584
|
`readfirst-cluster-rail${clusterFirst ? ' readfirst-cluster-first' : ''}${clusterLast ? ' readfirst-cluster-last' : ''}${clusterSatisfied ? ' readfirst-cluster-satisfied' : ''}`
|
|
606
585
|
: '';
|
|
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;
|
|
586
|
+
// The connection rail + status nodes are gone — the "One of the below is
|
|
587
|
+
// required" box (and the Covers / Covered-by chips) carry the grouping now.
|
|
588
|
+
const clusterNode = null;
|
|
621
589
|
const clusterHoverProps =
|
|
622
590
|
clustered && clusterMembers.length ?
|
|
623
591
|
{
|
|
@@ -626,6 +594,42 @@ export const CompactRow = memo(
|
|
|
626
594
|
}
|
|
627
595
|
: {};
|
|
628
596
|
|
|
597
|
+
// Secondary edit actions tuck into a "More" (⋮) menu so the card header stays
|
|
598
|
+
// calm: Fullscreen always, plus Remove field for a removable option. Rendered
|
|
599
|
+
// just before the Done ✓ in both expanded layouts.
|
|
600
|
+
const moreMenu = (
|
|
601
|
+
<ReqoreDropdown
|
|
602
|
+
className='options-readfirst-more'
|
|
603
|
+
icon='More2Fill'
|
|
604
|
+
flat
|
|
605
|
+
minimal
|
|
606
|
+
fixed
|
|
607
|
+
size='small'
|
|
608
|
+
tooltip='More actions'
|
|
609
|
+
items={[
|
|
610
|
+
{
|
|
611
|
+
label: 'Edit fullscreen',
|
|
612
|
+
icon: 'FullscreenLine',
|
|
613
|
+
onClick: () => setFocusedEditing(optionName),
|
|
614
|
+
} as IReqoreDropdownItem,
|
|
615
|
+
...(removable && !hidden ?
|
|
616
|
+
[
|
|
617
|
+
{
|
|
618
|
+
label: 'Remove field',
|
|
619
|
+
icon: 'DeleteBinLine',
|
|
620
|
+
intent: 'danger',
|
|
621
|
+
onClick: () =>
|
|
622
|
+
confirmAction({
|
|
623
|
+
title: 'Remove field',
|
|
624
|
+
onConfirm: () => removeSelectedOption(optionName),
|
|
625
|
+
}),
|
|
626
|
+
} as IReqoreDropdownItem,
|
|
627
|
+
]
|
|
628
|
+
: []),
|
|
629
|
+
]}
|
|
630
|
+
/>
|
|
631
|
+
);
|
|
632
|
+
|
|
629
633
|
if (isExpanded) {
|
|
630
634
|
if (inlineEditable) {
|
|
631
635
|
const collapse = () => toggleExpandedOption(optionName);
|
|
@@ -641,25 +645,40 @@ export const CompactRow = memo(
|
|
|
641
645
|
}
|
|
642
646
|
{...clusterHoverProps}
|
|
643
647
|
>
|
|
644
|
-
<
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
event.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
648
|
+
<StyledLabelBlock>
|
|
649
|
+
<StyledRowLabel
|
|
650
|
+
role='button'
|
|
651
|
+
tabIndex={0}
|
|
652
|
+
aria-label={`Collapse ${label}`}
|
|
653
|
+
title={schema?.short_desc || undefined}
|
|
654
|
+
$color={cKey}
|
|
655
|
+
$pointer
|
|
656
|
+
onClick={collapse}
|
|
657
|
+
onKeyDown={(event) => {
|
|
658
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
659
|
+
event.preventDefault();
|
|
660
|
+
collapse();
|
|
661
|
+
}
|
|
662
|
+
}}
|
|
663
|
+
>
|
|
664
|
+
{label}
|
|
665
|
+
{required ? <ReqoreIcon icon='Asterisk' color='danger' size='10px' /> : null}
|
|
666
|
+
</StyledRowLabel>
|
|
667
|
+
{/* Keep the short_desc visible while editing inline when the global
|
|
668
|
+
descriptions toggle is on — read rows show it, so opening a field
|
|
669
|
+
shouldn't make it vanish. */}
|
|
670
|
+
{showLabelDesc ?
|
|
671
|
+
<StyledLabelDesc
|
|
672
|
+
className='options-readfirst-label-desc'
|
|
673
|
+
size='small'
|
|
674
|
+
effect={{ opacity: 0.55 }}
|
|
675
|
+
>
|
|
676
|
+
{labelShortDesc}
|
|
677
|
+
</StyledLabelDesc>
|
|
678
|
+
: null}
|
|
679
|
+
</StyledLabelBlock>
|
|
662
680
|
<div
|
|
681
|
+
ref={editorRef}
|
|
663
682
|
style={{ minWidth: 0 }}
|
|
664
683
|
onKeyDown={(event) => {
|
|
665
684
|
if (event.key === 'Escape') {
|
|
@@ -678,12 +697,12 @@ export const CompactRow = memo(
|
|
|
678
697
|
message strip is visible. */}
|
|
679
698
|
{revertButton}
|
|
680
699
|
{clearValueButton}
|
|
700
|
+
{moreMenu}
|
|
681
701
|
<ReqoreButton
|
|
682
702
|
className='options-readfirst-done'
|
|
683
703
|
size='small'
|
|
684
|
-
flat
|
|
685
|
-
minimal
|
|
686
704
|
intent='success'
|
|
705
|
+
fixed
|
|
687
706
|
icon='CheckLine'
|
|
688
707
|
tooltip='Done'
|
|
689
708
|
onClick={collapse}
|
|
@@ -749,7 +768,7 @@ export const CompactRow = memo(
|
|
|
749
768
|
}}
|
|
750
769
|
>
|
|
751
770
|
<StyledCardHeading>
|
|
752
|
-
<StyledCardLabel $color={
|
|
771
|
+
<StyledCardLabel $color={cKey}>
|
|
753
772
|
{(schema as { icon?: string } | undefined)?.icon || (schema as { image?: string } | undefined)?.image ?
|
|
754
773
|
<ReqoreIcon
|
|
755
774
|
icon={(schema as { icon?: string } | undefined)?.icon as never}
|
|
@@ -811,18 +830,8 @@ export const CompactRow = memo(
|
|
|
811
830
|
</ReqoreButton>
|
|
812
831
|
);
|
|
813
832
|
})}
|
|
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). */}
|
|
833
|
+
{/* Clear-value sits before the More menu — the card analog of the
|
|
834
|
+
inline row's Clear. Empties the value (keeps the field). */}
|
|
826
835
|
{hasValue && !readOnly ?
|
|
827
836
|
<ReqoreButton
|
|
828
837
|
size='small'
|
|
@@ -835,16 +844,16 @@ export const CompactRow = memo(
|
|
|
835
844
|
onClick={() => handleValueChange(optionName, undefined)}
|
|
836
845
|
/>
|
|
837
846
|
: null}
|
|
847
|
+
{moreMenu}
|
|
838
848
|
<ReqoreButton
|
|
839
849
|
size='small'
|
|
840
|
-
icon='CheckLine'
|
|
850
|
+
icon={readOnly ? 'CloseLine' : 'CheckLine'}
|
|
841
851
|
intent='success'
|
|
842
852
|
fixed
|
|
843
853
|
className='options-readfirst-done'
|
|
854
|
+
tooltip={readOnly ? 'Close' : 'Done'}
|
|
844
855
|
onClick={() => toggleExpandedOption(optionName)}
|
|
845
|
-
|
|
846
|
-
{readOnly ? 'Close' : 'Done'}
|
|
847
|
-
</ReqoreButton>
|
|
856
|
+
/>
|
|
848
857
|
</ReqoreControlGroup>
|
|
849
858
|
</div>
|
|
850
859
|
{/* Same fullscreen focused-editing affordance as the classic cards —
|
|
@@ -869,6 +878,15 @@ export const CompactRow = memo(
|
|
|
869
878
|
|
|
870
879
|
const formatted = formatOptionValue(optionField, schema);
|
|
871
880
|
const empty = formatted === '';
|
|
881
|
+
// Inline reasons shown on the value line: validation / dependency / one-of /
|
|
882
|
+
// default-value hints, plus a read-only covered-by note (editable covered rows
|
|
883
|
+
// carry that in their chip instead).
|
|
884
|
+
const valueReasons: TInfoMsg[] = [
|
|
885
|
+
...inlineMessages,
|
|
886
|
+
...(empty && coveredByLabel ?
|
|
887
|
+
[{ content: `Covered by “${coveredByLabel}”`, intent: 'success' }]
|
|
888
|
+
: []),
|
|
889
|
+
];
|
|
872
890
|
// A hash row reveals its sub-fields as read-only sub-rows under a "view
|
|
873
891
|
// more" disclosure; the row itself still expands the real editor on click.
|
|
874
892
|
const valueType = getValueType(optionField, schema);
|
|
@@ -936,8 +954,7 @@ export const CompactRow = memo(
|
|
|
936
954
|
flat
|
|
937
955
|
compact
|
|
938
956
|
icon='LockLine'
|
|
939
|
-
|
|
940
|
-
effect={{uppercase: true, spaced: 1}}
|
|
957
|
+
tooltip='Disabled — depends on other fields. Click to locate them.'
|
|
941
958
|
items={[
|
|
942
959
|
{ divider: true, label: 'Unlocked by:', dividerAlign: 'left' } as IReqoreDropdownItem,
|
|
943
960
|
...dependencyEntries.flatMap((entry): IReqoreDropdownItem[] => [
|
|
@@ -972,9 +989,10 @@ export const CompactRow = memo(
|
|
|
972
989
|
if (target?.classList?.contains('readfirst-row')) {
|
|
973
990
|
readRowHeights.current[optionName] = Math.round(target.getBoundingClientRect().height);
|
|
974
991
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
992
|
+
// Optional fields aren't "added" — every one is editable in place. Opening a
|
|
993
|
+
// not-yet-set field just expands its editor (with an empty value); it joins
|
|
994
|
+
// the form's output the moment a value is set (and moves to Set / Needs
|
|
995
|
+
// attention), and drops back to Optional when cleared. No explicit add step.
|
|
978
996
|
toggleExpandedOption(optionName);
|
|
979
997
|
};
|
|
980
998
|
|
|
@@ -1002,18 +1020,36 @@ export const CompactRow = memo(
|
|
|
1002
1020
|
rowStripeColor ?
|
|
1003
1021
|
({ ['--readfirst-stripe']: rowStripeColor } as React.CSSProperties)
|
|
1004
1022
|
: undefined;
|
|
1005
|
-
const blockWrapped = hashEntries.length > 0 || !!infoBlock;
|
|
1006
1023
|
|
|
1024
|
+
// "Focus" trailing status dot, derived from the SHARED status helper (the
|
|
1025
|
+
// same one FormEngine uses to bucket rows into the Needs-attention/Set/Optional
|
|
1026
|
+
// boxes — so the dot and the box can never disagree). Hidden (not-yet-added)
|
|
1027
|
+
// and dependency-locked rows show no dot (the add/lock affordance speaks for
|
|
1028
|
+
// them). worstIntent already folds in schema + validation messages.
|
|
1029
|
+
const cSuccess = (theme?.intents as Record<string, string> | undefined)?.success;
|
|
1030
|
+
const rowStatus = getReadFirstStatus({
|
|
1031
|
+
empty,
|
|
1032
|
+
required,
|
|
1033
|
+
covered: !!coveredByLabel || groupResolved,
|
|
1034
|
+
invalid: worstIntent === 'danger',
|
|
1035
|
+
warned: worstIntent === 'warning',
|
|
1036
|
+
});
|
|
1037
|
+
const dotStatus = hidden || fieldDisabled ? undefined : rowStatus;
|
|
1038
|
+
const dotColor =
|
|
1039
|
+
dotStatus === 'invalid' ? cDanger
|
|
1040
|
+
: dotStatus === 'todo' ? cWarning
|
|
1041
|
+
: dotStatus === 'set' ? cSuccess || cInfo
|
|
1042
|
+
: undefined;
|
|
1007
1043
|
const row = (
|
|
1008
1044
|
<div
|
|
1009
1045
|
key={optionName}
|
|
1010
1046
|
data-field={optionName}
|
|
1011
1047
|
role='button'
|
|
1012
1048
|
tabIndex={0}
|
|
1013
|
-
aria-label={
|
|
1014
|
-
className={`readfirst-row options-readfirst-value${hidden ? ' readfirst-row-hidden' : ''}${fieldDisabled ? ' readfirst-row-disabled' : ''}${isHighlighted ? ' readfirst-row-group-highlight' : ''}${isFlashed ? ' readfirst-row-flash' : ''}${
|
|
1049
|
+
aria-label={label}
|
|
1050
|
+
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' : ''}${panelMessages.length || hashEntries.length ? ' readfirst-row-tall' : ''}${clusterBlockClass ? ' ' + clusterBlockClass : ''}`}
|
|
1015
1051
|
aria-disabled={fieldDisabled || undefined}
|
|
1016
|
-
style={
|
|
1052
|
+
style={stripeStyle}
|
|
1017
1053
|
onClick={activate}
|
|
1018
1054
|
onKeyDown={(event) => {
|
|
1019
1055
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -1027,31 +1063,37 @@ export const CompactRow = memo(
|
|
|
1027
1063
|
<StyledLabelBlock>
|
|
1028
1064
|
<StyledRowLabel title={schema?.short_desc || undefined} $color={cKey}>
|
|
1029
1065
|
{rowChromeIcon}
|
|
1030
|
-
{label
|
|
1031
|
-
|
|
1066
|
+
{/* label + asterisk + help flow as ONE inline run, so the asterisk
|
|
1067
|
+
stays right after the last word even when the name wraps. */}
|
|
1068
|
+
<span className='options-readfirst-label-text' style={{ minWidth: 0 }}>
|
|
1069
|
+
{label}
|
|
1070
|
+
{required ?
|
|
1071
|
+
<ReqoreIcon icon='Asterisk' color='danger' size='10px' margin='left' marginSize='tiny' />
|
|
1072
|
+
: null}
|
|
1073
|
+
{schema?.desc ?
|
|
1074
|
+
<ReqoreIcon
|
|
1075
|
+
icon='QuestionLine'
|
|
1076
|
+
size='12px'
|
|
1077
|
+
effect={{ opacity: 0.55 }}
|
|
1078
|
+
margin='left'
|
|
1079
|
+
marginSize='tiny'
|
|
1080
|
+
role='button'
|
|
1081
|
+
tabIndex={-1}
|
|
1082
|
+
aria-label='Help'
|
|
1083
|
+
className='options-readfirst-help'
|
|
1084
|
+
style={{ cursor: 'help' }}
|
|
1085
|
+
onClick={(event) => {
|
|
1086
|
+
event.stopPropagation();
|
|
1087
|
+
handleOptionLabelClick(optionName);
|
|
1088
|
+
}}
|
|
1089
|
+
/>
|
|
1090
|
+
: null}
|
|
1091
|
+
</span>
|
|
1032
1092
|
{typeLabel ?
|
|
1033
1093
|
<ReqoreTag size='tiny' minimal label={typeLabel} labelEffect={{ opacity: 0.55 }} />
|
|
1034
1094
|
: 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
1095
|
</StyledRowLabel>
|
|
1054
|
-
{
|
|
1096
|
+
{showLabelDesc ?
|
|
1055
1097
|
<StyledLabelDesc
|
|
1056
1098
|
className='options-readfirst-label-desc'
|
|
1057
1099
|
size='small'
|
|
@@ -1063,22 +1105,59 @@ export const CompactRow = memo(
|
|
|
1063
1105
|
</StyledLabelBlock>
|
|
1064
1106
|
<StyledRowValue
|
|
1065
1107
|
title={!empty && !hidden && typeof formatted === 'string' ? formatted : undefined}
|
|
1066
|
-
|
|
1108
|
+
// A SET value reads at full key brightness so it stands out as the
|
|
1109
|
+
// actual data — crucial when stacked under a (muted) description on
|
|
1110
|
+
// mobile, where a dim value blends into the prose. The bold name still
|
|
1111
|
+
// outranks it; the empty dash stays faint.
|
|
1112
|
+
$color={empty || hidden ? `${cMuted}66` : cKey}
|
|
1067
1113
|
$empty={empty || hidden}
|
|
1068
1114
|
>
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1115
|
+
<span className='options-readfirst-valuetext'>
|
|
1116
|
+
{hidden || empty ? '—' : renderReadFirstValue(optionField, schema, formatted)}
|
|
1117
|
+
</span>
|
|
1118
|
+
{valueReasons.map((m, i) => (
|
|
1119
|
+
<span
|
|
1120
|
+
key={`${m.content}-${i}`}
|
|
1121
|
+
className='options-readfirst-reason'
|
|
1122
|
+
style={{ color: reasonColor(m.intent) }}
|
|
1123
|
+
>
|
|
1124
|
+
{m.content}
|
|
1125
|
+
</span>
|
|
1126
|
+
))}
|
|
1127
|
+
{/* Dedicated schema message PANELS render right here, directly under the
|
|
1128
|
+
value (full width of the value column) — never pushed down by the
|
|
1129
|
+
label's short_desc. */}
|
|
1130
|
+
{panelMessages.length ?
|
|
1131
|
+
<div className='options-readfirst-info-panel'>{panelMessages.map(renderInfoStrip)}</div>
|
|
1132
|
+
: null}
|
|
1133
|
+
{/* The structured hash/list preview also lives in the value cell, so it
|
|
1134
|
+
starts directly under the value summary ("N fields"), not below the
|
|
1135
|
+
label's short_desc. */}
|
|
1136
|
+
{hashEntries.length ?
|
|
1137
|
+
// The inset lives inside the row now, so a click on a value chip would
|
|
1138
|
+
// bubble to the row's onClick and fire activate() a SECOND time (the
|
|
1139
|
+
// chip's onItemClick already calls it) — toggling the editor shut again.
|
|
1140
|
+
// Stop the bubble here; the chip's own handler still opens the editor.
|
|
1141
|
+
<StyledRowInset
|
|
1142
|
+
className='options-readfirst-inset'
|
|
1143
|
+
onClick={(e) => e.stopPropagation()}
|
|
1144
|
+
>
|
|
1145
|
+
<ReqoreCollapsibleContent
|
|
1146
|
+
maxCollapsedHeight={96}
|
|
1147
|
+
buttonProps={{ className: 'options-readfirst-viewmore' }}
|
|
1148
|
+
>
|
|
1149
|
+
<div className='options-readfirst-structured'>
|
|
1150
|
+
<StructuredDataView
|
|
1151
|
+
value={optionField?.value}
|
|
1152
|
+
collapsibleRoot={false}
|
|
1153
|
+
showTypes={showFieldTypes}
|
|
1154
|
+
defaultExpandDepth={2}
|
|
1155
|
+
onItemClick={() => activate()}
|
|
1156
|
+
/>
|
|
1157
|
+
</div>
|
|
1158
|
+
</ReqoreCollapsibleContent>
|
|
1159
|
+
</StyledRowInset>
|
|
1160
|
+
: null}
|
|
1082
1161
|
</StyledRowValue>
|
|
1083
1162
|
<StyledRowActions>
|
|
1084
1163
|
{/* Column discipline (table treatment): variable-width chips lead and
|
|
@@ -1093,63 +1172,24 @@ export const CompactRow = memo(
|
|
|
1093
1172
|
the "One of"/"Covers" chip — keep only "Covered by <X>", the one fact
|
|
1094
1173
|
the rail can't show. Non-clustered members (split-across-panels or
|
|
1095
1174
|
narrow mode, where there's no rail) keep the full chip as a fallback. */}
|
|
1096
|
-
{!hidden && !fieldDisabled &&
|
|
1175
|
+
{!hidden && !fieldDisabled && !clustered && !coveredByLabel ? requiredGroupChip : null}
|
|
1097
1176
|
{!hidden ? dependsOnChip : null}
|
|
1098
1177
|
{draftChip}
|
|
1099
|
-
{
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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}
|
|
1136
|
-
{/* Lock/add slot BEFORE the info slot so the ⓘ keeps the same far-right
|
|
1137
|
-
x on every row — a disabled field's lock sits to the ⓘ's left rather
|
|
1138
|
-
than pushing it inward. ADD for a hidden field; a plain LOCK for a
|
|
1139
|
-
field disabled for a non-dependency reason. Dependency-locked fields
|
|
1140
|
-
show the "Depends on" chip in the chips area instead; the whole row is
|
|
1141
|
-
click-to-edit, so there's no hover edit pencil. */}
|
|
1142
|
-
{hidden || (fieldDisabled && !dependencyEntries.length) ?
|
|
1178
|
+
{/* Remove-field lives in the expanded editor's "More" (⋮) menu now — no
|
|
1179
|
+
standalone delete button on the read row. */}
|
|
1180
|
+
{/* Lock slot BEFORE the info slot so the ⓘ keeps the same far-right x on
|
|
1181
|
+
every row — a disabled field's lock sits to the ⓘ's left rather than
|
|
1182
|
+
pushing it inward. (Not-yet-set optional fields show NO marker now —
|
|
1183
|
+
they're just editable in place, like every other row. Dependency-
|
|
1184
|
+
locked fields show the "Depends on" chip in the chips area instead.) */}
|
|
1185
|
+
{fieldDisabled && !dependencyEntries.length ?
|
|
1143
1186
|
<StyledActionSlot className='options-readfirst-trailing-slot' $width={18}>
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
<ReqoreIcon className='options-readfirst-locked' icon='LockLine' size='14px' />
|
|
1151
|
-
</span>
|
|
1152
|
-
}
|
|
1187
|
+
<span
|
|
1188
|
+
title={fieldDisabledReason}
|
|
1189
|
+
style={{ display: 'inline-flex', opacity: 0.45 }}
|
|
1190
|
+
>
|
|
1191
|
+
<ReqoreIcon className='options-readfirst-locked' icon='LockLine' size='14px' />
|
|
1192
|
+
</span>
|
|
1153
1193
|
</StyledActionSlot>
|
|
1154
1194
|
: null}
|
|
1155
1195
|
{infoToggle ?
|
|
@@ -1157,61 +1197,36 @@ export const CompactRow = memo(
|
|
|
1157
1197
|
{infoToggle}
|
|
1158
1198
|
</StyledActionSlot>
|
|
1159
1199
|
: null}
|
|
1200
|
+
{/* The revert affordance lives in the status-dot column (a changed field
|
|
1201
|
+
swaps its dot for the revert icon) so it sits at the same fixed x as
|
|
1202
|
+
the dot — out of the value's way instead of floating over it. */}
|
|
1203
|
+
<StyledActionSlot className='options-readfirst-statusdot-slot' $width={12}>
|
|
1204
|
+
{changed && !readOnly && !hidden ?
|
|
1205
|
+
<ReqoreButton
|
|
1206
|
+
className='options-readfirst-revert'
|
|
1207
|
+
size='tiny'
|
|
1208
|
+
flat
|
|
1209
|
+
minimal
|
|
1210
|
+
compact
|
|
1211
|
+
icon='HistoryLine'
|
|
1212
|
+
tooltip='Revert changes'
|
|
1213
|
+
onClick={(e: React.MouseEvent) => {
|
|
1214
|
+
e.stopPropagation();
|
|
1215
|
+
handleValueChange(
|
|
1216
|
+
optionName,
|
|
1217
|
+
originalValue.current?.[optionName]?.value,
|
|
1218
|
+
originalValue.current?.[optionName]?.type
|
|
1219
|
+
);
|
|
1220
|
+
}}
|
|
1221
|
+
/>
|
|
1222
|
+
: dotColor ?
|
|
1223
|
+
<StyledStatusDot $color={dotColor} $ring={dotStatus !== 'set'} aria-hidden />
|
|
1224
|
+
: null}
|
|
1225
|
+
</StyledActionSlot>
|
|
1160
1226
|
</StyledRowActions>
|
|
1161
1227
|
</div>
|
|
1162
1228
|
);
|
|
1163
1229
|
|
|
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
1230
|
return row;
|
|
1216
1231
|
}
|
|
1217
1232
|
);
|