@qoretechnologies/reqraft 0.10.60-pr.134.g36530dc → 0.10.60

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.
Files changed (72) hide show
  1. package/design/FORM_ENGINE_FIELD_MIGRATION.md +10 -4
  2. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  3. package/dist/components/dpqlEditor/DpqlEditor.js +2 -1
  4. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  5. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  6. package/dist/components/dpqlEditor/dpqlHelpers.js +8 -1
  7. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  8. package/dist/components/dpqlEditor/index.d.ts +1 -1
  9. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  10. package/dist/components/dpqlEditor/index.js.map +1 -1
  11. package/dist/components/dpqlEditor/types.d.ts +12 -0
  12. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  13. package/dist/components/dpqlEditor/useDpqlSession.d.ts +3 -1
  14. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  15. package/dist/components/dpqlEditor/useDpqlSession.js +9 -9
  16. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  17. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  18. package/dist/components/form/engine/FormEngine.js +16 -2
  19. package/dist/components/form/engine/FormEngine.js.map +1 -1
  20. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  21. package/dist/components/form/engine/OptionFieldMessages.js +2 -1
  22. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  23. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  24. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  25. package/dist/components/form/expressions/ExpressionField.js +2 -2
  26. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  27. package/dist/components/form/expressions/builder/argumentWrapper.d.ts.map +1 -1
  28. package/dist/components/form/expressions/builder/argumentWrapper.js +23 -1
  29. package/dist/components/form/expressions/builder/argumentWrapper.js.map +1 -1
  30. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  31. package/dist/components/form/expressions/builder/index.js +18 -4
  32. package/dist/components/form/expressions/builder/index.js.map +1 -1
  33. package/dist/components/form/fields/auto/AutoFormField.d.ts +30 -1
  34. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  35. package/dist/components/form/fields/auto/AutoFormField.js +52 -27
  36. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  37. package/dist/components/form/fields/number/Number.d.ts +7 -0
  38. package/dist/components/form/fields/number/Number.d.ts.map +1 -1
  39. package/dist/components/form/fields/number/Number.js +36 -17
  40. package/dist/components/form/fields/number/Number.js.map +1 -1
  41. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  42. package/dist/components/form/fields/template/TemplateField.js +3 -1
  43. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  44. package/dist/helpers/validations.d.ts +10 -0
  45. package/dist/helpers/validations.d.ts.map +1 -1
  46. package/dist/helpers/validations.js +16 -7
  47. package/dist/helpers/validations.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +2 -1
  51. package/dist/index.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/components/dpqlEditor/DpqlEditor.tsx +2 -0
  54. package/src/components/dpqlEditor/dpqlHelpers.ts +9 -2
  55. package/src/components/dpqlEditor/index.ts +1 -0
  56. package/src/components/dpqlEditor/types.ts +10 -0
  57. package/src/components/dpqlEditor/useDpqlSession.ts +11 -2
  58. package/src/components/form/engine/FormEngine.stories.tsx +22 -2
  59. package/src/components/form/engine/FormEngine.tsx +15 -2
  60. package/src/components/form/engine/OptionFieldMessages.tsx +3 -1
  61. package/src/components/form/expressions/ExpressionField.tsx +8 -0
  62. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +172 -0
  63. package/src/components/form/expressions/builder/argumentWrapper.tsx +26 -3
  64. package/src/components/form/expressions/builder/index.tsx +17 -4
  65. package/src/components/form/expressions/dpqlMockLanguage.ts +3 -2
  66. package/src/components/form/fields/auto/AutoFormField.stories.tsx +154 -12
  67. package/src/components/form/fields/auto/AutoFormField.tsx +57 -26
  68. package/src/components/form/fields/number/Number.stories.tsx +36 -9
  69. package/src/components/form/fields/number/Number.tsx +39 -32
  70. package/src/components/form/fields/template/TemplateField.tsx +2 -0
  71. package/src/helpers/validations.ts +25 -13
  72. package/src/index.tsx +1 -0
