@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g32331ea
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/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +128 -24
- 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 +78 -15
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +243 -85
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +22 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +5 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +72 -39
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +3 -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/formAppearance.d.ts +48 -0
- package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
- package/dist/components/form/engine/formAppearance.js +50 -0
- package/dist/components/form/engine/formAppearance.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +14 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +6 -2
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +9 -1
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
- package/dist/components/form/engine/rowFocus.js +17 -5
- package/dist/components/form/engine/rowFocus.js.map +1 -1
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +5 -3
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +4 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
- package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketHeader.js +4 -1
- package/dist/components/supportTicket/TicketHeader.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js +29 -10
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +141 -22
- package/src/components/form/engine/CompactToolbar.tsx +85 -16
- package/src/components/form/engine/FormEngine.stories.tsx +25 -13
- package/src/components/form/engine/FormEngine.tsx +358 -77
- package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
- package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
- package/src/components/form/engine/compactRowContext.ts +5 -0
- package/src/components/form/engine/compactRowStyles.ts +218 -38
- package/src/components/form/engine/compactToolbarContext.ts +3 -0
- package/src/components/form/engine/formAppearance.ts +78 -0
- package/src/components/form/engine/readFirst.ts +16 -3
- package/src/components/form/engine/rowFocus.ts +17 -6
- package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
- package/src/components/form/fields/Field.tsx +6 -5
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +7 -4
- package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
- package/src/components/form/index.tsx +1 -0
- package/src/components/supportTicket/TicketHeader.tsx +2 -1
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +17 -2
- package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { expect, fn, waitFor } from 'storybook/test';
|
|
4
|
+
import {
|
|
5
|
+
_testsClickText,
|
|
6
|
+
_testsWaitForText,
|
|
7
|
+
_testsWaitForTextToNotExist,
|
|
8
|
+
_testsWithViewport,
|
|
9
|
+
} from '../../../stories/Tests/utils';
|
|
10
|
+
import {
|
|
11
|
+
FormEngine,
|
|
12
|
+
IFormEngineGroup,
|
|
13
|
+
IFormEngineProps,
|
|
14
|
+
IOptions,
|
|
15
|
+
IOptionsSchemaArg,
|
|
16
|
+
} from './FormEngine';
|
|
17
|
+
import { CALM_REQUIRED_LABEL, CALM_REQUIRED_READ_LABEL } from './formAppearance';
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The calm appearance against the default one, over the fields a workflow
|
|
21
|
+
* creator actually serves (display names, groups and descriptions as the Qorus
|
|
22
|
+
* server's workflow metadata gives them), so the comparison is about the look
|
|
23
|
+
* and nothing else.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const groups: Record<string, IFormEngineGroup> = {
|
|
27
|
+
info: { label: 'Info', sort: 1 },
|
|
28
|
+
resources: { label: 'Resources', sort: 2 },
|
|
29
|
+
scaling: { label: 'Scaling', sort: 3 },
|
|
30
|
+
business: { label: 'Business', sort: 4 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// `group` is server-supplied metadata not yet in the typed schema; widen locally.
|
|
34
|
+
type TAppearanceField = IOptionsSchemaArg & { group?: string };
|
|
35
|
+
|
|
36
|
+
const options: Record<string, TAppearanceField> = {
|
|
37
|
+
name: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
display_name: 'Internal Name',
|
|
40
|
+
short_desc: 'The internal technical name of the object',
|
|
41
|
+
group: 'info',
|
|
42
|
+
preselected: true,
|
|
43
|
+
},
|
|
44
|
+
display_name: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
display_name: 'Name',
|
|
47
|
+
short_desc: 'The display name of the object',
|
|
48
|
+
required: true,
|
|
49
|
+
group: 'info',
|
|
50
|
+
},
|
|
51
|
+
desc: {
|
|
52
|
+
type: 'long-string',
|
|
53
|
+
display_name: 'Description',
|
|
54
|
+
short_desc: 'The markdown description of the object',
|
|
55
|
+
required: true,
|
|
56
|
+
group: 'info',
|
|
57
|
+
},
|
|
58
|
+
version: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
display_name: 'Version',
|
|
61
|
+
short_desc: 'The object’s version',
|
|
62
|
+
required: true,
|
|
63
|
+
group: 'info',
|
|
64
|
+
},
|
|
65
|
+
lang: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
display_name: 'Language',
|
|
68
|
+
required: true,
|
|
69
|
+
group: 'info',
|
|
70
|
+
messages: [
|
|
71
|
+
{
|
|
72
|
+
intent: 'info',
|
|
73
|
+
content: 'Changing the language later generates a new source skeleton.',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
allowed_values: [
|
|
77
|
+
{ value: { type: 'string', value: 'qore' }, display_name: 'Qore' },
|
|
78
|
+
{ value: { type: 'string', value: 'python' }, display_name: 'Python' },
|
|
79
|
+
{ value: { type: 'string', value: 'java' }, display_name: 'Java' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
classes: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
display_name: 'Classes',
|
|
85
|
+
short_desc: 'Any classes used by the object',
|
|
86
|
+
group: 'resources',
|
|
87
|
+
preselected: true,
|
|
88
|
+
},
|
|
89
|
+
autostart: {
|
|
90
|
+
type: 'int',
|
|
91
|
+
display_name: 'Autostart',
|
|
92
|
+
short_desc: 'The number of execution instances started automatically for the workflow',
|
|
93
|
+
group: 'scaling',
|
|
94
|
+
preselected: true,
|
|
95
|
+
},
|
|
96
|
+
max_instances: {
|
|
97
|
+
type: 'int',
|
|
98
|
+
display_name: 'Maximum Instances',
|
|
99
|
+
short_desc: 'The maximum number of workflow execution instances that can be started',
|
|
100
|
+
group: 'scaling',
|
|
101
|
+
preselected: true,
|
|
102
|
+
},
|
|
103
|
+
remote: {
|
|
104
|
+
type: 'bool',
|
|
105
|
+
display_name: 'Remote?',
|
|
106
|
+
short_desc: 'Should this interface run in a dedicated process?',
|
|
107
|
+
group: 'scaling',
|
|
108
|
+
preselected: true,
|
|
109
|
+
},
|
|
110
|
+
business_category: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
display_name: 'Business Category',
|
|
113
|
+
short_desc: 'Automation intelligence category for this interface',
|
|
114
|
+
group: 'business',
|
|
115
|
+
preselected: true,
|
|
116
|
+
},
|
|
117
|
+
author: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
display_name: 'Author',
|
|
120
|
+
short_desc: 'The author of the object',
|
|
121
|
+
group: 'info',
|
|
122
|
+
},
|
|
123
|
+
sla_threshold: {
|
|
124
|
+
type: 'int',
|
|
125
|
+
display_name: 'SLA Threshold',
|
|
126
|
+
short_desc: 'The time in seconds before which workflow orders should get a final status',
|
|
127
|
+
group: 'scaling',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const filledValue: IOptions = {
|
|
132
|
+
name: { type: 'string', value: 'supplier-payment-release' },
|
|
133
|
+
display_name: { type: 'string', value: 'Supplier Payment Release' },
|
|
134
|
+
desc: {
|
|
135
|
+
type: 'long-string',
|
|
136
|
+
value: 'Reconciles inbound orders with supplier confirmations and payment holds before release.',
|
|
137
|
+
},
|
|
138
|
+
version: { type: 'string', value: '1.0' },
|
|
139
|
+
lang: { type: 'string', value: 'qore' },
|
|
140
|
+
classes: { type: 'string', value: 'SupplyChainReconciler' },
|
|
141
|
+
autostart: { type: 'int', value: 1 },
|
|
142
|
+
max_instances: { type: 'int', value: 20 },
|
|
143
|
+
remote: { type: 'bool', value: false },
|
|
144
|
+
business_category: { type: 'string', value: 'Supply Chain' },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const newFormValue: IOptions = {
|
|
148
|
+
lang: { type: 'string', value: 'qore' },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const meta: Meta<typeof FormEngine> = {
|
|
152
|
+
component: FormEngine,
|
|
153
|
+
title: 'Form/Engine/FormEngine Appearance',
|
|
154
|
+
args: {
|
|
155
|
+
name: 'workflow',
|
|
156
|
+
compact: true,
|
|
157
|
+
options,
|
|
158
|
+
groups,
|
|
159
|
+
value: filledValue,
|
|
160
|
+
onChange: fn(),
|
|
161
|
+
},
|
|
162
|
+
parameters: {
|
|
163
|
+
layout: 'fullscreen',
|
|
164
|
+
},
|
|
165
|
+
render: ({ value, onChange, ...rest }: IFormEngineProps) => {
|
|
166
|
+
const [val, setValue] = useState(value);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
style={{
|
|
171
|
+
padding: '24px 16px',
|
|
172
|
+
width: '100%',
|
|
173
|
+
maxWidth: 1040,
|
|
174
|
+
margin: '0 auto',
|
|
175
|
+
boxSizing: 'border-box',
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<FormEngine
|
|
179
|
+
{...rest}
|
|
180
|
+
value={val}
|
|
181
|
+
onChange={(name, next, info) => {
|
|
182
|
+
setValue(next);
|
|
183
|
+
onChange?.(name, next, info);
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default meta;
|
|
192
|
+
type Story = StoryObj<typeof meta>;
|
|
193
|
+
|
|
194
|
+
/* --------------------------------------------------------------------------
|
|
195
|
+
* Measurements the calm look depends on
|
|
196
|
+
* ------------------------------------------------------------------------ */
|
|
197
|
+
|
|
198
|
+
const rgbOf = (color: string): number[] =>
|
|
199
|
+
(color.match(/[\d.]+/g) || []).slice(0, 3).map((channel) => Number(channel));
|
|
200
|
+
|
|
201
|
+
/** Amber: red well above blue (the warning colour), as opposed to a quiet grey. */
|
|
202
|
+
const isAmber = (color: string): boolean => {
|
|
203
|
+
const [r, , b] = rgbOf(color);
|
|
204
|
+
return r - b > 60;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** A grey: no channel more than a few steps from the others. */
|
|
208
|
+
const isGrey = (color: string): boolean => {
|
|
209
|
+
const [r, g, b] = rgbOf(color);
|
|
210
|
+
return Math.max(r, g, b) - Math.min(r, g, b) <= 8;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const box = (key: 'attention' | 'set' | 'optional'): HTMLElement | undefined =>
|
|
214
|
+
[...document.querySelectorAll<HTMLElement>('.options-readfirst-group')].find((element) => {
|
|
215
|
+
const heading = element.querySelector('.reqore-panel-title')?.textContent ?? '';
|
|
216
|
+
// The first box is "To fill in" until something in it is a fault (a new form).
|
|
217
|
+
return key === 'attention' ? /^(Needs attention|To fill in)/.test(heading)
|
|
218
|
+
: heading.startsWith(key === 'set' ? 'Set' : 'Optional');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const row = (field: string): HTMLElement | null =>
|
|
222
|
+
document.querySelector<HTMLElement>(`.readfirst-row[data-field="${field}"]`);
|
|
223
|
+
|
|
224
|
+
/** The section is a plain panel: a grey surface, a grey edge, and no tint. */
|
|
225
|
+
const expectCalmSection = (element: HTMLElement | undefined) => {
|
|
226
|
+
expect(element).toBeTruthy();
|
|
227
|
+
const style = getComputedStyle(element!);
|
|
228
|
+
expect(isGrey(style.backgroundColor)).toBe(true);
|
|
229
|
+
expect(style.backgroundImage).toBe('none');
|
|
230
|
+
expect(isGrey(style.borderTopColor)).toBe(true);
|
|
231
|
+
expect(Number.parseFloat(style.borderTopWidth)).toBe(1);
|
|
232
|
+
// The count is a quiet number, not a tag.
|
|
233
|
+
expect(element!.querySelector('.reqore-panel-title .reqore-tag')).toBeNull();
|
|
234
|
+
expect(element!.querySelector('.options-readfirst-group-count')).toBeTruthy();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const expectNoUppercase = (selector: string) => {
|
|
238
|
+
const elements = [...document.querySelectorAll<HTMLElement>(selector)];
|
|
239
|
+
expect(elements.length).toBeGreaterThan(0);
|
|
240
|
+
for (const element of elements) {
|
|
241
|
+
expect(getComputedStyle(element).textTransform).not.toBe('uppercase');
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** No status dot on a row that is filled and fine. */
|
|
246
|
+
const expectNoDot = (field: string) => {
|
|
247
|
+
expect(row(field)).toBeTruthy();
|
|
248
|
+
expect(row(field)!.querySelector('.options-readfirst-status-dot')).toBeNull();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/* --------------------------------------------------------------------------
|
|
252
|
+
* Stories
|
|
253
|
+
* ------------------------------------------------------------------------ */
|
|
254
|
+
|
|
255
|
+
export const DefaultLook: Story = {
|
|
256
|
+
parameters: {
|
|
257
|
+
docs: {
|
|
258
|
+
description: {
|
|
259
|
+
story:
|
|
260
|
+
'The default appearance over a filled workflow form, for comparison with **Calm**: tinted Set box, a green Ready and meter, a green dot on every filled row, upper-case group labels and a red/green Yes/No chip.',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
play: async () => {
|
|
265
|
+
await _testsWaitForText('Supplier Payment Release');
|
|
266
|
+
// The default look is untouched: the tinted box and its count tag.
|
|
267
|
+
await waitFor(() => expect(box('set')).toBeTruthy());
|
|
268
|
+
expect(box('set')!.querySelector('.reqore-panel-title .reqore-tag')).toBeTruthy();
|
|
269
|
+
expect(row('display_name')!.querySelector('.options-readfirst-status-dot')).toBeTruthy();
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export const Calm: Story = {
|
|
274
|
+
args: {
|
|
275
|
+
appearance: 'calm',
|
|
276
|
+
},
|
|
277
|
+
parameters: {
|
|
278
|
+
docs: {
|
|
279
|
+
description: {
|
|
280
|
+
story:
|
|
281
|
+
'`appearance="calm"` over the same filled form: the sections are plain panels on the theme’s panel surface with label/value rows split by hairlines, labels grey, group headings in sentence case, the count a quiet number, no dot on a healthy row, No as a word, and a short meter whose done part is grey.',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
play: async () => {
|
|
286
|
+
await _testsWaitForText('Supplier Payment Release');
|
|
287
|
+
await waitFor(() => expect(box('set')).toBeTruthy());
|
|
288
|
+
|
|
289
|
+
// Sections are calm panels — no green tint, no recessed Optional box.
|
|
290
|
+
expectCalmSection(box('set'));
|
|
291
|
+
expectCalmSection(box('optional'));
|
|
292
|
+
expect(getComputedStyle(box('optional')!).backgroundColor).toBe(
|
|
293
|
+
getComputedStyle(box('set')!).backgroundColor
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Rows are split by one full-width hairline each, and there is no gap
|
|
297
|
+
// between them.
|
|
298
|
+
const rows = [...box('set')!.querySelectorAll<HTMLElement>('.readfirst-row')];
|
|
299
|
+
expect(rows.length).toBeGreaterThan(4);
|
|
300
|
+
const second = rows[2];
|
|
301
|
+
expect(Number.parseFloat(getComputedStyle(second).borderTopWidth)).toBe(1);
|
|
302
|
+
|
|
303
|
+
// Sentence case everywhere, grey labels.
|
|
304
|
+
expectNoUppercase('.options-readfirst-group-label');
|
|
305
|
+
expect(document.querySelector('.options-readfirst-group-label')?.textContent).toBe('Info');
|
|
306
|
+
const label = row('display_name')!.querySelector<HTMLElement>('.options-readfirst-label-text')!;
|
|
307
|
+
const value = row('display_name')!.querySelector<HTMLElement>('.options-readfirst-valuetext')!;
|
|
308
|
+
expect(getComputedStyle(label).color).not.toBe(getComputedStyle(value).color);
|
|
309
|
+
|
|
310
|
+
// The section's header label, its group headings and its rows' labels all
|
|
311
|
+
// start on one left edge.
|
|
312
|
+
const headerText = box('set')!
|
|
313
|
+
.querySelector<HTMLElement>('.reqore-panel-title p')!
|
|
314
|
+
.getBoundingClientRect().left;
|
|
315
|
+
expect(Math.abs(headerText - label.getBoundingClientRect().left)).toBeLessThanOrEqual(1);
|
|
316
|
+
const heading = document.querySelector<HTMLElement>('.options-readfirst-group-label')!;
|
|
317
|
+
expect(
|
|
318
|
+
Math.abs(
|
|
319
|
+
heading.getBoundingClientRect().left +
|
|
320
|
+
Number.parseFloat(getComputedStyle(heading).paddingLeft) -
|
|
321
|
+
label.getBoundingClientRect().left
|
|
322
|
+
)
|
|
323
|
+
).toBeLessThanOrEqual(1);
|
|
324
|
+
|
|
325
|
+
// No dot on a filled row, no asterisk on a required one.
|
|
326
|
+
expectNoDot('display_name');
|
|
327
|
+
expectNoDot('version');
|
|
328
|
+
expect(row('display_name')!.querySelectorAll('svg').length).toBe(0);
|
|
329
|
+
|
|
330
|
+
// A note that only informs is not blue.
|
|
331
|
+
const note = row('lang')!.querySelector<HTMLElement>('.options-readfirst-info-panel .reqore-message')!;
|
|
332
|
+
expect(note).toBeTruthy();
|
|
333
|
+
expect(isGrey(getComputedStyle(note).backgroundColor)).toBe(true);
|
|
334
|
+
expect(isGrey(getComputedStyle(note).borderTopColor)).toBe(true);
|
|
335
|
+
|
|
336
|
+
// Yes/No is a word, not a coloured chip.
|
|
337
|
+
expect(row('remote')!.textContent).toContain('No');
|
|
338
|
+
expect(row('remote')!.querySelector('.reqore-tag')).toBeNull();
|
|
339
|
+
|
|
340
|
+
// The readiness line: the word in the text colour, the meter's done part grey.
|
|
341
|
+
const status = document.querySelector<HTMLElement>('.options-readfirst-status')!;
|
|
342
|
+
expect(status.textContent).toBe('Ready');
|
|
343
|
+
expect(isGrey(getComputedStyle(status).color)).toBe(true);
|
|
344
|
+
const bar = document.querySelector<HTMLElement>('.options-readfirst-completion-bar')!;
|
|
345
|
+
expect(isGrey(getComputedStyle(bar, '::before').backgroundColor)).toBe(true);
|
|
346
|
+
expect(bar.getBoundingClientRect().width).toBeLessThanOrEqual(361);
|
|
347
|
+
expect(bar.getBoundingClientRect().height).toBe(4);
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export const CalmNeedsAttention: Story = {
|
|
352
|
+
args: {
|
|
353
|
+
appearance: 'calm',
|
|
354
|
+
value: newFormValue,
|
|
355
|
+
// After a submit attempt: every empty required field is now a fault. A form nobody
|
|
356
|
+
// has been to yet reads "To fill in" instead (CalmNewForm).
|
|
357
|
+
revealRequired: true,
|
|
358
|
+
},
|
|
359
|
+
parameters: {
|
|
360
|
+
docs: {
|
|
361
|
+
description: {
|
|
362
|
+
story:
|
|
363
|
+
'A form after a submit attempt (`revealRequired`) in the calm look: the Needs attention section carries one amber dot and a word, each empty required field says **Required · add a value** in amber where its value would be (no red asterisk, no red message), and only the outstanding part of the meter is coloured. Opening a required field shows the same words under the editor as plain amber text.',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
play: async () => {
|
|
368
|
+
await waitFor(() => expect(box('attention')).toBeTruthy(), { timeout: 10000 });
|
|
369
|
+
expectCalmSection(box('attention'));
|
|
370
|
+
expect(box('attention')!.querySelector('.options-readfirst-group-dot')).toBeTruthy();
|
|
371
|
+
|
|
372
|
+
// The missing values are amber words in the value column.
|
|
373
|
+
await waitFor(() =>
|
|
374
|
+
expect(
|
|
375
|
+
[...document.querySelectorAll('.options-readfirst-valuetext')].filter(
|
|
376
|
+
(element) => element.textContent === CALM_REQUIRED_LABEL
|
|
377
|
+
).length
|
|
378
|
+
).toBe(3)
|
|
379
|
+
);
|
|
380
|
+
await _testsWaitForTextToNotExist('This field is required');
|
|
381
|
+
const word = row('display_name')!.querySelector<HTMLElement>('.options-readfirst-valuetext')!;
|
|
382
|
+
expect(isGrey(getComputedStyle(word).color)).toBe(false);
|
|
383
|
+
expect(getComputedStyle(word).fontStyle).toBe('normal');
|
|
384
|
+
// The amber word is the row's one signal: no dot beside it.
|
|
385
|
+
expectNoDot('display_name');
|
|
386
|
+
|
|
387
|
+
// Only the outstanding part of the readiness line is coloured.
|
|
388
|
+
const status = document.querySelector<HTMLElement>('.options-readfirst-status')!;
|
|
389
|
+
expect(status.textContent).toBe('Incomplete');
|
|
390
|
+
expect(isGrey(getComputedStyle(status).color)).toBe(true);
|
|
391
|
+
const link = document.querySelector<HTMLElement>('.options-readfirst-attention-link')!;
|
|
392
|
+
expect(isGrey(getComputedStyle(link).color)).toBe(false);
|
|
393
|
+
|
|
394
|
+
// Inside an open required field the requirement is plain amber text.
|
|
395
|
+
await _testsClickText('Name');
|
|
396
|
+
await waitFor(() =>
|
|
397
|
+
expect(document.querySelector('.options-field-message')?.textContent).toBe(
|
|
398
|
+
CALM_REQUIRED_LABEL
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
expect(document.querySelector('.options-field-message')?.closest('.reqore-tag')).toBeNull();
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export const CalmChoiceHeading: Story = {
|
|
406
|
+
args: {
|
|
407
|
+
appearance: 'calm',
|
|
408
|
+
},
|
|
409
|
+
parameters: {
|
|
410
|
+
docs: {
|
|
411
|
+
description: {
|
|
412
|
+
story:
|
|
413
|
+
'A choice field opened in the calm look: its three choices are headed by the field’s name as written (“Language:”), not an upper-case micro label.',
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
play: async () => {
|
|
418
|
+
await _testsWaitForText('Supplier Payment Release');
|
|
419
|
+
await _testsClickText('Qore');
|
|
420
|
+
await waitFor(() =>
|
|
421
|
+
expect(document.querySelector('.reqraft-choice-group-heading')).toBeTruthy()
|
|
422
|
+
);
|
|
423
|
+
expectNoUppercase('.reqraft-choice-group-heading');
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
export const CalmReadOnly: Story = {
|
|
428
|
+
args: {
|
|
429
|
+
appearance: 'calm',
|
|
430
|
+
readOnly: true,
|
|
431
|
+
value: { ...filledValue, version: { type: 'string', value: '' } },
|
|
432
|
+
},
|
|
433
|
+
parameters: {
|
|
434
|
+
docs: {
|
|
435
|
+
description: {
|
|
436
|
+
story:
|
|
437
|
+
'The calm look in read mode: one list of label/value rows split by hairlines, flush with the host’s edge and with no card of its own (a read view usually sits in one already); an unset required value reads **Required · not set**, because a reader cannot add it.',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
play: async () => {
|
|
442
|
+
await _testsWaitForText('Supplier Payment Release');
|
|
443
|
+
expect(document.querySelector('.options-readfirst-group')).toBeNull();
|
|
444
|
+
const first = document.querySelector<HTMLElement>('.readfirst-read-list > *')!;
|
|
445
|
+
expect(Number.parseFloat(getComputedStyle(first).borderTopWidth)).toBe(0);
|
|
446
|
+
expect(Number.parseFloat(getComputedStyle(row('display_name')!).borderTopWidth)).toBe(1);
|
|
447
|
+
expect(document.querySelectorAll('.options-readfirst-status-dot').length).toBe(0);
|
|
448
|
+
expect(row('version')!.textContent).toContain(CALM_REQUIRED_READ_LABEL);
|
|
449
|
+
expect(row('remote')!.querySelector('.reqore-tag')).toBeNull();
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
export const CalmNewForm: Story = {
|
|
454
|
+
args: {
|
|
455
|
+
appearance: 'calm',
|
|
456
|
+
value: newFormValue,
|
|
457
|
+
},
|
|
458
|
+
parameters: {
|
|
459
|
+
docs: {
|
|
460
|
+
description: {
|
|
461
|
+
story:
|
|
462
|
+
'A brand-new form in the calm look. A new form is not already wrong: its required fields are still to fill in, so the first section reads **To fill in** with no amber dot, each empty required field says **Required · add a value** in the quiet colour (under its editor too, while it is open for typing), and the readiness line says "3 to fill in". Opening a required field and leaving it empty makes it a fault: the section becomes **Needs attention**, with its amber dot, that field\'s words turn amber, and the readiness line says "1 need attention · 2 to fill in".',
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
play: async () => {
|
|
467
|
+
await waitFor(() => expect(box('attention')).toBeTruthy(), { timeout: 10000 });
|
|
468
|
+
const heading = () => box('attention')!.querySelector('.reqore-panel-title')!.textContent ?? '';
|
|
469
|
+
expect(heading()).toContain('To fill in');
|
|
470
|
+
expect(box('attention')!.querySelector('.options-readfirst-group-dot')).toBeNull();
|
|
471
|
+
const word = () =>
|
|
472
|
+
row('display_name')!.querySelector<HTMLElement>('.options-readfirst-valuetext')!;
|
|
473
|
+
await waitFor(() => expect(word().textContent).toBe(CALM_REQUIRED_LABEL));
|
|
474
|
+
expect(isAmber(getComputedStyle(word()).color)).toBe(false);
|
|
475
|
+
const link = document.querySelector<HTMLElement>('.options-readfirst-attention-link')!;
|
|
476
|
+
expect(link.textContent).toContain('to fill in');
|
|
477
|
+
expect(isAmber(getComputedStyle(link).color)).toBe(false);
|
|
478
|
+
|
|
479
|
+
// Open Name: an open field is not yet a left one, so the line under its editor
|
|
480
|
+
// says what it needs in the quiet colour (a host that opens the first empty field
|
|
481
|
+
// on a new form must not show it as already wrong).
|
|
482
|
+
await _testsClickText('Name');
|
|
483
|
+
const openMessage = await waitFor(() => {
|
|
484
|
+
// An open row is no longer a read-first row, so it is found by its field alone.
|
|
485
|
+
const found = Array.from(
|
|
486
|
+
document.querySelectorAll<HTMLElement>('[data-field="display_name"] .options-field-message')
|
|
487
|
+
).find((el) => el.textContent === CALM_REQUIRED_LABEL);
|
|
488
|
+
expect(found).toBeTruthy();
|
|
489
|
+
return found!;
|
|
490
|
+
});
|
|
491
|
+
expect(isAmber(getComputedStyle(openMessage).color)).toBe(false);
|
|
492
|
+
expect(heading()).toContain('To fill in');
|
|
493
|
+
|
|
494
|
+
// Then open another field: Name has been visited and left empty.
|
|
495
|
+
await _testsClickText('Version');
|
|
496
|
+
await waitFor(() => expect(heading()).toContain('Needs attention'));
|
|
497
|
+
// Only the visited field is a fault; the other two are still to fill in.
|
|
498
|
+
const line = document.querySelector<HTMLElement>('.options-readfirst-completion')!.textContent;
|
|
499
|
+
expect(line).toContain('1 need attention');
|
|
500
|
+
expect(line).toContain('2 to fill in');
|
|
501
|
+
expect(box('attention')!.querySelector('.options-readfirst-group-dot')).toBeTruthy();
|
|
502
|
+
await waitFor(() => expect(isAmber(getComputedStyle(word()).color)).toBe(true));
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
export const CalmOnAPhone: Story = {
|
|
507
|
+
args: {
|
|
508
|
+
appearance: 'calm',
|
|
509
|
+
value: newFormValue,
|
|
510
|
+
},
|
|
511
|
+
parameters: {
|
|
512
|
+
qlip: { viewport: { width: 390, height: 844 } },
|
|
513
|
+
docs: {
|
|
514
|
+
description: {
|
|
515
|
+
story:
|
|
516
|
+
'The calm look on a 390px phone: the readiness meter wraps under its words, each row stacks its value under its name inside the section, and nothing scrolls sideways.',
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
play: async ({ canvasElement }) => {
|
|
521
|
+
await _testsWithViewport(390, 844, async (narrowed) => {
|
|
522
|
+
await waitFor(() => expect(box('attention')).toBeTruthy(), { timeout: 10000 });
|
|
523
|
+
if (!narrowed) return;
|
|
524
|
+
await waitFor(() =>
|
|
525
|
+
expect(document.querySelector('.readfirst-narrow')).toBeTruthy()
|
|
526
|
+
);
|
|
527
|
+
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(
|
|
528
|
+
document.documentElement.clientWidth
|
|
529
|
+
);
|
|
530
|
+
const section = box('attention')!.getBoundingClientRect();
|
|
531
|
+
for (const element of canvasElement.querySelectorAll<HTMLElement>(
|
|
532
|
+
'.options-readfirst-group .readfirst-row'
|
|
533
|
+
)) {
|
|
534
|
+
const rect = element.getBoundingClientRect();
|
|
535
|
+
expect(rect.left).toBeGreaterThanOrEqual(section.left - 1);
|
|
536
|
+
expect(rect.right).toBeLessThanOrEqual(section.right + 1);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
},
|
|
540
|
+
};
|
|
@@ -122,8 +122,12 @@ const scroller = () => document.getElementById(SCROLLER) as HTMLElement;
|
|
|
122
122
|
|
|
123
123
|
/** One box's header element, found by the label it carries. */
|
|
124
124
|
const boxHeader = (label: string): HTMLElement => {
|
|
125
|
+
// The first box is "To fill in" until something in it is a fault (a new form).
|
|
126
|
+
const labels = label === 'Needs attention' ? ['Needs attention', 'To fill in'] : [label];
|
|
125
127
|
const box = Array.from(document.querySelectorAll('.options-readfirst-group')).find((el) =>
|
|
126
|
-
|
|
128
|
+
labels.some((name) =>
|
|
129
|
+
(el.querySelector(':scope > .reqore-panel-title')?.textContent ?? '').includes(name)
|
|
130
|
+
)
|
|
127
131
|
);
|
|
128
132
|
if (!box) {
|
|
129
133
|
throw new Error(`no status box labelled ${label}`);
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ReqoreControlGroup,
|
|
3
|
+
ReqoreP,
|
|
4
|
+
ReqoreTag,
|
|
5
|
+
ReqoreVerticalSpacer,
|
|
6
|
+
useReqoreTheme,
|
|
7
|
+
} from '@qoretechnologies/reqore';
|
|
2
8
|
import { IReqoreTagProps } from '@qoretechnologies/reqore/dist/components/Tag';
|
|
3
9
|
import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
|
|
10
|
+
import { getReadableColor } from '@qoretechnologies/reqore/dist/helpers/colors';
|
|
4
11
|
import { IQorusFormField, IQorusFormSchema, TQorusForm } from '@qoretechnologies/ts-toolkit';
|
|
5
12
|
import { isArray, size } from 'lodash';
|
|
6
13
|
import { useMemo } from 'react';
|
|
@@ -19,6 +26,7 @@ import {
|
|
|
19
26
|
unsatisfiedRequiredGroups,
|
|
20
27
|
} from '../../../helpers/validations';
|
|
21
28
|
import { OptionsContext } from './optionsContext';
|
|
29
|
+
import { CALM_REQUIRED_LABEL, CALM_REQUIRED_READ_LABEL, useIsCalmForm } from './formAppearance';
|
|
22
30
|
|
|
23
31
|
/** A `messages` entry that appears only for certain values of its siblings. */
|
|
24
32
|
export interface IConditionalFieldMessage {
|
|
@@ -335,6 +343,22 @@ export interface IOptionFieldMessagesProps {
|
|
|
335
343
|
* reqraft's built-in editors are known.
|
|
336
344
|
*/
|
|
337
345
|
isRendererOnly?: (type?: any) => boolean;
|
|
346
|
+
/**
|
|
347
|
+
* The calm appearance says an empty required field the calm way: an amber
|
|
348
|
+
* "Required · add a value" (or, read-only, "Required · not set") rather than
|
|
349
|
+
* a red "This field is required". Waiting for a person is amber; red is for
|
|
350
|
+
* something that failed.
|
|
351
|
+
*/
|
|
352
|
+
calm?: boolean;
|
|
353
|
+
/** Read-only: the reader cannot add a value, so the calm wording only states it. */
|
|
354
|
+
readOnly?: boolean;
|
|
355
|
+
/**
|
|
356
|
+
* Nobody has been to this field yet. The requirement is still stated, but with no
|
|
357
|
+
* intent: a new form is not already wrong, and saying what is required is not saying
|
|
358
|
+
* something failed. It takes its colour once the person has opened the field and left
|
|
359
|
+
* it empty (`FormEngine`'s `visitedOptions` / `revealRequired`).
|
|
360
|
+
*/
|
|
361
|
+
requirementPending?: boolean;
|
|
338
362
|
}
|
|
339
363
|
|
|
340
364
|
/**
|
|
@@ -349,6 +373,9 @@ export const getOptionFieldMessages = ({
|
|
|
349
373
|
allOptions,
|
|
350
374
|
getType,
|
|
351
375
|
isRendererOnly,
|
|
376
|
+
calm,
|
|
377
|
+
readOnly,
|
|
378
|
+
requirementPending,
|
|
352
379
|
}: IOptionFieldMessagesProps): IReqoreTagProps[] => {
|
|
353
380
|
const optionSchema = schema[name];
|
|
354
381
|
const result: IReqoreTagProps[] = [];
|
|
@@ -394,7 +421,13 @@ export const getOptionFieldMessages = ({
|
|
|
394
421
|
// locked field is not asked for: the dependency message below says what
|
|
395
422
|
// unlocks it, and until then there is nothing to fill in.
|
|
396
423
|
if (optionSchema?.required && !locked) {
|
|
397
|
-
|
|
424
|
+
const message =
|
|
425
|
+
calm ?
|
|
426
|
+
{ label: readOnly ? CALM_REQUIRED_READ_LABEL : CALM_REQUIRED_LABEL, intent: 'warning' }
|
|
427
|
+
: { label: 'This field is required', intent: 'danger' };
|
|
428
|
+
result.push(
|
|
429
|
+
requirementPending ? { label: message.label } : (message as IReqoreTagProps)
|
|
430
|
+
);
|
|
398
431
|
}
|
|
399
432
|
|
|
400
433
|
if (
|
|
@@ -440,7 +473,10 @@ export const OptionFieldMessages = ({
|
|
|
440
473
|
allOptions,
|
|
441
474
|
getType,
|
|
442
475
|
isRendererOnly,
|
|
476
|
+
requirementPending,
|
|
443
477
|
}: IOptionFieldMessagesProps) => {
|
|
478
|
+
const calm = useIsCalmForm();
|
|
479
|
+
const theme = useReqoreTheme();
|
|
444
480
|
const messages: IReqoreTagProps[] = useMemo(
|
|
445
481
|
() =>
|
|
446
482
|
getOptionFieldMessages({
|
|
@@ -450,6 +486,8 @@ export const OptionFieldMessages = ({
|
|
|
450
486
|
allOptions,
|
|
451
487
|
getType,
|
|
452
488
|
isRendererOnly,
|
|
489
|
+
calm,
|
|
490
|
+
requirementPending,
|
|
453
491
|
}),
|
|
454
492
|
[
|
|
455
493
|
JSON.stringify(schema),
|
|
@@ -457,6 +495,8 @@ export const OptionFieldMessages = ({
|
|
|
457
495
|
JSON.stringify(allOptions),
|
|
458
496
|
name,
|
|
459
497
|
isRendererOnly,
|
|
498
|
+
calm,
|
|
499
|
+
requirementPending,
|
|
460
500
|
]
|
|
461
501
|
);
|
|
462
502
|
|
|
@@ -464,6 +504,37 @@ export const OptionFieldMessages = ({
|
|
|
464
504
|
return null;
|
|
465
505
|
}
|
|
466
506
|
|
|
507
|
+
// Calm: each message is a line of text in its own colour under the editor —
|
|
508
|
+
// a word, not a tinted chip with a warning glyph.
|
|
509
|
+
if (calm) {
|
|
510
|
+
return (
|
|
511
|
+
<ReqoreControlGroup
|
|
512
|
+
className='options-field-messages'
|
|
513
|
+
vertical
|
|
514
|
+
gapSize='tiny'
|
|
515
|
+
style={{ marginTop: 6 }}
|
|
516
|
+
>
|
|
517
|
+
{messages.map((message, index) => (
|
|
518
|
+
<ReqoreP
|
|
519
|
+
key={index}
|
|
520
|
+
size='small'
|
|
521
|
+
intent={message.intent}
|
|
522
|
+
// A requirement nobody has been asked about yet carries no intent: it is
|
|
523
|
+
// said in the same faint colour as the row's pending word, not in full text.
|
|
524
|
+
effect={
|
|
525
|
+
message.intent ? undefined : (
|
|
526
|
+
{ color: `${getReadableColor(theme, undefined, undefined, true)}a6` }
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
className='options-field-message'
|
|
530
|
+
>
|
|
531
|
+
{message.label}
|
|
532
|
+
</ReqoreP>
|
|
533
|
+
))}
|
|
534
|
+
</ReqoreControlGroup>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
467
538
|
return (
|
|
468
539
|
<>
|
|
469
540
|
<ReqoreVerticalSpacer height={5} />
|