@qoretechnologies/reqraft 0.10.60-pr.131.gdcd7bc5 → 0.10.60-pr.131.gfcd7fef
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/FORM_ENGINE_FIELD_MIGRATION.md +10 -4
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +1 -1
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +1 -1
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +30 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +52 -27
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/FormEngine.tsx +0 -1
- package/src/components/form/expressions/builder/index.tsx +0 -1
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +153 -11
- package/src/components/form/fields/auto/AutoFormField.tsx +57 -26
|
@@ -52,6 +52,94 @@ const waitForText = async (text: string | RegExp) => {
|
|
|
52
52
|
await waitFor(() => expect(body.queryAllByText(text)[0]).toBeTruthy(), { timeout: 10000 });
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
/** The readable names of the types an untyped field's picker offers. */
|
|
56
|
+
const IMMEDIATE_TYPE_NAMES = [
|
|
57
|
+
'True/False',
|
|
58
|
+
'Date',
|
|
59
|
+
'Text',
|
|
60
|
+
'Binary',
|
|
61
|
+
'Decimal',
|
|
62
|
+
'List',
|
|
63
|
+
'Key/Value {}',
|
|
64
|
+
'Integer',
|
|
65
|
+
'RGB Color',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Opens the type picker and returns the open list. Asserts the closed trigger
|
|
70
|
+
* reads `label` (the chosen type's readable name) when one is given, and that
|
|
71
|
+
* it carries no item count: a count on a fixed list of types is noise.
|
|
72
|
+
*/
|
|
73
|
+
const openTypePicker = async (canvasElement: HTMLElement, label?: string) => {
|
|
74
|
+
const trigger = await waitFor(
|
|
75
|
+
() => {
|
|
76
|
+
const button = canvasElement.querySelector(
|
|
77
|
+
'.auto-field-group [aria-haspopup]'
|
|
78
|
+
) as HTMLElement | null;
|
|
79
|
+
expect(button).toBeTruthy();
|
|
80
|
+
return button!;
|
|
81
|
+
},
|
|
82
|
+
{ timeout: 10000 }
|
|
83
|
+
);
|
|
84
|
+
if (label) {
|
|
85
|
+
await waitFor(() => expect(trigger.textContent).toContain(label), { timeout: 10000 });
|
|
86
|
+
}
|
|
87
|
+
await expect(trigger.querySelector('.reqore-button-badge')).toBeNull();
|
|
88
|
+
await userEvent.click(trigger);
|
|
89
|
+
return waitFor(
|
|
90
|
+
() => {
|
|
91
|
+
const el = document.querySelector('.reqore-popover-content') as HTMLElement | null;
|
|
92
|
+
expect(el).toBeTruthy();
|
|
93
|
+
return el!;
|
|
94
|
+
},
|
|
95
|
+
{ timeout: 10000 }
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Closes the open type picker the way an author does — a click outside the
|
|
101
|
+
* list (reqore's popover closes on an outside click; Escape could discard an
|
|
102
|
+
* edit) — and waits until the list is gone.
|
|
103
|
+
*/
|
|
104
|
+
const closeTypePicker = async (popover: HTMLElement) => {
|
|
105
|
+
await userEvent.click(document.body);
|
|
106
|
+
await waitFor(
|
|
107
|
+
() => {
|
|
108
|
+
expect(popover.isConnected).toBe(false);
|
|
109
|
+
expect(document.querySelector('.reqore-popover-content')).toBeNull();
|
|
110
|
+
},
|
|
111
|
+
{ timeout: 10000 }
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Nothing (an open list) covers the control: it is on top at its centre and
|
|
117
|
+
* across its width — a full-width editor's centre is clear of a narrow list
|
|
118
|
+
* anchored at its left, so the centre alone would not show it.
|
|
119
|
+
*/
|
|
120
|
+
const expectUncovered = async (control: HTMLElement) => {
|
|
121
|
+
const box = control.getBoundingClientRect();
|
|
122
|
+
for (const fraction of [0.1, 0.3, 0.5, 0.7, 0.9]) {
|
|
123
|
+
const hit = document.elementFromPoint(
|
|
124
|
+
box.left + box.width * fraction,
|
|
125
|
+
box.top + box.height / 2
|
|
126
|
+
);
|
|
127
|
+
await expect(hit !== null && (hit === control || control.contains(hit))).toBe(true);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const expectOffered = async (popover: HTMLElement, labels: string[]) => {
|
|
132
|
+
await waitFor(
|
|
133
|
+
() => {
|
|
134
|
+
for (const label of labels) {
|
|
135
|
+
expect(within(popover).getAllByText(label, { exact: true }).length).toBeGreaterThan(0);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{ timeout: 10000 }
|
|
139
|
+
);
|
|
140
|
+
await expect(within(popover).queryByText(/^soft/)).not.toBeInTheDocument();
|
|
141
|
+
};
|
|
142
|
+
|
|
55
143
|
// --- reqraft behavioral stories (kept from the field-migration batch) -------
|
|
56
144
|
|
|
57
145
|
/** Untyped, no value — the field opens on an editor to type into, never on a type question. */
|
|
@@ -193,21 +281,21 @@ export const AllowedTypesSubset: Story = {
|
|
|
193
281
|
docs: {
|
|
194
282
|
description: {
|
|
195
283
|
story:
|
|
196
|
-
'Renders AutoFormField with allowedTypes restricted to Integer and Text. The type picker offers those 2 types
|
|
284
|
+
'Renders AutoFormField with allowedTypes restricted to Integer and Text. The type picker offers exactly those 2 types, with no item count, and the field can be typed into meanwhile without being asked for a data type.',
|
|
197
285
|
},
|
|
198
286
|
},
|
|
199
287
|
},
|
|
200
288
|
async play({ canvasElement }) {
|
|
201
|
-
// The picker badge reflects the restricted list — 2 instead of 14.
|
|
202
|
-
await waitForText('2');
|
|
203
289
|
await expect(within(canvasElement).queryByText('Please select data type')).not.toBeInTheDocument();
|
|
290
|
+
const popover = await openTypePicker(canvasElement);
|
|
291
|
+
await expectOffered(popover, ['Integer', 'Text']);
|
|
292
|
+
await expect(within(popover).queryByText('Decimal')).not.toBeInTheDocument();
|
|
204
293
|
},
|
|
205
294
|
};
|
|
206
295
|
|
|
207
|
-
/**
|
|
296
|
+
/** The picker offers no `soft*` type: a typed-in literal has nothing to convert. */
|
|
208
297
|
export const NoSoftTypes: Story = {
|
|
209
298
|
args: {
|
|
210
|
-
noSoft: true,
|
|
211
299
|
/* The picker belongs to a value with a type: an untyped field that is
|
|
212
300
|
empty is typed into, and one holding text has resolved to `string`. */
|
|
213
301
|
defaultType: 'auto',
|
|
@@ -217,13 +305,14 @@ export const NoSoftTypes: Story = {
|
|
|
217
305
|
docs: {
|
|
218
306
|
description: {
|
|
219
307
|
story:
|
|
220
|
-
'Renders an untyped AutoFormField holding a text value
|
|
308
|
+
'Renders an untyped AutoFormField holding a text value. The type picker, chosen as Text, offers the 9 base types by readable name (True/False, Date, Text, Binary, Decimal, List, Key/Value {}, Integer, RGB Color) and no soft type, and shows no item count.',
|
|
221
309
|
},
|
|
222
310
|
},
|
|
223
311
|
},
|
|
224
312
|
async play({ canvasElement }) {
|
|
225
|
-
await waitForText('9');
|
|
226
313
|
await expect(within(canvasElement).queryByText('Please select data type')).not.toBeInTheDocument();
|
|
314
|
+
const popover = await openTypePicker(canvasElement, 'Text');
|
|
315
|
+
await expectOffered(popover, IMMEDIATE_TYPE_NAMES);
|
|
227
316
|
},
|
|
228
317
|
};
|
|
229
318
|
|
|
@@ -501,13 +590,66 @@ export const AutoDoesNotChangeTypeBackToAuto: Story = {
|
|
|
501
590
|
docs: {
|
|
502
591
|
description: {
|
|
503
592
|
story:
|
|
504
|
-
'Renders AutoFormField in auto mode with a string value — the inferred type
|
|
593
|
+
'Renders AutoFormField in auto mode with a string value — the inferred type is shown as Text and the picker still offers every base type so the operator can switch away from auto without the field snapping back.',
|
|
505
594
|
},
|
|
506
595
|
},
|
|
507
596
|
},
|
|
508
|
-
play
|
|
509
|
-
await
|
|
510
|
-
await
|
|
597
|
+
async play({ canvasElement }) {
|
|
598
|
+
const popover = await openTypePicker(canvasElement, 'Text');
|
|
599
|
+
await expectOffered(popover, IMMEDIATE_TYPE_NAMES);
|
|
600
|
+
// End on what the story is about: the text value, still in its editor.
|
|
601
|
+
await closeTypePicker(popover);
|
|
602
|
+
const editor = await within(canvasElement).findByDisplayValue('test');
|
|
603
|
+
await expectUncovered(editor);
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/** A soft type in a caller's allowed types is offered once, as its base type. */
|
|
608
|
+
export const AllowedSoftTypesOfferedAsTheirBase: Story = {
|
|
609
|
+
args: {
|
|
610
|
+
allowedTypes: [{ name: 'softint' }, { name: 'int' }, { name: 'softstring' }],
|
|
611
|
+
},
|
|
612
|
+
parameters: {
|
|
613
|
+
docs: {
|
|
614
|
+
description: {
|
|
615
|
+
story:
|
|
616
|
+
'Renders AutoFormField whose allowedTypes are softint, int and softstring. The type picker offers Integer once and Text, with no soft type and no item count.',
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
async play({ canvasElement }) {
|
|
621
|
+
const popover = await openTypePicker(canvasElement);
|
|
622
|
+
await expectOffered(popover, ['Integer', 'Text']);
|
|
623
|
+
await expect(within(popover).getAllByText('Integer', { exact: true })).toHaveLength(1);
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
/** A value saved with a soft type is edited with its base type's editor. */
|
|
628
|
+
export const SavedSoftTypeEditsAsItsBase: Story = {
|
|
629
|
+
args: {
|
|
630
|
+
defaultType: 'auto',
|
|
631
|
+
defaultInternalType: 'softint',
|
|
632
|
+
value: 5,
|
|
633
|
+
},
|
|
634
|
+
parameters: {
|
|
635
|
+
docs: {
|
|
636
|
+
description: {
|
|
637
|
+
story:
|
|
638
|
+
'Renders an untyped AutoFormField holding 5, saved with the soft type softint. It is edited with the integer editor and the type picker reads Integer, instead of drawing "Unknown type!".',
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
async play({ canvasElement }) {
|
|
643
|
+
const canvas = within(canvasElement);
|
|
644
|
+
await expect(await canvas.findByDisplayValue('5')).toBeInTheDocument();
|
|
645
|
+
await expect(canvas.queryByText('Unknown type!')).not.toBeInTheDocument();
|
|
646
|
+
const popover = await openTypePicker(canvasElement, 'Integer');
|
|
647
|
+
await expectOffered(popover, IMMEDIATE_TYPE_NAMES);
|
|
648
|
+
// End on what the story is about: the integer editor, holding 5.
|
|
649
|
+
await closeTypePicker(popover);
|
|
650
|
+
const input = await canvas.findByDisplayValue('5');
|
|
651
|
+
await expect(input.tagName).toBe('INPUT');
|
|
652
|
+
await expectUncovered(input);
|
|
511
653
|
},
|
|
512
654
|
};
|
|
513
655
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
13
13
|
import { IWithReqoreSize } from '@qoretechnologies/reqore/dist/types/global';
|
|
14
14
|
import { TQorusFormFieldSchema, TQorusType } from '@qoretechnologies/ts-toolkit';
|
|
15
|
-
import { isEqual, size } from 'lodash';
|
|
15
|
+
import { isEqual, size, uniqBy } from 'lodash';
|
|
16
16
|
import React, { memo, useCallback, useEffect, useState } from 'react';
|
|
17
17
|
import { useUpdateEffect } from 'react-use';
|
|
18
18
|
import useMount from 'react-use/lib/useMount';
|
|
@@ -97,6 +97,10 @@ export interface IAutoFieldProps
|
|
|
97
97
|
level?: number;
|
|
98
98
|
defaultType?: IQorusType;
|
|
99
99
|
defaultInternalType?: IQorusType;
|
|
100
|
+
/**
|
|
101
|
+
* @deprecated Inert: the type picker never offers a soft type (see
|
|
102
|
+
* `getImmediateValueTypes`). Kept so existing callers still compile.
|
|
103
|
+
*/
|
|
100
104
|
noSoft?: boolean;
|
|
101
105
|
|
|
102
106
|
/** SEAM: inert in reqraft — saved-values storage is IDE-only. */
|
|
@@ -134,7 +138,17 @@ export interface IAutoFieldProps
|
|
|
134
138
|
[key: string]: any;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
/**
|
|
142
|
+
* The types the picker offers for a value typed into this field.
|
|
143
|
+
*
|
|
144
|
+
* No Qore soft type (`softint`, `softstring`, `softlist`, …) is among them. A
|
|
145
|
+
* soft type is a DECLARATION: it says that whatever a variable or parameter is
|
|
146
|
+
* given is converted to the base type. A value typed in here is already a
|
|
147
|
+
* literal of the type its editor makes, so there is nothing to convert and no
|
|
148
|
+
* editor of its own to draw — the picker used to offer them anyway, and each
|
|
149
|
+
* one drew "Unknown type!".
|
|
150
|
+
*/
|
|
151
|
+
export const getImmediateValueTypes = (): { name: IQorusType; display_name: string }[] => [
|
|
138
152
|
{ name: 'bool', display_name: 'True/False' },
|
|
139
153
|
{ name: 'date', display_name: 'Date' },
|
|
140
154
|
{ name: 'string', display_name: 'Text' },
|
|
@@ -146,6 +160,21 @@ export const DefaultNoSoftTypes = [
|
|
|
146
160
|
{ name: 'rgbcolor', display_name: 'RGB Color' },
|
|
147
161
|
];
|
|
148
162
|
|
|
163
|
+
/** @deprecated Use `getImmediateValueTypes()`; every picker now offers this list. */
|
|
164
|
+
export const DefaultNoSoftTypes = getImmediateValueTypes();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The type a value is EDITED as: a soft type is edited as its base type.
|
|
168
|
+
*
|
|
169
|
+
* A soft type still reaches this field from data saved before the picker
|
|
170
|
+
* stopped offering them, and from a declared type the server sends (an option
|
|
171
|
+
* or a config item declared `softint`). It is shown and edited as the base
|
|
172
|
+
* type it converts to, so that value keeps its editor; the declared type
|
|
173
|
+
* itself is left as it is and is what the field reports back.
|
|
174
|
+
*/
|
|
175
|
+
export const editorTypeOf = <T extends string | undefined | null>(type: T): T =>
|
|
176
|
+
(typeof type === 'string' && /^soft[a-z]/.test(type) ? type.slice(4) : type) as T;
|
|
177
|
+
|
|
149
178
|
function AutoField<T = any>({
|
|
150
179
|
name,
|
|
151
180
|
onChange,
|
|
@@ -155,7 +184,8 @@ function AutoField<T = any>({
|
|
|
155
184
|
defaultInternalType,
|
|
156
185
|
requestFieldData,
|
|
157
186
|
type,
|
|
158
|
-
|
|
187
|
+
// Inert (deprecated); destructured so it does not land in `...rest`.
|
|
188
|
+
noSoft, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
159
189
|
path,
|
|
160
190
|
arg_schema,
|
|
161
191
|
compact,
|
|
@@ -185,9 +215,11 @@ function AutoField<T = any>({
|
|
|
185
215
|
...rest
|
|
186
216
|
}: IAutoFieldProps & T) {
|
|
187
217
|
const [currentType, setType] = useState<IQorusType>(defaultInternalType || null);
|
|
188
|
-
const [
|
|
218
|
+
const [storedInternalType, setInternalType] = useState<IQorusType>(
|
|
189
219
|
defaultInternalType || 'any'
|
|
190
220
|
);
|
|
221
|
+
// What the value is edited as; see `editorTypeOf`.
|
|
222
|
+
const currentInternalType = editorTypeOf(storedInternalType);
|
|
191
223
|
const [isSetToNull, setIsSetToNull] = useState<boolean>(false);
|
|
192
224
|
const [finalArgSchema, setFinalArgSchema] = useState<IOptionsSchema>(
|
|
193
225
|
typeof arg_schema === 'string' ? undefined : arg_schema
|
|
@@ -203,7 +235,6 @@ function AutoField<T = any>({
|
|
|
203
235
|
defaultInternalType,
|
|
204
236
|
requestFieldData,
|
|
205
237
|
type,
|
|
206
|
-
noSoft,
|
|
207
238
|
path,
|
|
208
239
|
arg_schema,
|
|
209
240
|
column,
|
|
@@ -288,7 +319,7 @@ function AutoField<T = any>({
|
|
|
288
319
|
});
|
|
289
320
|
|
|
290
321
|
useUpdateEffect(() => {
|
|
291
|
-
if (defaultType &&
|
|
322
|
+
if (defaultType && storedInternalType !== defaultType) {
|
|
292
323
|
setType(defaultType);
|
|
293
324
|
setInternalType(defaultType);
|
|
294
325
|
}
|
|
@@ -1055,26 +1086,23 @@ function AutoField<T = any>({
|
|
|
1055
1086
|
currentType === 'auto' ||
|
|
1056
1087
|
currentType === 'any'));
|
|
1057
1088
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
{
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
{ value: 'rgbcolor' },
|
|
1076
|
-
]
|
|
1077
|
-
: DefaultNoSoftTypes);
|
|
1089
|
+
// A caller's own list is offered as the editors it names, so a soft type in
|
|
1090
|
+
// it is offered once, as its base type.
|
|
1091
|
+
const types = allowedTypes
|
|
1092
|
+
? uniqBy(
|
|
1093
|
+
allowedTypes.map((allowed) => {
|
|
1094
|
+
const name = editorTypeOf(allowed.name);
|
|
1095
|
+
return {
|
|
1096
|
+
...allowed,
|
|
1097
|
+
name,
|
|
1098
|
+
display_name:
|
|
1099
|
+
allowed.display_name ||
|
|
1100
|
+
getImmediateValueTypes().find((type) => type.name === name)?.display_name,
|
|
1101
|
+
};
|
|
1102
|
+
}),
|
|
1103
|
+
'name'
|
|
1104
|
+
)
|
|
1105
|
+
: getImmediateValueTypes();
|
|
1078
1106
|
|
|
1079
1107
|
if (error) {
|
|
1080
1108
|
return <ReqoreMessage intent='danger'>{error}</ReqoreMessage>;
|
|
@@ -1102,6 +1130,9 @@ function AutoField<T = any>({
|
|
|
1102
1130
|
minimal={rest.minimal}
|
|
1103
1131
|
size={rest.size}
|
|
1104
1132
|
items={types as ISelectFormFieldItem[]}
|
|
1133
|
+
/* A count on a fixed list of types says nothing the open list
|
|
1134
|
+
does not; the IDE's picker shows none either. */
|
|
1135
|
+
hideItemCount
|
|
1105
1136
|
value={currentInternalType}
|
|
1106
1137
|
onChange={(value) => {
|
|
1107
1138
|
handleTypeChange(name, value as IQorusType);
|