@@ -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);
@@ -51,12 +51,14 @@ export const Integer: Story = {
51
51
  const input = canvas.getByLabelText('Number');
52
52
 
53
53
  await expect(input).toBeInTheDocument();
54
- await expect(input).toHaveValue(42);
55
- await expect(input).toHaveAttribute('type', 'number');
54
+ await expect(input).toHaveValue('42');
55
+ // typed freely, on a numeric keyboard
56
+ await expect(input).toHaveAttribute('type', 'text');
57
+ await expect(input).toHaveAttribute('inputmode', 'numeric');
56
58
 
57
59
  await userEvent.clear(input);
58
60
  await userEvent.type(input, '10');
59
- await expect(input).toHaveValue(10);
61
+ await expect(input).toHaveValue('10');
60
62
  await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith(10), { timeout: 500 });
61
63
  },
62
64
  };
@@ -70,7 +72,7 @@ export const Float: Story = {
70
72
  docs: {
71
73
  description: {
72
74
  story:
73
- 'Renders the Number field in float mode with 3.14 pre-populated — the input uses a 0.1 step and accepts decimals.',
75
+ 'Renders the Number field in float mode with 3.14 pre-populated — the input asks for a decimal keyboard and accepts decimals.',
74
76
  },
75
77
  },
76
78
  },
@@ -78,12 +80,12 @@ export const Float: Story = {
78
80
  const canvas = within(canvasElement);
79
81
  const input = canvas.getByLabelText('Number');
80
82
 
81
- await expect(input).toHaveValue(3.14);
82
- await expect(input).toHaveAttribute('step', '0.1');
83
+ await expect(input).toHaveValue('3.14');
84
+ await expect(input).toHaveAttribute('inputmode', 'decimal');
83
85
 
84
86
  await userEvent.clear(input);
85
87
  await userEvent.type(input, '10.9');
86
- await expect(input).toHaveValue(10.9);
88
+ await expect(input).toHaveValue('10.9');
87
89
  await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith(10.9), { timeout: 500 });
88
90
  },
89
91
  };
@@ -104,7 +106,7 @@ export const Empty: Story = {
104
106
  const input = canvas.getByLabelText('Number');
105
107
 
106
108
  await expect(input).toBeInTheDocument();
107
- await expect(input).toHaveValue(null);
109
+ await expect(input).toHaveValue('');
108
110
  },
109
111
  };
110
112
 
@@ -127,6 +129,31 @@ export const Disabled: Story = {
127
129
  const input = canvas.getByLabelText('Number');
128
130
 
129
131
  await expect(input).toBeDisabled();
130
- await expect(input).toHaveValue(99);
132
+ await expect(input).toHaveValue('99');
133
+ },
134
+ };
135
+
136
+ export const TypedFreely: Story = {
137
+ args: {
138
+ value: 7,
139
+ type: 'int',
140
+ },
141
+ parameters: {
142
+ docs: {
143
+ description: {
144
+ story:
145
+ 'Renders the Number field in integer mode after typing what is not a number: 12abc stays as typed (the form flags it, it is not cut to 12), and a template ($local:quantity) or an expression (@qty * 2) is passed on as typed for the field around it to take up.',
146
+ },
147
+ },
148
+ },
149
+ async play({ args, canvasElement }) {
150
+ const canvas = within(canvasElement);
151
+ const input = canvas.getByLabelText('Number');
152
+ for (const text of ['12abc', '@qty * 2', '$local:quantity']) {
153
+ await userEvent.clear(input);
154
+ await userEvent.type(input, text);
155
+ await expect(input).toHaveValue(text);
156
+ await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith(text), { timeout: 500 });
157
+ }
131
158
  },
132
159
  };
@@ -27,6 +27,22 @@ export interface INumberFormFieldProps extends Omit<IReqoreInputProps, 'value' |
27
27
  max_value?: number;
28
28
  }
29
29
 
