@qoretechnologies/reqraft 0.10.4 → 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/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +7 -9
- 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 +8 -27
- 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 +92 -38
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +8 -3
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +4 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +18 -25
- package/src/components/form/engine/CompactToolbar.tsx +4 -17
- package/src/components/form/engine/FormEngine.stories.tsx +117 -10
- package/src/components/form/engine/FormEngine.tsx +84 -16
- package/src/components/form/engine/compactRowStyles.ts +33 -29
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +9 -2
- package/src/components/form/fields/auto/AutoFormField.tsx +3 -0
|
@@ -475,6 +475,14 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
475
475
|
* form should line up with the section description above it. Default `false`.
|
|
476
476
|
*/
|
|
477
477
|
compactFlush?: boolean;
|
|
478
|
+
/**
|
|
479
|
+
* Compact mode only: this form is an EMBEDDED sub-form (e.g. an arg_schema
|
|
480
|
+
* field's nested form) rather than the top-level scroller. It doesn't own a
|
|
481
|
+
* scroll context, so the toolbar isn't sticky and its header drops the dark
|
|
482
|
+
* blurred backdrop (and the stacking context that goes with it) — it sits
|
|
483
|
+
* transparently inside the parent's edit card. Default `false`.
|
|
484
|
+
*/
|
|
485
|
+
compactNested?: boolean;
|
|
478
486
|
/** Compact mode only: per-group display metadata (label / icon / subtitle /
|
|
479
487
|
* order) — the server only sends the bare group key. */
|
|
480
488
|
groups?: Record<string, IFormEngineGroup>;
|
|
@@ -531,6 +539,7 @@ export const FormEngine = ({
|
|
|
531
539
|
showTypeToggle = true,
|
|
532
540
|
compact,
|
|
533
541
|
compactFlush = false,
|
|
542
|
+
compactNested = false,
|
|
534
543
|
commitMode = 'immediate',
|
|
535
544
|
expandMode = 'single',
|
|
536
545
|
onCommit,
|
|
@@ -624,9 +633,15 @@ export const FormEngine = ({
|
|
|
624
633
|
const flashTimeout = useRef<ReturnType<typeof setTimeout>>();
|
|
625
634
|
const flashOptions = useCallback((optionNames: string[], scrollToFirst = false) => {
|
|
626
635
|
if (scrollToFirst && optionNames[0]) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
636
|
+
// Defer to the next frame: when this fires for a field that just changed
|
|
637
|
+
// panels, its row has only just re-mounted in the new box — scrolling in the
|
|
638
|
+
// same tick targets the stale (pre-move) layout, so the page doesn't budge.
|
|
639
|
+
// A rAF lets the new position settle first.
|
|
640
|
+
requestAnimationFrame(() => {
|
|
641
|
+
document
|
|
642
|
+
.querySelector(`.readfirst-row[data-field="${optionNames[0]}"]`)
|
|
643
|
+
?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
644
|
+
});
|
|
630
645
|
}
|
|
631
646
|
setFlashedOptions(optionNames);
|
|
632
647
|
clearTimeout(flashTimeout.current);
|
|
@@ -637,6 +652,28 @@ export const FormEngine = ({
|
|
|
637
652
|
[flashOptions]
|
|
638
653
|
);
|
|
639
654
|
useEffect(() => () => clearTimeout(flashTimeout.current), []);
|
|
655
|
+
|
|
656
|
+
// Follow a field across panels: when its status bucket changes — e.g. you fill
|
|
657
|
+
// an optional field and it jumps to Set / Needs attention — scroll to its new
|
|
658
|
+
// row and flash it so it's easy to keep track of. `settledBucket` holds each
|
|
659
|
+
// field's current panel (frozen while the field is being edited, it re-buckets
|
|
660
|
+
// on collapse), so diffing it after every render catches the move the instant it
|
|
661
|
+
// lands in the new panel. Runs every render; the diff is cheap and only fires a
|
|
662
|
+
// scroll on an ACTUAL move of a non-expanded field.
|
|
663
|
+
const prevSettledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
if (!compact) return;
|
|
666
|
+
const cur = settledBucket.current;
|
|
667
|
+
const prev = prevSettledBucket.current;
|
|
668
|
+
const moved = Object.keys(cur).find(
|
|
669
|
+
(name) => prev[name] && prev[name] !== cur[name] && !expandedOptions.includes(name)
|
|
670
|
+
);
|
|
671
|
+
prevSettledBucket.current = { ...cur };
|
|
672
|
+
if (moved) {
|
|
673
|
+
flashOptions([moved], true);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
640
677
|
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
641
678
|
// Info panels auto-open on Tier-1 content; the per-row user override sticks.
|
|
642
679
|
const [infoPanelOverrides, setInfoPanelOverrides] = useState<Record<string, boolean>>({});
|
|
@@ -1038,6 +1075,10 @@ export const FormEngine = ({
|
|
|
1038
1075
|
meta: undefined,
|
|
1039
1076
|
};
|
|
1040
1077
|
});
|
|
1078
|
+
// Collapse it too: a removed field drops back to the (collapsed) Optional box
|
|
1079
|
+
// as a quiet addable row — if it was being edited, that editor must close
|
|
1080
|
+
// rather than linger as an open editor for a field that's no longer added.
|
|
1081
|
+
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1041
1082
|
}, []);
|
|
1042
1083
|
|
|
1043
1084
|
const handleAddOptionalFieldChange = useCallback(
|
|
@@ -1908,15 +1949,18 @@ export const FormEngine = ({
|
|
|
1908
1949
|
pushRow(optionName, false);
|
|
1909
1950
|
}
|
|
1910
1951
|
});
|
|
1911
|
-
//
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1952
|
+
// Surface EVERY not-yet-added optional field as an addable (hidden) row, so
|
|
1953
|
+
// the whole schema is browsable inline — they all land in the Optional box
|
|
1954
|
+
// (hidden ⇒ 'optional' bucket) instead of being buried in the Fields menu.
|
|
1955
|
+
// Narrowed by the same filters as the listed rows (search query + required-
|
|
1956
|
+
// only). availableOptions (listed) and filteredOptions (these) are disjoint —
|
|
1957
|
+
// the former is built from fixedValue keys, the latter excludes them — so a
|
|
1958
|
+
// field is never both a listed and a hidden row.
|
|
1959
|
+
forEach(filteredOptions, (_schema, optionName) => {
|
|
1960
|
+
if (matchesFilters(optionName)) {
|
|
1961
|
+
pushRow(optionName, true);
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1920
1964
|
|
|
1921
1965
|
// User sort (Fields menu → "Sort by"), applied WITHIN each group so the
|
|
1922
1966
|
// group sections and the required-group rails are preserved. Schema order is
|
|
@@ -2102,19 +2146,35 @@ export const FormEngine = ({
|
|
|
2102
2146
|
<StyledCompactWrap
|
|
2103
2147
|
ref={setCompactWrap}
|
|
2104
2148
|
className='options-readfirst-scroll'
|
|
2105
|
-
|
|
2149
|
+
// A nested sub-form sits flush inside the parent's card — no outer
|
|
2150
|
+
// gutter (the card already provides the breathing room).
|
|
2151
|
+
$flush={compactFlush || compactNested}
|
|
2106
2152
|
>
|
|
2107
2153
|
<StyledCompactPanel
|
|
2108
|
-
|
|
2154
|
+
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
2155
|
+
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
2156
|
+
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
2157
|
+
// sticky, the backdrop, and the stacking context so its header is
|
|
2158
|
+
// transparent inside the parent's card.
|
|
2159
|
+
$headerBg={compactNested ? 'transparent' : headerBg}
|
|
2160
|
+
$nested={compactNested}
|
|
2109
2161
|
flat
|
|
2110
|
-
|
|
2162
|
+
// No panel background: the form sits transparently on whatever
|
|
2163
|
+
// hosts it (page, drawer, or — for an arg_schema field — the
|
|
2164
|
+
// parent's edit card) instead of stacking its own dark surface.
|
|
2165
|
+
// The status boxes keep their own tints; the sticky toolbar keeps
|
|
2166
|
+
// its blurred header via the $headerBg override.
|
|
2167
|
+
transparent
|
|
2168
|
+
stickyHeader={!compactNested}
|
|
2111
2169
|
padded={false}
|
|
2112
2170
|
actions={compactHeaderActions}
|
|
2113
2171
|
contentStyle={{
|
|
2114
2172
|
display: 'flex',
|
|
2115
2173
|
flexFlow: 'column',
|
|
2116
2174
|
gap: '10px',
|
|
2117
|
-
|
|
2175
|
+
// Nested sub-form: no surrounding panel padding (it's flush in
|
|
2176
|
+
// the parent card); top-level keeps a small bottom gutter.
|
|
2177
|
+
padding: compactNested ? '0' : '0 0 12px',
|
|
2118
2178
|
}}
|
|
2119
2179
|
>
|
|
2120
2180
|
{size(groupKeys) === 0 ?
|
|
@@ -2147,6 +2207,14 @@ export const FormEngine = ({
|
|
|
2147
2207
|
minimal
|
|
2148
2208
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
2149
2209
|
collapsible
|
|
2210
|
+
// The Optional box now holds every not-yet-added field, so
|
|
2211
|
+
// it starts COLLAPSED to keep the form focused on what's in
|
|
2212
|
+
// use. But a SEARCH must surface matching addable fields —
|
|
2213
|
+
// and ReqorePanel unmounts collapsed content — so force it
|
|
2214
|
+
// open whenever a query is active. (isCollapsed is the
|
|
2215
|
+
// panel's controllable state; manual toggling still works
|
|
2216
|
+
// when no query is set.)
|
|
2217
|
+
isCollapsed={box.key === 'optional' && !query}
|
|
2150
2218
|
label={
|
|
2151
2219
|
<StyledGroupHeader>
|
|
2152
2220
|
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
@@ -44,12 +44,17 @@ export const PANEL_LEFT_CSS = `calc(${LABEL_COL} + ${COMPACT_ROW_PAD_X + COMPACT
|
|
|
44
44
|
// content blurs softly through.
|
|
45
45
|
export const StyledCompactPanel = styled(ReqorePanel)<{
|
|
46
46
|
$headerBg: string;
|
|
47
|
+
$nested?: boolean;
|
|
47
48
|
}>`
|
|
48
49
|
> .reqore-panel-title {
|
|
49
50
|
background: ${({ $headerBg }) => $headerBg};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
/* The blur + translateZ exist only to make the STICKY top-level toolbar ghost
|
|
52
|
+
content beneath it; a nested sub-form's header isn't sticky, so skip them
|
|
53
|
+
(and the stacking context translateZ creates). */
|
|
54
|
+
${({ $nested }) =>
|
|
55
|
+
$nested ?
|
|
56
|
+
''
|
|
57
|
+
: 'backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); transform: translateZ(0);'}
|
|
53
58
|
padding-top: ${GAP_FROM_SIZE[HEADER_GAP]}px;
|
|
54
59
|
padding-bottom: ${GAP_FROM_SIZE[HEADER_GAP]}px;
|
|
55
60
|
}
|
|
@@ -289,18 +294,11 @@ export const StyledRowValue = styled.div<{ $color: string; $empty?: boolean }>`
|
|
|
289
294
|
export const StyledRowActions = styled.div`
|
|
290
295
|
display: flex;
|
|
291
296
|
align-items: center;
|
|
292
|
-
/*
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
below the value. */
|
|
297
|
-
align-self: start;
|
|
297
|
+
/* No align-self override: the actions (incl. the status dot) follow the row's
|
|
298
|
+
own vertical alignment — CENTRED on the common single-line row, TOP-aligned
|
|
299
|
+
(first line) on the tall rows that opt into align-items:start (descriptions /
|
|
300
|
+
message panels / hash previews). */
|
|
298
301
|
gap: 6px;
|
|
299
|
-
.options-readfirst-statusdot-slot {
|
|
300
|
-
align-self: flex-start;
|
|
301
|
-
align-items: center;
|
|
302
|
-
height: 12px;
|
|
303
|
-
}
|
|
304
302
|
`;
|
|
305
303
|
|
|
306
304
|
// A single status mark pinned at the row's trailing edge: one dot, colour =
|
|
@@ -383,14 +381,18 @@ export const StyledGroupBody = styled.div<{
|
|
|
383
381
|
grid wider than its container and produce a horizontal scrollbar. The 0
|
|
384
382
|
minimum lets it shrink and the value cell's ellipsis take over instead. */
|
|
385
383
|
grid-template-columns: ${LABEL_COL} minmax(0, 1fr) auto;
|
|
386
|
-
/*
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
(
|
|
390
|
-
|
|
384
|
+
/* CENTRE the cells vertically: on the common single-line read row the label,
|
|
385
|
+
value and status dot all sit on one centred line. Tall rows (a shown
|
|
386
|
+
description, message panels or a hash preview) opt back into top-alignment
|
|
387
|
+
(.readfirst-row-info-open / .readfirst-row-tall below) so the label + dot
|
|
388
|
+
stay on the value's FIRST line instead of floating to the middle. */
|
|
389
|
+
align-items: center;
|
|
391
390
|
gap: 14px;
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
/* Generous, SYMMETRIC vertical padding so a single-line row isn't cramped
|
|
392
|
+
(content sits with even breathing room top + bottom); a min-height floor
|
|
393
|
+
keeps the rare shorter row a comfortable tap target. */
|
|
394
|
+
min-height: 40px;
|
|
395
|
+
padding: 9px 10px;
|
|
394
396
|
border-radius: 6px;
|
|
395
397
|
cursor: pointer;
|
|
396
398
|
transition: background 0.12s ease;
|
|
@@ -469,10 +471,11 @@ export const StyledGroupBody = styled.div<{
|
|
|
469
471
|
the ✓/↺ cluster get small offsets to sit optically centred on it. */
|
|
470
472
|
align-items: start;
|
|
471
473
|
background: ${({ $hover }) => $hover};
|
|
472
|
-
/*
|
|
473
|
-
|
|
474
|
+
/* No top padding (the per-cell nudges below anchor the editor to the first
|
|
475
|
+
line), but a real BOTTOM padding so the editor never sits flush against
|
|
476
|
+
the row's bottom edge — a tall input used to look clipped/unfinished. */
|
|
474
477
|
padding-top: 0;
|
|
475
|
-
padding-bottom:
|
|
478
|
+
padding-bottom: 9px;
|
|
476
479
|
/* Tighter column gap: the editor's trailing template ⋮ and our ✓ should
|
|
477
480
|
read as one control cluster, not two separated groups. */
|
|
478
481
|
column-gap: 6px;
|
|
@@ -566,11 +569,12 @@ export const StyledGroupBody = styled.div<{
|
|
|
566
569
|
/* (The required-group connection rail was removed — the "One of the below is
|
|
567
570
|
required" box now carries the grouping.) */
|
|
568
571
|
|
|
569
|
-
/*
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
.readfirst-row-info-open
|
|
572
|
+
/* Tall rows top-anchor (overriding the row's centred default) so the label and
|
|
573
|
+
dot stay on the value's FIRST line rather than floating to the vertical
|
|
574
|
+
middle: .readfirst-row-info-open = a shown short_desc grows the LABEL;
|
|
575
|
+
.readfirst-row-tall = message panels / a hash preview grow the VALUE cell. */
|
|
576
|
+
.readfirst-row-info-open,
|
|
577
|
+
.readfirst-row-tall {
|
|
574
578
|
align-items: start;
|
|
575
579
|
}
|
|
576
580
|
/* Narrow stacks label-over-value: the value aligns flush UNDER the label (no
|
|
@@ -190,9 +190,16 @@ export const ViaFormEngine: Story = {
|
|
|
190
190
|
},
|
|
191
191
|
async play({ canvasElement }) {
|
|
192
192
|
const canvas = within(canvasElement);
|
|
193
|
-
|
|
193
|
+
// Generous timeout: findByText defaults to 1s, which flakes under CI load
|
|
194
|
+
// while the engine boots the auto field's type picker (the rest of the suite
|
|
195
|
+
// waits ~10s).
|
|
196
|
+
await expect(
|
|
197
|
+
await canvas.findByText('My Auto Field', undefined, { timeout: 10000 })
|
|
198
|
+
).toBeInTheDocument();
|
|
194
199
|
// The auto field renders its type picker inside the engine-driven form.
|
|
195
|
-
await expect(
|
|
200
|
+
await expect(
|
|
201
|
+
await canvas.findByText('Please select data type', undefined, { timeout: 10000 })
|
|
202
|
+
).toBeInTheDocument();
|
|
196
203
|
},
|
|
197
204
|
};
|
|
198
205
|
|
|
@@ -545,6 +545,9 @@ function AutoField<T = any>({
|
|
|
545
545
|
wrapperPadding='top'
|
|
546
546
|
flat
|
|
547
547
|
compact={compact}
|
|
548
|
+
// Embedded sub-form: no scroll context of its own, so its toolbar
|
|
549
|
+
// isn't sticky and its header stays transparent (no dark backdrop).
|
|
550
|
+
compactNested
|
|
548
551
|
name={name}
|
|
549
552
|
uniqueName={uniqueName}
|
|
550
553
|
options={finalArgSchema}
|