@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.
@@ -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 (its badge reads 2), and the field can be typed into meanwhile without being asked for a data type.',
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
- /** `noSoft` hides the `soft*` variants from the picker (9 instead of 14). */
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 with noSoft enabled. The picker for the type the value resolved to leaves out the soft* variants, so its badge reads 9 instead of 14.',
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 ("string") is shown and the picker still exposes all 14 types so the operator can switch away from auto without the field snapping back.',
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: async () => {
509
- await waitForText('string');
510
- await waitForText('14');
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
- export const DefaultNoSoftTypes = [
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
- noSoft,
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 [currentInternalType, setInternalType] = useState<IQorusType>(
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 && currentInternalType !== 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
- const types =
1059
- allowedTypes ||
1060
- (!noSoft
1061
- ? [
1062
- { value: 'bool' },
1063
- { value: 'softbool' },
1064
- { value: 'date' },
1065
- { value: 'string' },
1066
- { value: 'softstring' },
1067
- { value: 'binary' },
1068
- { value: 'float' },
1069
- { value: 'softfloat' },
1070
- { value: 'list' },
1071
- { value: 'softlist' },
1072
- { value: 'hash' },
1073
- { value: 'int' },
1074
- { value: 'softint' },
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);