30
+ /** A whole number as typed: an optional sign and digits. */
31
+ const WHOLE_NUMBER = /^[-+]?\d+$/;
32
+ /** A decimal number as typed: an optional sign, digits with a point, and an exponent. */
33
+ const DECIMAL_NUMBER = /^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/;
34
+
35
+ /**
36
+ * What a typed text is: the number it writes, or - when it writes none - the text as typed. A field of a
37
+ * number type is typed freely (David's review of qorus#646): a template (`$local:x`) or an expression
38
+ * (`@qty * 2`) is taken up by the field around it, and anything else is kept as typed for the form to flag
39
+ * as not a number. `parseInt` cut `12abc` to 12 without a word.
40
+ */
41
+ export const typedNumber = (text: string, type: 'int' | 'float'): number | string => {
42
+ const trimmed = text.trim();
43
+ return (type === 'int' ? WHOLE_NUMBER : DECIMAL_NUMBER).test(trimmed) ? Number(trimmed) : text;
44
+ };
45
+
30
46
  export const NumberFormField = ({
31
47
  onChange,
32
48
  autoFocus,
@@ -37,7 +53,10 @@ export const NumberFormField = ({
37
53
  max_value,
38
54
  ...rest
39
55
  }: INumberFormFieldProps) => {
40
- const [localValue, setLocalValue] = useState<number | string>(value ?? '');
56
+ // What is shown is what was typed; what the form gets is the number it writes, or the text when it writes
57
+ // none. Showing the number instead dropped a decimal point mid-typing: `10.` is 10, and `10.9` became `109`.
58
+ const [text, setText] = useState<string>(value === undefined || value === null ? '' : String(value));
59
+ const localValue = text === '' ? '' : typedNumber(text, type);
41
60
  // The parent's echo of an emit still in flight is not a change: copying it
42
61
  // over the local value lost what was typed since. See `EmittedValues`.
43
62
  const emitted = useEmittedValues<number | string | undefined>();
@@ -47,7 +66,7 @@ export const NumberFormField = ({
47
66
  return;
48
67
  }
49
68
  if (value !== localValue) {
50
- setLocalValue(value ?? '');
69
+ setText(value === undefined || value === null ? '' : String(value));
51
70
  }
52
71
  }, [value]);
53
72
 
@@ -62,17 +81,9 @@ export const NumberFormField = ({
62
81
  [localValue, onChange]
63
82
  );
64
83
 
65
- const handleChange = useCallback(
66
- (rawValue: number | string): void => {
67
- if (rawValue === '' || rawValue === '-' || rawValue === '-.') {
68
- setLocalValue(rawValue);
69
- return;
70
- }
71
- const parsed = type === 'int' ? parseInt(rawValue as string, 10) : parseFloat(rawValue as string);
72
- setLocalValue(isNaN(parsed) ? rawValue : parsed);
73
- },
74
- [type]
75
- );
84
+ const handleChange = useCallback((rawValue: number | string): void => {
85
+ setText(String(rawValue));
86
+ }, []);
76
87
 
77
88
  const handleInputChange = useCallback(
78
89
  (event: ChangeEvent<HTMLInputElement>): void => {
@@ -82,7 +93,7 @@ export const NumberFormField = ({
82
93
  );
83
94
 
84
95
  const handleResetClick = useCallback((): void => {
85
- setLocalValue(0);
96
+ setText('0');
86
97
  emitted.record(0);
87
98
  onChange?.(0);
88
99
  }, [onChange]);
@@ -98,17 +109,12 @@ export const NumberFormField = ({
98
109
  [autoFocus]
99
110
  );
100
111
 
101
- const handleItemSelect = useCallback((item) => setLocalValue(item.value), []);
112
+ const handleItemSelect = useCallback((item) => setText(String(item.value)), []);
102
113
 
103
- // The schema's bounds under the names the input uses. Omitted entirely when the schema
104
- // declares none, so an undeclared bound stays undeclared rather than becoming `min={0}`.
105
- const bounds = useMemo(
106
- () => ({
107
- ...(typeof min_value === 'number' ? { min: min_value } : {}),
108
- ...(typeof max_value === 'number' ? { max: max_value } : {}),
109
- }),
110
- [min_value, max_value]
111
- );
114
+ // The schema's bounds are the form's to check (a text input has no min or max); they are named in the
115
+ // props only to keep them off the DOM node.
116
+ void min_value;
117
+ void max_value;
112
118
 
113
119
  // IDE `NumberField` parity: with templates the input is wrapped in a
114
120
  // focus-opened dropdown of the template values.
@@ -121,12 +127,13 @@ export const NumberFormField = ({
121
127
  icon='MoneyDollarCircleLine'
122
128
  items={templates?.items}
123
129
  filterable
124
- value={localValue}
130
+ value={text}
125
131
  onItemSelect={handleItemSelect}
126
132
  onChange={handleInputChange}
127
- type='number'
128
- step={type === 'int' ? 1 : 0.1}
129
- {...bounds}
133
+ // typed freely: a number input drops what is not part of a number, so a template or an expression
134
+ // could not be typed; the keyboard is still a numeric one
135
+ type='text'
136
+ inputMode={type === 'int' ? 'numeric' : 'decimal'}
130
137
  onClearClick={handleResetClick}
131
138
  focusRules={focusRules}
132
139
  {...TemplatesListProps}
@@ -138,14 +145,14 @@ export const NumberFormField = ({
138
145
  <ReqoreInput
139
146
  fluid
140
147
  icon='MoneyDollarCircleLine'
141
- value={localValue}
148
+ value={text}
142
149
  onChange={handleInputChange}
143
- type='number'
144
- step={type === 'int' ? 1 : 0.1}
150
+ // typed freely, on a numeric keyboard (see above)
151
+ type='text'
152
+ inputMode={type === 'int' ? 'numeric' : 'decimal'}
145
153
  onClearClick={handleResetClick}
146
154
  focusRules={focusRules}
147
155
  {...rest}
148
- {...bounds}
149
156
  />
150
157
  );
151
158
  };
@@ -1529,6 +1529,8 @@ const TemplateFieldImpl = memo(
1529
1529
  readOnly={rest.readOnly || rest.disabled}
1530
1530
  expressions={rest.expressions}
1531
1531
  expressionsUrl={rest.expressions_url}
1532
+ // the record's fields for the Text view, from the field's schema (`dpql_fields`)
1533
+ fields={rest.dpql_fields}
1532
1534
  serverHandled={rest.server_expression_handling}
1533
1535
  extraActions={extraActions}
1534
1536
  size={rest.size}
@@ -1824,24 +1824,36 @@ export const getTypeFromValue = (value: any) => {
1824
1824
  }
1825
1825
  };
1826
1826
 
1827
- export const validateOptionWithRequiredGroups = (
1827
+ /**
1828
+ * The groups of `groups` none of whose options has a value. A required group asks for one of its
1829
+ * options; an option in several groups is settled only when each of them is.
1830
+ */
1831
+ export const unsatisfiedRequiredGroups = (
1828
1832
  options: TQorusForm,
1829
1833
  schema: IQorusFormSchema,
1830
1834
  groups: string[]
1831
- ): boolean => {
1832
- if (groups) {
1833
- const optionsInGroups = getOptionsFromRequiredGroups(schema, groups);
1834
- return optionsInGroups.some((option) =>
1835
- isValueDefined(
1836
- ((schema[option] as TQorusFormFieldSchema)?.ui_type as string) ||
1837
- ((schema[option] as TQorusFormFieldSchema)?.type as string),
1838
- options?.[option]?.value
1835
+ ): string[] =>
1836
+ (groups ?? []).filter(
1837
+ (group) =>
1838
+ !getOptionsFromRequiredGroups(schema, [group]).some((option) =>
1839
+ isValueDefined(
1840
+ ((schema[option] as TQorusFormFieldSchema)?.ui_type as string) ||
1841
+ ((schema[option] as TQorusFormFieldSchema)?.type as string),
1842
+ options?.[option]?.value
1843
+ )
1839
1844
  )
1840
- );
1841
- }
1845
+ );
1842
1846
 
1843
- return true;
1844
- };
1847
+ /**
1848
+ * Whether an option's required groups are settled: each has an option with a value. Any value in any of
1849
+ * the groups used to do, so an option in two groups passed while one of them was still empty - it was
1850
+ * neither flagged nor counted, and the form said fewer fields needed attention than did.
1851
+ */
1852
+ export const validateOptionWithRequiredGroups = (
1853
+ options: TQorusForm,
1854
+ schema: IQorusFormSchema,
1855
+ groups: string[]
1856
+ ): boolean => !unsatisfiedRequiredGroups(options, schema, groups).length;
1845
1857
 
1846
1858
  /** Why a required option is not satisfied by the current values. */
1847
1859
  export type TUnresolvedRequiredOptionReason =
package/src/index.tsx CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  hasAllDependenciesFullfilled,
10
10
  parseDependency,
11
11
  validateOptionWithRequiredGroups,
12
+ unsatisfiedRequiredGroups,
12
13
  } from './helpers/validations';
13
14
  export type { IValidationResult } from './helpers/validations';
14
15
  export { thinScrollbar } from './helpers/scrollbar';