@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,11 +3,8 @@ import {
|
|
|
3
3
|
ReqoreControlGroup,
|
|
4
4
|
ReqoreDropdown,
|
|
5
5
|
ReqoreInput,
|
|
6
|
-
ReqoreMessage,
|
|
7
|
-
ReqoreProgress,
|
|
8
|
-
ReqoreSpan,
|
|
9
|
-
ReqoreTag,
|
|
10
6
|
} from '@qoretechnologies/reqore';
|
|
7
|
+
import { useReqoreTheme } from '@qoretechnologies/reqore/dist/hooks/useTheme';
|
|
11
8
|
import { IReqoreControlGroupProps } from '@qoretechnologies/reqore/dist/components/ControlGroup';
|
|
12
9
|
import {
|
|
13
10
|
IReqoreDropdownItem,
|
|
@@ -28,20 +25,73 @@ const SORT_MODES: { value: TCompactSort; label: string; tooltip: string }[] = [
|
|
|
28
25
|
{ value: 'invalid', label: 'Invalid first', tooltip: 'Fields needing attention first' },
|
|
29
26
|
];
|
|
30
27
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// `flex: 1; min-width: 0` makes only the bar yield as the row narrows.
|
|
28
|
+
// "Focus" header: a summary line ({pct}% complete · {set}/{total} set ·
|
|
29
|
+
// {attention} need attention →) above a SEGMENTED meter — a green run (set) then
|
|
30
|
+
// an amber run (needs attention) then the empty remainder.
|
|
35
31
|
const StyledCompletion = styled.div`
|
|
36
32
|
display: flex;
|
|
37
|
-
|
|
38
|
-
gap:
|
|
33
|
+
flex-flow: column;
|
|
34
|
+
gap: 8px;
|
|
39
35
|
padding: 0 2px;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
`;
|
|
37
|
+
const StyledCompletionLine = styled.div`
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: baseline;
|
|
40
|
+
gap: 10px;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
`;
|
|
43
|
+
const StyledMeter = styled.div<{ $set: number; $attention: number; $track: string; $set_c: string; $att_c: string }>`
|
|
44
|
+
position: relative;
|
|
45
|
+
height: 4px;
|
|
46
|
+
border-radius: 3px;
|
|
47
|
+
width: 100%;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
background: ${({ $track }) => $track};
|
|
50
|
+
/* No per-segment radius — the container's overflow:hidden + radius rounds only
|
|
51
|
+
the OUTER corners, so the green/amber runs meet flush (no notch). */
|
|
52
|
+
&::before,
|
|
53
|
+
&::after {
|
|
54
|
+
content: '';
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 0;
|
|
57
|
+
bottom: 0;
|
|
58
|
+
}
|
|
59
|
+
/* green run: 0 → set% */
|
|
60
|
+
&::before {
|
|
61
|
+
left: 0;
|
|
62
|
+
width: ${({ $set }) => $set}%;
|
|
63
|
+
background: ${({ $set_c }) => $set_c};
|
|
64
|
+
}
|
|
65
|
+
/* amber run: set% → set%+attention% */
|
|
66
|
+
&::after {
|
|
67
|
+
left: ${({ $set }) => $set}%;
|
|
68
|
+
width: ${({ $attention }) => $attention}%;
|
|
69
|
+
background: ${({ $att_c }) => $att_c};
|
|
43
70
|
}
|
|
44
71
|
`;
|
|
72
|
+
// One shared text size for the whole summary line, so "Draft", "1/6 set" and
|
|
73
|
+
// "N need attention" all read at the same scale (no chips, no size jumps).
|
|
74
|
+
const StyledSummary = styled.span<{ $color?: string }>`
|
|
75
|
+
font-size: 13px;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
color: ${({ $color }) => $color || 'inherit'};
|
|
78
|
+
`;
|
|
79
|
+
const StyledAttentionLink = styled.span<{ $color: string }>`
|
|
80
|
+
color: ${({ $color }) => $color};
|
|
81
|
+
font-size: 13px;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
&:hover {
|
|
85
|
+
text-decoration: underline;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
// The percentage, pushed to the far right of the summary line.
|
|
89
|
+
const StyledPct = styled.span`
|
|
90
|
+
margin-left: auto;
|
|
91
|
+
font-weight: 700;
|
|
92
|
+
font-size: 17px;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
`;
|
|
45
95
|
|
|
46
96
|
/**
|
|
47
97
|
* The compact form's sticky toolbar (completion meter + field filter + "Fields"
|
|
@@ -55,8 +105,8 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
|
|
|
55
105
|
const {
|
|
56
106
|
readOnly,
|
|
57
107
|
invalidCount,
|
|
108
|
+
attentionCount,
|
|
58
109
|
completion,
|
|
59
|
-
showInvalidOnly,
|
|
60
110
|
onToggleInvalidOnly,
|
|
61
111
|
hasMultipleOptions,
|
|
62
112
|
compactQuery,
|
|
@@ -78,51 +128,53 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
|
|
|
78
128
|
onRevertAll,
|
|
79
129
|
} = useContext(CompactToolbarContext);
|
|
80
130
|
|
|
131
|
+
const theme = useReqoreTheme();
|
|
132
|
+
const intents = (theme.intents || {}) as Record<string, string>;
|
|
133
|
+
const cSuccess = intents.success || '#4a7110';
|
|
134
|
+
const cWarning = intents.warning || '#d17c29';
|
|
135
|
+
const cText = (theme.text?.color as string) || '#e8e8e8';
|
|
136
|
+
const cTrack = `${cText}1f`;
|
|
137
|
+
const setPct = completion.total ? (completion.set / completion.total) * 100 : 0;
|
|
138
|
+
const attentionPct = completion.total ? (attentionCount / completion.total) * 100 : 0;
|
|
139
|
+
|
|
81
140
|
return (
|
|
82
141
|
<ReqoreControlGroup {...reqoreProps} vertical fluid fixed={false} gapSize='big'>
|
|
83
142
|
{completion.total ?
|
|
84
143
|
<StyledCompletion className='options-readfirst-completion'>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<
|
|
144
|
+
<StyledCompletionLine>
|
|
145
|
+
{!readOnly ?
|
|
146
|
+
<StyledSummary
|
|
88
147
|
className='options-readfirst-status'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: null}
|
|
113
|
-
<ReqoreSpan size='small' effect={{ opacity: 0.7, noWrap: true }}>
|
|
114
|
-
{completion.set} / {completion.total} fields set
|
|
115
|
-
</ReqoreSpan>
|
|
116
|
-
<ReqoreProgress
|
|
148
|
+
$color={invalidCount ? cWarning : cSuccess}
|
|
149
|
+
style={{ fontWeight: 600 }}
|
|
150
|
+
>
|
|
151
|
+
{invalidCount ? 'Draft' : 'Ready'}
|
|
152
|
+
</StyledSummary>
|
|
153
|
+
: null}
|
|
154
|
+
<StyledSummary style={{ opacity: 0.5 }}>
|
|
155
|
+
{!readOnly ? '· ' : ''}
|
|
156
|
+
{completion.set}/{completion.total} set
|
|
157
|
+
{!readOnly && attentionCount ? ' ·' : ''}
|
|
158
|
+
</StyledSummary>
|
|
159
|
+
{!readOnly && attentionCount ?
|
|
160
|
+
<StyledAttentionLink
|
|
161
|
+
$color={cWarning}
|
|
162
|
+
className='options-readfirst-attention-link'
|
|
163
|
+
onClick={onToggleInvalidOnly}
|
|
164
|
+
>
|
|
165
|
+
{attentionCount} need attention →
|
|
166
|
+
</StyledAttentionLink>
|
|
167
|
+
: null}
|
|
168
|
+
<StyledPct>{completion.pct}%</StyledPct>
|
|
169
|
+
</StyledCompletionLine>
|
|
170
|
+
<StyledMeter
|
|
117
171
|
className='options-readfirst-completion-bar'
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
172
|
+
$set={setPct}
|
|
173
|
+
$attention={attentionPct}
|
|
174
|
+
$track={cTrack}
|
|
175
|
+
$set_c={cSuccess}
|
|
176
|
+
$att_c={cWarning}
|
|
122
177
|
/>
|
|
123
|
-
<ReqoreSpan size='small' effect={{ opacity: 0.7, noWrap: true }}>
|
|
124
|
-
{completion.pct}%
|
|
125
|
-
</ReqoreSpan>
|
|
126
178
|
</StyledCompletion>
|
|
127
179
|
: null}
|
|
128
180
|
|
|
@@ -147,10 +199,10 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
|
|
|
147
199
|
{!readOnly ?
|
|
148
200
|
<ReqoreDropdown
|
|
149
201
|
fixed
|
|
150
|
-
|
|
202
|
+
flat
|
|
151
203
|
filterable
|
|
152
204
|
icon='Filter3Line'
|
|
153
|
-
|
|
205
|
+
tooltip='Fields'
|
|
154
206
|
className='options-readfirst-fields'
|
|
155
207
|
intent={requiredOnly ? 'info' : undefined}
|
|
156
208
|
badge={requiredOnly ? 'Required only' : undefined}
|
|
@@ -238,7 +290,8 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
|
|
|
238
290
|
: null}
|
|
239
291
|
<ReqoreButton
|
|
240
292
|
fixed
|
|
241
|
-
|
|
293
|
+
flat
|
|
294
|
+
minimal={showAllDescriptions === true}
|
|
242
295
|
icon={showAllDescriptions ? 'InformationFill' : 'InformationLine'}
|
|
243
296
|
className='options-readfirst-descriptions'
|
|
244
297
|
active={showAllDescriptions}
|
|
@@ -248,19 +301,6 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
|
|
|
248
301
|
/>
|
|
249
302
|
</ReqoreControlGroup>
|
|
250
303
|
: null}
|
|
251
|
-
{invalidCount && !readOnly ?
|
|
252
|
-
<ReqoreMessage
|
|
253
|
-
intent={showInvalidOnly ? 'info' : 'danger'}
|
|
254
|
-
opaque={false}
|
|
255
|
-
size='small'
|
|
256
|
-
className='options-readfirst-invalid-banner'
|
|
257
|
-
onClick={onToggleInvalidOnly}
|
|
258
|
-
>
|
|
259
|
-
{showInvalidOnly ?
|
|
260
|
-
'Showing invalid fields only. Click here again to show all fields.'
|
|
261
|
-
: `${invalidCount < 2 ? 'A field is not valid and requires' : `${invalidCount} fields are not valid and require`} attention. Click here to only show invalid fields.`}
|
|
262
|
-
</ReqoreMessage>
|
|
263
|
-
: null}
|
|
264
304
|
</ReqoreControlGroup>
|
|
265
305
|
: null}
|
|
266
306
|
</ReqoreControlGroup>
|
|
@@ -963,7 +963,7 @@ const CompactSchema: Record<string, TCompactField> = {
|
|
|
963
963
|
};
|
|
964
964
|
|
|
965
965
|
// `description` is intentionally left empty so the required-but-unset state is
|
|
966
|
-
// visible (a "
|
|
966
|
+
// visible (a "— Required" row and an invalid-field message).
|
|
967
967
|
const CompactValue: IOptions = {
|
|
968
968
|
name: { type: 'string', value: 'order-fulfilment' },
|
|
969
969
|
lang: { type: 'string', value: 'python' },
|
|
@@ -1022,10 +1022,16 @@ export const Compact: Story = {
|
|
|
1022
1022
|
play: async () => {
|
|
1023
1023
|
// Groups render with their display metadata; rows show formatted values.
|
|
1024
1024
|
await _testsWaitForText('Identity and core settings');
|
|
1025
|
+
// Regression: `general` is a REAL consumer-defined group here (it's in
|
|
1026
|
+
// CompactGroups, and `description`/`tags` set `group: 'general'`), so its
|
|
1027
|
+
// sub-label MUST render. It must NOT be suppressed as the synthetic "no
|
|
1028
|
+
// group" catch-all — doing so visually merged its rows (e.g. Tags) into the
|
|
1029
|
+
// group above them.
|
|
1030
|
+
await _testsWaitForText('General');
|
|
1025
1031
|
await _testsWaitForText('order-fulfilment');
|
|
1026
1032
|
await _testsWaitForText('orders, batch');
|
|
1027
1033
|
await _testsWaitForText('Yes');
|
|
1028
|
-
await _testsWaitForText('
|
|
1034
|
+
await _testsWaitForText('—');
|
|
1029
1035
|
},
|
|
1030
1036
|
};
|
|
1031
1037
|
|
|
@@ -1039,11 +1045,12 @@ export const CompactReadOnly: Story = {
|
|
|
1039
1045
|
await _testsWaitForText('order-fulfilment');
|
|
1040
1046
|
// Read-only hides the Draft/Ready badge (the meter itself stays)…
|
|
1041
1047
|
await _testsWaitForTextToNotExist('Draft');
|
|
1042
|
-
// …and rows open in view mode:
|
|
1048
|
+
// …and rows open in view mode: the card's done (✓/close) button collapses
|
|
1049
|
+
// back. The button is icon-only now, so assert it by class, not text.
|
|
1043
1050
|
await _testsClickText('order-fulfilment');
|
|
1044
|
-
await
|
|
1051
|
+
await waitFor(() => expect(document.querySelector('.options-readfirst-done')).toBeTruthy());
|
|
1045
1052
|
await _testsClickButton({ selector: '.options-readfirst-done' });
|
|
1046
|
-
await
|
|
1053
|
+
await waitFor(() => expect(document.querySelector('.options-readfirst-done')).toBeNull());
|
|
1047
1054
|
},
|
|
1048
1055
|
};
|
|
1049
1056
|
|
|
@@ -1057,8 +1064,8 @@ export const CompactEmpty: Story = {
|
|
|
1057
1064
|
},
|
|
1058
1065
|
play: async () => {
|
|
1059
1066
|
// Both required fields read as unset; the four optional ones as "Not set".
|
|
1060
|
-
|
|
1061
|
-
await _testsWaitForTextsCount('
|
|
1067
|
+
// All six empty fields read as a calm dash (the red asterisk marks required).
|
|
1068
|
+
await _testsWaitForTextsCount('—', undefined, 6);
|
|
1062
1069
|
},
|
|
1063
1070
|
};
|
|
1064
1071
|
|
|
@@ -1539,11 +1546,10 @@ export const CompactValidIdentifierRule: Story = {
|
|
|
1539
1546
|
play: async () => {
|
|
1540
1547
|
await _testsWaitForText('Variable name');
|
|
1541
1548
|
await _testsWaitForText('1-bad-identifier');
|
|
1542
|
-
// The rules-driven validation marks the form as needing attention
|
|
1549
|
+
// The rules-driven validation marks the form as needing attention — the
|
|
1550
|
+
// dedicated "Needs attention" box (and the header link) signal it.
|
|
1543
1551
|
await _testsWaitForText('Draft');
|
|
1544
|
-
await _testsWaitForText(
|
|
1545
|
-
'A field is not valid and requires attention. Click here to only show invalid fields.'
|
|
1546
|
-
);
|
|
1552
|
+
await _testsWaitForText('Needs attention');
|
|
1547
1553
|
},
|
|
1548
1554
|
};
|
|
1549
1555
|
|
|
@@ -1608,7 +1614,19 @@ export const CompactFocusedEditing: Story = {
|
|
|
1608
1614
|
await waitFor(() => expect(document.querySelector('.options-readfirst-card')).toBeTruthy(), {
|
|
1609
1615
|
timeout: 10000,
|
|
1610
1616
|
});
|
|
1611
|
-
|
|
1617
|
+
// Fullscreen now lives in the card's "More" (⋮) menu, before the Done ✓.
|
|
1618
|
+
await _testsClickButton({ selector: '.options-readfirst-more' });
|
|
1619
|
+
let fsItem: Element | undefined;
|
|
1620
|
+
await waitFor(
|
|
1621
|
+
() => {
|
|
1622
|
+
fsItem = Array.from(document.querySelectorAll('.reqore-menu-item')).find((element) =>
|
|
1623
|
+
element.textContent?.includes('Edit fullscreen')
|
|
1624
|
+
);
|
|
1625
|
+
expect(fsItem).toBeTruthy();
|
|
1626
|
+
},
|
|
1627
|
+
{ timeout: 10000 }
|
|
1628
|
+
);
|
|
1629
|
+
await fireEvent.click(fsItem as Element);
|
|
1612
1630
|
await waitFor(() => expect(document.querySelector('.reqore-modal')).toBeTruthy(), {
|
|
1613
1631
|
timeout: 10000,
|
|
1614
1632
|
});
|
|
@@ -1709,7 +1727,7 @@ export const CompactReadFirstEditing: Story = {
|
|
|
1709
1727
|
await _testsWaitForText('Yes');
|
|
1710
1728
|
await _testsWaitForText('Python');
|
|
1711
1729
|
// The required-but-empty field shows its placeholder instead of an editor.
|
|
1712
|
-
await _testsWaitForText('
|
|
1730
|
+
await _testsWaitForText('—');
|
|
1713
1731
|
// No field editor (textarea) is mounted while everything is collapsed.
|
|
1714
1732
|
await expect(document.querySelectorAll('.reqore-textarea')).toHaveLength(0);
|
|
1715
1733
|
|
|
@@ -1798,14 +1816,31 @@ export const CompactFieldsMenu: Story = {
|
|
|
1798
1816
|
await clickFieldsMenuItem('Default fields');
|
|
1799
1817
|
await _testsWaitForTextToNotExist('Notes');
|
|
1800
1818
|
|
|
1801
|
-
// The
|
|
1802
|
-
//
|
|
1819
|
+
// The delete affordance now lives in the expanded editor's "More" (⋮) menu:
|
|
1820
|
+
// re-add Notes, open it, then Remove field via More → the confirm modal →
|
|
1821
|
+
// Confirm.
|
|
1803
1822
|
await clickFieldsMenuItem('Select all');
|
|
1804
1823
|
await _testsWaitForText('Notes');
|
|
1805
|
-
await
|
|
1806
|
-
|
|
1824
|
+
await _testsClickText('Notes');
|
|
1825
|
+
await waitFor(
|
|
1826
|
+
() =>
|
|
1827
|
+
expect(
|
|
1828
|
+
document.querySelector('[data-field="notes"] .options-readfirst-more')
|
|
1829
|
+
).toBeTruthy(),
|
|
1830
|
+
{ timeout: 10000 }
|
|
1831
|
+
);
|
|
1832
|
+
await _testsClickButton({ selector: '[data-field="notes"] .options-readfirst-more' });
|
|
1833
|
+
let removeItem: Element | undefined;
|
|
1834
|
+
await waitFor(
|
|
1835
|
+
() => {
|
|
1836
|
+
removeItem = Array.from(document.querySelectorAll('.reqore-menu-item')).find((element) =>
|
|
1837
|
+
element.textContent?.includes('Remove field')
|
|
1838
|
+
);
|
|
1839
|
+
expect(removeItem).toBeTruthy();
|
|
1840
|
+
},
|
|
1841
|
+
{ timeout: 10000 }
|
|
1807
1842
|
);
|
|
1808
|
-
await
|
|
1843
|
+
await fireEvent.click(removeItem as Element);
|
|
1809
1844
|
await _testsClickButton({ label: 'Confirm' });
|
|
1810
1845
|
await _testsWaitForTextToNotExist('Notes');
|
|
1811
1846
|
},
|
|
@@ -1847,6 +1882,31 @@ export const CompactDescriptionsToggle: Story = {
|
|
|
1847
1882
|
// One toggle reveals the short_desc on every field that has one.
|
|
1848
1883
|
await _testsWaitForText('The server hostname or IP address');
|
|
1849
1884
|
await _testsWaitForText('TCP port to connect on');
|
|
1885
|
+
|
|
1886
|
+
// Regression: opening a field for INLINE editing must keep its description
|
|
1887
|
+
// visible while the global toggle is on — it used to vanish because the
|
|
1888
|
+
// inline editor's label dropped the short_desc.
|
|
1889
|
+
await _testsClickText('Host');
|
|
1890
|
+
await waitFor(
|
|
1891
|
+
() =>
|
|
1892
|
+
expect(
|
|
1893
|
+
document.querySelector(
|
|
1894
|
+
'.readfirst-row-editing[data-field="host"] .options-readfirst-label-desc'
|
|
1895
|
+
)
|
|
1896
|
+
).toBeTruthy(),
|
|
1897
|
+
{ timeout: 10000 }
|
|
1898
|
+
);
|
|
1899
|
+
await _testsWaitForText('The server hostname or IP address');
|
|
1900
|
+
// Collapse back to the read row (Done) before toggling descriptions off.
|
|
1901
|
+
await _testsClickButton({ selector: '[data-field="host"] .options-readfirst-done' });
|
|
1902
|
+
await waitFor(
|
|
1903
|
+
() =>
|
|
1904
|
+
expect(
|
|
1905
|
+
document.querySelector('.readfirst-row-editing[data-field="host"]')
|
|
1906
|
+
).toBeFalsy(),
|
|
1907
|
+
{ timeout: 10000 }
|
|
1908
|
+
);
|
|
1909
|
+
|
|
1850
1910
|
// Toggling off hides them again.
|
|
1851
1911
|
await _testsClickButton({ selector: '.options-readfirst-descriptions' });
|
|
1852
1912
|
await _testsWaitForTextToNotExist('The server hostname or IP address');
|
|
@@ -1874,7 +1934,9 @@ export const CompactSearchHidden: Story = {
|
|
|
1874
1934
|
value: 'notes',
|
|
1875
1935
|
});
|
|
1876
1936
|
await _testsWaitForText('Notes');
|
|
1877
|
-
await
|
|
1937
|
+
await waitFor(() =>
|
|
1938
|
+
expect(document.querySelector('.readfirst-row-hidden[data-field="notes"]')).toBeTruthy()
|
|
1939
|
+
);
|
|
1878
1940
|
|
|
1879
1941
|
// Rows are keyboard-operable (role=button + Enter): focusing the hidden row
|
|
1880
1942
|
// and pressing Enter adds the field and opens its inline editor.
|
|
@@ -2928,29 +2990,17 @@ export const CompactFieldTypes: Story = {
|
|
|
2928
2990
|
// fresh nodes per click.
|
|
2929
2991
|
await _testsWaitForText('This value fails validation upstream.');
|
|
2930
2992
|
await _testsWaitForText('Deprecated — migrate before 2026-09.');
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
);
|
|
2935
|
-
await expect(catalogPanel('infoMsgQuiet')).toBeNull();
|
|
2936
|
-
await fireEvent.click(
|
|
2937
|
-
document.querySelector(
|
|
2938
|
-
'.readfirst-row[data-field="infoMsgQuiet"] .options-readfirst-info-slot .options-readfirst-info-toggle'
|
|
2939
|
-
) as HTMLElement
|
|
2940
|
-
);
|
|
2993
|
+
// Dedicated schema messages render as panels, visible WITHOUT interaction
|
|
2994
|
+
// (the per-row ⓘ is gone — descriptions are revealed by the global toggle or
|
|
2995
|
+
// by expanding the field).
|
|
2941
2996
|
await _testsWaitForText('Requests are signed automatically.');
|
|
2942
2997
|
await _testsWaitForText('Connection verified.');
|
|
2943
|
-
//
|
|
2944
|
-
|
|
2945
|
-
await expect(catalogPanel('infoShortDesc')).toBeNull();
|
|
2946
|
-
await fireEvent.click(
|
|
2947
|
-
document.querySelector(
|
|
2948
|
-
'.readfirst-row[data-field="infoShortDesc"] .options-readfirst-info-slot .options-readfirst-info-toggle'
|
|
2949
|
-
) as HTMLElement
|
|
2950
|
-
);
|
|
2998
|
+
// The global descriptions toggle reveals each field's short_desc under its name.
|
|
2999
|
+
await fireEvent.click(document.querySelector('.options-readfirst-descriptions') as HTMLElement);
|
|
2951
3000
|
await _testsWaitForText(
|
|
2952
3001
|
'A one-line summary shown under the field name and in the hover title.'
|
|
2953
3002
|
);
|
|
3003
|
+
// A field with a long desc still exposes the ? help affordance.
|
|
2954
3004
|
await expect(
|
|
2955
3005
|
document.querySelector('.readfirst-row[data-field="infoLongDesc"] .options-readfirst-help')
|
|
2956
3006
|
).toBeTruthy();
|
|
@@ -2988,7 +3038,10 @@ const _compactExpandAllRows = async () => {
|
|
|
2988
3038
|
document.querySelectorAll<HTMLElement>(
|
|
2989
3039
|
'.readfirst-row:not(.readfirst-row-editing):not(.readfirst-row-disabled):not(.readfirst-row-hidden)'
|
|
2990
3040
|
)
|
|
2991
|
-
|
|
3041
|
+
// An arg_schema field opens a NESTED compact sub-form (recursive compact);
|
|
3042
|
+
// its rows live inside the parent's edit card — don't count those as
|
|
3043
|
+
// top-level read rows to expand.
|
|
3044
|
+
).filter((r) => !r.closest('.options-readfirst-card'));
|
|
2992
3045
|
// Generous guard: the catalog has ~70 fields.
|
|
2993
3046
|
for (let guard = 0; guard < 120; guard++) {
|
|
2994
3047
|
const remaining = readRows();
|
|
@@ -3090,7 +3143,7 @@ export const CompactRequiredGroups: Story = {
|
|
|
3090
3143
|
// contiguous members (byHost/byFile in Connection) cluster into a rail, which
|
|
3091
3144
|
// carries the grouping in place of a chip; only the lone member (byUrl in
|
|
3092
3145
|
// General) keeps a "One of" chip — so exactly one chip, not three.
|
|
3093
|
-
await _testsWaitForTextsCount('
|
|
3146
|
+
await _testsWaitForTextsCount('—', undefined, 3);
|
|
3094
3147
|
await _testsWaitForText('Draft');
|
|
3095
3148
|
await _testsWaitForTextsCount('One of', undefined, 1);
|
|
3096
3149
|
|
|
@@ -3147,13 +3200,14 @@ export const CompactRequiredGroups: Story = {
|
|
|
3147
3200
|
await _testsWaitForText('https://example.com');
|
|
3148
3201
|
|
|
3149
3202
|
// One fulfilled member satisfies the group → the badge flips to Ready and the
|
|
3150
|
-
//
|
|
3151
|
-
//
|
|
3203
|
+
// Once satisfied: the filled member keeps a "Covers" chip; the empty siblings
|
|
3204
|
+
// show their "Covered by 'By URL'" note INLINE (not a chip), and no "One of"
|
|
3205
|
+
// remains. So exactly one required-group chip stays (the coverer's).
|
|
3152
3206
|
await _testsWaitForText('Ready');
|
|
3153
3207
|
await _testsWaitForTextsCount('Covered by “By URL”', undefined, 2);
|
|
3154
3208
|
await _testsWaitForText('Covers');
|
|
3155
3209
|
await _testsWaitForTextToNotExist('One of');
|
|
3156
|
-
await expect(document.querySelectorAll('.options-readfirst-required-group')).toHaveLength(
|
|
3210
|
+
await expect(document.querySelectorAll('.options-readfirst-required-group')).toHaveLength(1);
|
|
3157
3211
|
},
|
|
3158
3212
|
};
|
|
3159
3213
|
|
|
@@ -3682,57 +3736,41 @@ export const CompactShowcase: Story = {
|
|
|
3682
3736
|
).toBeTruthy();
|
|
3683
3737
|
await waitFor(() => {
|
|
3684
3738
|
// The intent stripe rides the value surface's left border, fed by
|
|
3685
|
-
// --readfirst-stripe on the field's BLOCK root.
|
|
3686
|
-
//
|
|
3739
|
+
// --readfirst-stripe on the field's BLOCK root. Schema messages now render
|
|
3740
|
+
// inside the value cell, so the block root is the row itself.
|
|
3687
3741
|
const intentRow = document.querySelector(
|
|
3688
|
-
'.
|
|
3742
|
+
'.readfirst-row[data-field="chromeIntent"]'
|
|
3689
3743
|
) as HTMLElement;
|
|
3690
3744
|
expect(intentRow?.style?.getPropertyValue('--readfirst-stripe')).toBeTruthy();
|
|
3691
3745
|
});
|
|
3692
3746
|
await _testsWaitForText('••••••');
|
|
3693
3747
|
await _testsWaitForText('This field also carries a warning message.');
|
|
3694
|
-
// The
|
|
3695
|
-
//
|
|
3696
|
-
// alongside the info affordances.
|
|
3748
|
+
// The unmet auth one-of group (authToken/authCertFile) renders the
|
|
3749
|
+
// "One of the below is required" cluster box.
|
|
3697
3750
|
await waitFor(() =>
|
|
3698
|
-
expect(document.
|
|
3751
|
+
expect(document.querySelector('.options-readfirst-required-cluster')).toBeTruthy()
|
|
3699
3752
|
);
|
|
3700
3753
|
|
|
3701
|
-
//
|
|
3702
|
-
//
|
|
3703
|
-
// wrapper element.
|
|
3704
|
-
const infoToggle = (field: string) =>
|
|
3705
|
-
document.querySelector(
|
|
3706
|
-
`.readfirst-row[data-field="${field}"] .options-readfirst-info-slot .options-readfirst-info-toggle`
|
|
3707
|
-
) as HTMLElement;
|
|
3754
|
+
// Schema message panels render inside the value cell of the row itself,
|
|
3755
|
+
// directly beneath the value.
|
|
3708
3756
|
const infoPanel = (field: string) =>
|
|
3709
3757
|
document.querySelector(
|
|
3710
|
-
`.
|
|
3758
|
+
`.readfirst-row[data-field="${field}"] .options-readfirst-info-panel`
|
|
3711
3759
|
);
|
|
3712
3760
|
|
|
3713
|
-
//
|
|
3714
|
-
//
|
|
3715
|
-
// default-value note; toggling again hides it.
|
|
3716
|
-
await expect(infoPanel('metaDefault')).toBeNull();
|
|
3717
|
-
await expect(infoToggle('metaDefault')).toBeTruthy();
|
|
3718
|
-
await fireEvent.click(infoToggle('metaDefault'));
|
|
3761
|
+
// Default-value notes and validation/dependency hints now render as a compact
|
|
3762
|
+
// INLINE reason (no ⓘ, no panel) — visible without any interaction.
|
|
3719
3763
|
await _testsWaitForText('Default: thirty — Falls back to 30 seconds when unset.');
|
|
3720
|
-
|
|
3721
|
-
await waitFor(() => expect(infoPanel('metaDefault')).toBeNull());
|
|
3722
|
-
|
|
3723
|
-
// Auto-open panels can be dismissed the same way (override sticks).
|
|
3764
|
+
// Dedicated schema messages stay prominent PANELS, also always visible.
|
|
3724
3765
|
await expect(infoPanel('apiEndpoint')).toBeTruthy();
|
|
3725
|
-
await fireEvent.click(infoToggle('apiEndpoint'));
|
|
3726
|
-
await waitFor(() => expect(infoPanel('apiEndpoint')).toBeNull());
|
|
3727
|
-
await fireEvent.click(infoToggle('apiEndpoint'));
|
|
3728
3766
|
await _testsWaitForText('v1 endpoints are deprecated — migrate to /v2 before 2026-09.');
|
|
3729
3767
|
|
|
3730
|
-
// short_desc renders UNDER the field name
|
|
3731
|
-
//
|
|
3768
|
+
// short_desc renders UNDER the field name when the global descriptions toggle
|
|
3769
|
+
// is engaged (the per-row ⓘ is gone).
|
|
3732
3770
|
const labelDesc = (field: string) =>
|
|
3733
3771
|
document.querySelector(`.readfirst-row[data-field="${field}"] .options-readfirst-label-desc`);
|
|
3734
3772
|
await expect(labelDesc('chromeIcon')).toBeNull();
|
|
3735
|
-
await fireEvent.click(
|
|
3773
|
+
await fireEvent.click(document.querySelector('.options-readfirst-descriptions') as HTMLElement);
|
|
3736
3774
|
await waitFor(() => expect(labelDesc('chromeIcon')).toBeTruthy());
|
|
3737
3775
|
},
|
|
3738
3776
|
};
|
|
@@ -3817,38 +3855,14 @@ export const CompactRequiredGroupRails: Story = {
|
|
|
3817
3855
|
},
|
|
3818
3856
|
play: async () => {
|
|
3819
3857
|
await _testsWaitForText('API key');
|
|
3820
|
-
//
|
|
3858
|
+
// The unmet `target` group (email/slack/webhook/sms, none set) renders the
|
|
3859
|
+
// "One of the below is required" cluster box.
|
|
3821
3860
|
await waitFor(() =>
|
|
3822
|
-
expect(document.
|
|
3861
|
+
expect(document.querySelector('.options-readfirst-required-cluster')).toBeTruthy()
|
|
3823
3862
|
);
|
|
3824
|
-
// The
|
|
3825
|
-
//
|
|
3826
|
-
await
|
|
3827
|
-
const apiNode = document.querySelector(
|
|
3828
|
-
'.readfirst-row[data-field="apiKey"] .options-readfirst-node'
|
|
3829
|
-
) as HTMLElement;
|
|
3830
|
-
const oauthNode = document.querySelector(
|
|
3831
|
-
'.readfirst-row[data-field="oauthToken"] .options-readfirst-node'
|
|
3832
|
-
) as HTMLElement;
|
|
3833
|
-
// email is in the `target` group, which has NO member set — still unmet.
|
|
3834
|
-
const emailNode = document.querySelector(
|
|
3835
|
-
'.readfirst-row[data-field="email"] .options-readfirst-node'
|
|
3836
|
-
) as HTMLElement;
|
|
3837
|
-
// Fill marks the member carrying the value: apiKey filled (bg = its border
|
|
3838
|
-
// colour), oauthToken hollow (bg = the form background).
|
|
3839
|
-
expect(getComputedStyle(apiNode).backgroundColor).not.toBe(
|
|
3840
|
-
getComputedStyle(oauthNode).backgroundColor
|
|
3841
|
-
);
|
|
3842
|
-
// Colour follows the GROUP, not the member. The credential group is satisfied
|
|
3843
|
-
// by apiKey, so its empty alternative (oauthToken) reads the SAME colour as
|
|
3844
|
-
// the filled node — not warning. The still-unmet target group's node (email)
|
|
3845
|
-
// keeps the warning colour, so it differs.
|
|
3846
|
-
const apiBorder = getComputedStyle(apiNode).borderTopColor;
|
|
3847
|
-
const oauthBorder = getComputedStyle(oauthNode).borderTopColor;
|
|
3848
|
-
const emailBorder = getComputedStyle(emailNode).borderTopColor;
|
|
3849
|
-
expect(oauthBorder).toBe(apiBorder);
|
|
3850
|
-
expect(oauthBorder).not.toBe(emailBorder);
|
|
3851
|
-
});
|
|
3863
|
+
// The met `credential` group needs no box: apiKey satisfies it, so its empty
|
|
3864
|
+
// alternative oauthToken reads as covered by its sibling.
|
|
3865
|
+
await _testsWaitForText('Covered by “API key”');
|
|
3852
3866
|
},
|
|
3853
3867
|
};
|
|
3854
3868
|
|
|
@@ -3867,9 +3881,12 @@ export const CompactFieldSortWithinGroups: Story = {
|
|
|
3867
3881
|
)
|
|
3868
3882
|
).map((row) => row.getAttribute('data-field'));
|
|
3869
3883
|
|
|
3870
|
-
//
|
|
3884
|
+
// Rows are bucketed into status boxes (Needs attention → Set → Optional);
|
|
3885
|
+
// within a box, schema order holds. The `target` group is unmet (attention),
|
|
3886
|
+
// the `credential` group is met by apiKey (set), so the attention members
|
|
3887
|
+
// come first, then the set members.
|
|
3871
3888
|
await waitFor(() =>
|
|
3872
|
-
expect(order()).toEqual(['
|
|
3889
|
+
expect(order()).toEqual(['email', 'slack', 'webhook', 'sms', 'apiKey', 'oauthToken'])
|
|
3873
3890
|
);
|
|
3874
3891
|
|
|
3875
3892
|
// Open the Fields menu, drill into the (collapsed) "Sort by" submenu, then
|
|
@@ -3887,13 +3904,13 @@ export const CompactFieldSortWithinGroups: Story = {
|
|
|
3887
3904
|
await waitFor(() => expect(menuItem('Name A→Z')).toBeTruthy());
|
|
3888
3905
|
await fireEvent.click(menuItem('Name A→Z') as HTMLElement);
|
|
3889
3906
|
|
|
3890
|
-
// Sorted by display name inside each group; the
|
|
3891
|
-
// (
|
|
3907
|
+
// Sorted by display name inside each group; the status boxes stay separate
|
|
3908
|
+
// (attention: email/slack/sms/webhook | set: apiKey/oauthToken).
|
|
3892
3909
|
await waitFor(() =>
|
|
3893
|
-
expect(order()).toEqual(['
|
|
3910
|
+
expect(order()).toEqual(['email', 'slack', 'sms', 'webhook', 'apiKey', 'oauthToken'])
|
|
3894
3911
|
);
|
|
3895
|
-
// The required-group clusters survive the re-sort
|
|
3896
|
-
|
|
3912
|
+
// The required-group clusters survive the re-sort (both groups keep their
|
|
3913
|
+
// first-member marker, used to anchor the cluster box).
|
|
3897
3914
|
await expect(document.querySelectorAll('.readfirst-cluster-first')).toHaveLength(2);
|
|
3898
3915
|
},
|
|
3899
3916
|
};
|