@qoretechnologies/reqraft 0.10.46-pr.95.ge595a13 → 0.10.46

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 (147) hide show
  1. package/.claude/CLAUDE.md +12 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
  4. package/dist/components/composer/Composer.d.ts.map +1 -1
  5. package/dist/components/composer/Composer.js +5 -1
  6. package/dist/components/composer/Composer.js.map +1 -1
  7. package/dist/components/form/engine/CompactRow.d.ts +14 -0
  8. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  9. package/dist/components/form/engine/CompactRow.js +199 -41
  10. package/dist/components/form/engine/CompactRow.js.map +1 -1
  11. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.js +72 -72
  13. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  14. package/dist/components/form/engine/FormEngine.d.ts +65 -1
  15. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  16. package/dist/components/form/engine/FormEngine.js +276 -108
  17. package/dist/components/form/engine/FormEngine.js.map +1 -1
  18. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.js +55 -13
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  21. package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
  22. package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
  23. package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
  24. package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
  25. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  26. package/dist/components/form/engine/compactRowStyles.js +15 -3
  27. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  28. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  29. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  30. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  31. package/dist/components/form/engine/readFirst.d.ts +24 -0
  32. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  33. package/dist/components/form/engine/readFirst.js +84 -6
  34. package/dist/components/form/engine/readFirst.js.map +1 -1
  35. package/dist/components/form/fields/Field.d.ts.map +1 -1
  36. package/dist/components/form/fields/Field.js +1 -1
  37. package/dist/components/form/fields/Field.js.map +1 -1
  38. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  39. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  40. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  41. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  42. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  43. package/dist/components/form/fields/auto/AutoFormField.js +64 -14
  44. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  45. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  46. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  47. package/dist/components/form/fields/long-string/LongString.js +25 -4
  48. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  49. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  50. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  51. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  52. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  53. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  54. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  55. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  56. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  57. package/dist/components/form/fields/template/TemplateField.js +81 -27
  58. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  59. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  60. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  61. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  62. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  63. package/dist/components/form/index.d.ts +1 -0
  64. package/dist/components/form/index.d.ts.map +1 -1
  65. package/dist/components/form/index.js +1 -0
  66. package/dist/components/form/index.js.map +1 -1
  67. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
  68. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
  69. package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
  70. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
  71. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  72. package/dist/components/ticketThread/TicketThread.js +1 -1
  73. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  74. package/dist/helpers/common.d.ts +8 -1
  75. package/dist/helpers/common.d.ts.map +1 -1
  76. package/dist/helpers/common.js +9 -1
  77. package/dist/helpers/common.js.map +1 -1
  78. package/dist/helpers/expressions.d.ts.map +1 -1
  79. package/dist/helpers/expressions.js +15 -2
  80. package/dist/helpers/expressions.js.map +1 -1
  81. package/dist/helpers/optionUiTypes.d.ts +30 -0
  82. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  83. package/dist/helpers/optionUiTypes.js +156 -0
  84. package/dist/helpers/optionUiTypes.js.map +1 -0
  85. package/dist/helpers/options.d.ts +19 -0
  86. package/dist/helpers/options.d.ts.map +1 -1
  87. package/dist/helpers/options.js +25 -1
  88. package/dist/helpers/options.js.map +1 -1
  89. package/dist/helpers/providerValue.d.ts +68 -0
  90. package/dist/helpers/providerValue.d.ts.map +1 -0
  91. package/dist/helpers/providerValue.js +50 -0
  92. package/dist/helpers/providerValue.js.map +1 -0
  93. package/dist/helpers/singleLineString.d.ts +37 -0
  94. package/dist/helpers/singleLineString.d.ts.map +1 -0
  95. package/dist/helpers/singleLineString.js +66 -0
  96. package/dist/helpers/singleLineString.js.map +1 -0
  97. package/dist/helpers/templates.d.ts +100 -0
  98. package/dist/helpers/templates.d.ts.map +1 -1
  99. package/dist/helpers/templates.js +237 -4
  100. package/dist/helpers/templates.js.map +1 -1
  101. package/dist/helpers/validations.d.ts +103 -2
  102. package/dist/helpers/validations.d.ts.map +1 -1
  103. package/dist/helpers/validations.js +858 -91
  104. package/dist/helpers/validations.js.map +1 -1
  105. package/dist/index.d.ts +1 -1
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +2 -1
  108. package/dist/index.js.map +1 -1
  109. package/package.json +1 -1
  110. package/src/components/composer/Composer.tsx +7 -3
  111. package/src/components/form/engine/CompactRow.tsx +279 -33
  112. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  113. package/src/components/form/engine/FormEngine.stories.tsx +995 -12
  114. package/src/components/form/engine/FormEngine.tsx +377 -94
  115. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
  116. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
  117. package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
  118. package/src/components/form/engine/compactRowStyles.ts +55 -1
  119. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  120. package/src/components/form/engine/readFirst.ts +125 -35
  121. package/src/components/form/fields/Field.tsx +3 -1
  122. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  123. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
  124. package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
  125. package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
  126. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  127. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  128. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  129. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  130. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  131. package/src/components/form/fields/template/TemplateField.stories.tsx +389 -1
  132. package/src/components/form/fields/template/TemplateField.tsx +100 -35
  133. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  134. package/src/components/form/index.tsx +1 -0
  135. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
  136. package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
  137. package/src/components/ticketThread/TicketThread.tsx +7 -0
  138. package/src/helpers/common.ts +11 -1
  139. package/src/helpers/expressions.ts +16 -2
  140. package/src/helpers/optionUiTypes.ts +161 -0
  141. package/src/helpers/options.ts +23 -0
  142. package/src/helpers/providerValue.ts +125 -0
  143. package/src/helpers/singleLineString.ts +61 -0
  144. package/src/helpers/templates.ts +272 -3
  145. package/src/helpers/validations.ts +1177 -109
  146. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  147. package/src/index.tsx +1 -0
@@ -10,10 +10,58 @@ import isNaN from 'lodash/isNaN';
10
10
  import isNumber from 'lodash/isNumber';
11
11
  import isObject from 'lodash/isPlainObject';
12
12
  import size from 'lodash/size';
13
- import { getAddress, getProtocol, splitByteSize } from './common';
14
- import { getOptionsFromRequiredGroups } from './options';
13
+ import uniqWith from 'lodash/uniqWith';
14
+ import { fixOperatorValue, getAddress, getProtocol, splitByteSize } from './common';
15
+ import { isOptionInterfaceUiType } from './optionUiTypes';
16
+ import { getListElementValue, getOptionsFromRequiredGroups } from './options';
17
+ import { IProviderType, TVariableActionValue, maybeBuildOptionProvider } from './providerValue';
15
18
  import { getTemplateKey, getTemplateValue, isValueTemplate } from './templates';
16
19
 
20
+ /** The five cron fields, in order, as a schedule hash may name them. */
21
+ const CRON_FIELDS: [string, string][] = [
22
+ ['minutes', 'minute'],
23
+ ['hours', 'hour'],
24
+ ['days', 'day'],
25
+ ['months', 'month'],
26
+ ['dow', 'weekday'],
27
+ ];
28
+
29
+ /**
30
+ * A five-field cron expression, from either representation a caller may hold.
31
+ *
32
+ * A schedule reaches the form in one of two shapes, and both are legitimate: a
33
+ * joined string (`"0 0 1 1 *"`), or the hash a Qorus job carries
34
+ * (`{minutes, hours, days, months, dow}` — also accepted under the singular
35
+ * `minute`/`hour`/`day`/`month`/`weekday` spellings). The renderer already
36
+ * accepted both; validation did not, and fed the hash straight to
37
+ * `isValidCron`, which calls `.trim()` on it.
38
+ *
39
+ * Returns `undefined` for anything that is neither, so the caller reports an
40
+ * invalid value rather than throwing.
41
+ */
42
+ export const cronExpressionFromValue = (value: unknown): string | undefined => {
43
+ if (typeof value === 'string') {
44
+ return value;
45
+ }
46
+
47
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
48
+ return undefined;
49
+ }
50
+
51
+ const hash = value as Record<string, unknown>;
52
+ const parts = CRON_FIELDS.map(([plural, singular]) => {
53
+ const field = hash[plural] ?? hash[singular];
54
+ return field === undefined || field === null || field === '' ? '*' : String(field);
55
+ });
56
+
57
+ // An object with none of the five keys is not a schedule — every part
58
+ // defaulting to `*` would otherwise turn any object at all into "* * * * *"
59
+ return parts.every((part) => part === '*') &&
60
+ !CRON_FIELDS.some(([plural, singular]) => plural in hash || singular in hash)
61
+ ? undefined
62
+ : parts.join(' ');
63
+ };
64
+
17
65
  export interface IValidationResult {
18
66
  isValid: boolean;
19
67
  reason?: string;
@@ -29,17 +77,67 @@ interface IFieldValidationProps {
29
77
  validation_regex?: string;
30
78
  required_groups?: string[];
31
79
  optionSchema?: IQorusFormSchema;
32
- options?: TQorusForm;
80
+ /** The sibling option VALUES, for required-group and dependency checks.
81
+ * Widened because a schema entry is also passed straight in as `field`, and
82
+ * a schema entry carries an `options` of its own — the field's UI options
83
+ * bag (`{ file?: DropzoneOptions }`) — under the same name. */
84
+ options?: TQorusForm | Record<string, any>;
33
85
  isFunction?: boolean;
34
86
  arg_schema?: IQorusFormSchema;
35
87
  ui_element_type?: string;
36
88
  element_type?: string;
37
89
  allowed_values?: any[];
90
+ element_allowed_values?: any[];
38
91
  disabled?: boolean;
39
92
  metadata?: Record<string, any>;
93
+ /** The expression catalogue, when a caller holds one already — see
94
+ * {@link setExpressionCatalogueReader} for where it comes from otherwise. */
95
+ expressions?: any[];
96
+ /** The two key names an `array-of-pairs` row is made of. */
97
+ fields?: string[];
98
+ /** Set for a provider chosen as an FSM variable, which must support an action. */
99
+ isVariable?: boolean;
100
+ /** The declaration a `var-action` is resolved against. Partial because it
101
+ * carries only the provider half — the action supplies the variable half,
102
+ * and the two are merged before the provider validator sees them. */
103
+ variableData?: { value?: Partial<TVariableActionValue> };
40
104
  [key: string]: any;
41
105
  }
42
106
 
107
+ /**
108
+ * Where the expression catalogue comes from when a caller does not carry one.
109
+ *
110
+ * Every other rule here is a pure function of the value and its schema, but an
111
+ * expression can only be checked against the catalogue of operations the server
112
+ * publishes — which is application state, fetched once and held in a store. The
113
+ * library must not reach into a consumer's store, so the consumer hands the
114
+ * reader in instead: qorus-ide registers one that reads its expressions store.
115
+ *
116
+ * Unregistered, the catalogue is empty, and `case 'expression'` then validates
117
+ * only what an expression can be judged on by itself (see the ordering there).
118
+ */
119
+ type TExpressionCatalogueReader = () => any[] | undefined;
120
+
121
+ let expressionCatalogueReader: TExpressionCatalogueReader | undefined;
122
+
123
+ export const setExpressionCatalogueReader = (reader?: TExpressionCatalogueReader): void => {
124
+ expressionCatalogueReader = reader;
125
+ };
126
+
127
+ const readExpressionCatalogue = (field?: IFieldValidationProps): any[] =>
128
+ field?.expressions ?? expressionCatalogueReader?.() ?? [];
129
+
130
+ // permissive patterns for the semantic string formats — mirror the qore DataProvider
131
+ // QoreStringFormatDataType server backstop (email/uuid/hostname/ipv4/ipv6/phone)
132
+ const SEMANTIC_FORMAT_PATTERNS: Record<string, RegExp> = {
133
+ email: /^[^@\s]+@[^@\s]+$/,
134
+ uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,
135
+ hostname: /^[A-Za-z0-9.-]+$/,
136
+ ipv4: /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
137
+ ipv6: /^([0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}$/,
138
+ phone: /^\+?[0-9 ().-]{3,}$/,
139
+ };
140
+
43
141
  const validResult = (): IValidationResult => ({ isValid: true, reasons: [] });
44
142
 
45
143
  const invalidResult = (
@@ -115,16 +213,6 @@ export const _validateField = (
115
213
  if (canBeNull && (value === null || value === undefined)) {
116
214
  return validResult();
117
215
  }
118
- // Expression values (`is_expression`) carry the AST `{ exp, args }` instead
119
- // of a typed literal — validate that an expression is chosen, not the
120
- // base type. (`isFunction` is set by FormEngine for expression options.)
121
- if (field?.isFunction) {
122
- const ast = (value as { value?: any })?.value ?? value;
123
- return resultFromBoolean(
124
- !!(ast as { exp?: string })?.exp,
125
- 'Expression operation is required'
126
- );
127
- }
128
216
  // Strip type parameters e.g. hash<string, int> → hash
129
217
  const pos: number = type.indexOf('<');
130
218
  if (pos > 0) {
@@ -137,17 +225,44 @@ export const _validateField = (
137
225
 
138
226
  return resultFromBoolean(hasKey && hasValue, 'Template must include key and value');
139
227
  }
228
+ // Expression values (`is_expression`) carry the AST `{ exp, args }` instead of
229
+ // a typed literal, so the declared type says nothing about them — validate
230
+ // them as the expression they are, arguments and all. (`isFunction` is set by
231
+ // FormEngine, and by the option recursion below, for expression options.)
232
+ if (field?.isFunction) {
233
+ // The AST is normally the value itself; a caller that still holds the
234
+ // option envelope hands over `{ value: ast }`, so accept both.
235
+ const ast = (value as { value?: any })?.value ?? value;
236
+
237
+ return validateFieldWithResult(
238
+ 'expression',
239
+ { value: ast },
240
+ omit(field, ['isFunction']),
241
+ canBeNull
242
+ );
243
+ }
140
244
 
141
245
  // Check if the field has required groups
142
246
  if (field?.required_groups) {
143
247
  if (
144
248
  !isValueDefined(type, value) &&
145
- validateOptionWithRequiredGroups(field.options, field.optionSchema, field.required_groups)
249
+ validateOptionWithRequiredGroups(
250
+ field.options as TQorusForm,
251
+ field.optionSchema,
252
+ field.required_groups
253
+ )
146
254
  ) {
147
255
  return validResult();
148
256
  }
149
257
  }
150
258
 
259
+ // An interface type names another Qorus object by its name, so a non-empty
260
+ // string is the whole contract. `connection` is the exception: it carries
261
+ // enablement and authentication state of its own, checked below.
262
+ if (isOptionInterfaceUiType(type) && type !== 'connection') {
263
+ return validateFieldWithResult('string', value, field);
264
+ }
265
+
151
266
  switch (type) {
152
267
  case 'richtext': {
153
268
  if (!value || !size(value)) {
@@ -200,33 +315,67 @@ export const _validateField = (
200
315
  'Connection string validation failed'
201
316
  );
202
317
  }
318
+ // semantic string formats — permissive patterns mirror the qore DataProvider
319
+ // QoreStringFormatDataType backstop; a server-supplied validation_regex, if
320
+ // present, is also enforced
321
+ case 'email':
322
+ case 'uuid':
323
+ case 'hostname':
324
+ case 'ipv4':
325
+ case 'ipv6':
326
+ case 'phone': {
327
+ if (value === undefined || value === null || value === '' || typeof value !== 'string') {
328
+ return invalidResult('Value must be a non-empty text');
329
+ }
330
+ if (field?.validation_regex && !value.match(field.validation_regex)) {
331
+ return invalidResult('Value does not match the required format');
332
+ }
333
+ return resultFromBoolean(
334
+ SEMANTIC_FORMAT_PATTERNS[type].test(value),
335
+ `Value is not a valid ${type}`
336
+ );
337
+ }
203
338
  case 'binary': {
204
339
  if (typeof value !== 'string') {
205
340
  return invalidResult('Binary value must be a string');
206
341
  }
207
342
 
208
- const hex = value.trim().replace(/^0x/i, '');
343
+ const trimmed = value.trim();
209
344
 
210
- if (hex.length === 0) {
345
+ if (trimmed.length === 0) {
211
346
  return invalidResult('Binary value is empty');
212
347
  }
213
348
 
214
- if (hex.length % 2 !== 0) {
215
- return invalidResult('Binary hex length must be even');
349
+ // the canonical wire form is base64 (matching the server decode); a "data:<mime>;base64,..." URL
350
+ // and legacy "0x"-prefixed hex are also accepted
351
+ if (/^data:[^;]+;base64,/.test(trimmed)) {
352
+ return validResult();
216
353
  }
217
354
 
218
- if (!/^[0-9a-fA-F]+$/.test(hex)) {
219
- return invalidResult('Binary value must contain only hex characters');
355
+ if (/^0x/i.test(trimmed)) {
356
+ const hex = trimmed.replace(/^0x/i, '');
357
+ return resultFromBoolean(
358
+ hex.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(hex),
359
+ 'Binary hex value is invalid'
360
+ );
220
361
  }
221
362
 
222
- return validResult();
363
+ return resultFromBoolean(
364
+ /^[A-Za-z0-9+/]+={0,2}$/.test(trimmed),
365
+ 'Binary value must be base64, a data: URL, or 0x-prefixed hex'
366
+ );
223
367
  }
224
368
  case 'string':
225
369
  case 'mapper':
226
370
  case 'workflow':
227
371
  case 'service':
228
372
  case 'job':
373
+ case 'softstring':
374
+ case 'select-string':
375
+ case 'file-string':
376
+ case 'file-as-string':
229
377
  case 'long-string':
378
+ case 'method-name':
230
379
  case 'code-editor':
231
380
  case 'data': {
232
381
  if (value === undefined || value === null || value === '' || typeof value !== 'string') {
@@ -302,6 +451,128 @@ export const _validateField = (
302
451
 
303
452
  return reasons.length ? invalidResult(reasons) : validResult();
304
453
  }
454
+ case 'collection-documents': {
455
+ // The value is an array of rows in mixed lifecycle states.
456
+ // A row is valid when it's either:
457
+ // 1. Server-managed: has `ai_documentid` (number) + `name`.
458
+ // 2. Client-managed: has `name` + `content` (base64) — i.e.
459
+ // a fresh upload that hasn't landed yet.
460
+ // Failed rows count as valid for form-level purposes (the
461
+ // user can Retry; the form shouldn't block save just because
462
+ // an upload errored). Empty list is valid here — the
463
+ // top-level required check (`field.required`) handles "must
464
+ // pick at least one" gating.
465
+ if (value === undefined || value === null) {
466
+ return validResult();
467
+ }
468
+ if (!Array.isArray(value)) {
469
+ return invalidResult('Value must be a list of documents');
470
+ }
471
+ const reasons: string[] = [];
472
+ value.forEach((row: any, idx: number) => {
473
+ if (!row || typeof row !== 'object') {
474
+ reasons.push(`Row ${idx + 1} is not an object`);
475
+ return;
476
+ }
477
+ if (typeof row.name !== 'string' || !row.name) {
478
+ reasons.push(`Row ${idx + 1} is missing a name`);
479
+ return;
480
+ }
481
+ const hasServerId = typeof row.ai_documentid === 'number';
482
+ const hasContent = typeof row.content === 'string' && !!row.content;
483
+ if (!hasServerId && !hasContent) {
484
+ reasons.push(
485
+ `Row ${idx + 1} ("${row.name}") has neither uploaded ` +
486
+ `content nor a server document id`
487
+ );
488
+ }
489
+ });
490
+ return reasons.length ? invalidResult(undefined, reasons) : validResult();
491
+ }
492
+ case 'array-of-pairs': {
493
+ let valid = true;
494
+ const reasons: string[] = [];
495
+ // Check if every pair has key & value
496
+ // assigned properly
497
+ if (!Array.isArray(value)) {
498
+ return invalidResult('Value must be a list');
499
+ }
500
+ if (
501
+ !value?.every(
502
+ (pair: { [key: string]: string }): boolean =>
503
+ pair[field.fields[0]] !== '' && pair[field.fields[1]] !== ''
504
+ )
505
+ ) {
506
+ valid = false;
507
+ reasons.push('All pairs must include both fields');
508
+ }
509
+ // Get a list of unique values
510
+ const uniqueValues: any[] = uniqWith(
511
+ value,
512
+ (cur, prev) => cur[field.fields[0]] === prev[field.fields[0]]
513
+ );
514
+ // Check if there are any duplicates
515
+ if (size(uniqueValues) !== size(value)) {
516
+ valid = false;
517
+ reasons.push('Pairs must use unique keys');
518
+ }
519
+
520
+ return valid ? validResult() : invalidResult(undefined, reasons);
521
+ }
522
+ case 'class-connectors': {
523
+ if (!Array.isArray(value) || value.length === 0) {
524
+ return invalidResult('At least one connector is required');
525
+ }
526
+ let valid = true;
527
+ const reasons: string[] = [];
528
+ // Check if every pair has name, input method and output method
529
+ // assigned properly
530
+ if (
531
+ !value?.every(
532
+ (pair: { [key: string]: string }): boolean =>
533
+ !!pair.name && pair.name !== '' && !!pair.method && pair.method !== ''
534
+ )
535
+ ) {
536
+ valid = false;
537
+ reasons.push('Each connector must have a name and method');
538
+ }
539
+ // Get a list of unique values
540
+ const uniqueValues: any[] = uniqWith(value, (cur, prev) => cur.name === prev.name);
541
+ // Check if there are any duplicates
542
+ if (size(uniqueValues) !== size(value)) {
543
+ valid = false;
544
+ reasons.push('Connector names must be unique');
545
+ }
546
+
547
+ return valid ? validResult() : invalidResult(undefined, reasons);
548
+ }
549
+ // Classes check
550
+ case 'class-array': {
551
+ if (!(Array.isArray(value) && value.length > 0)) {
552
+ return invalidResult('At least one class must be provided');
553
+ }
554
+ let valid = true;
555
+ const reasons: string[] = [];
556
+ // Check if the fields are not empty
557
+ if (
558
+ !value?.every((pair: { [key: string]: string }): boolean => !!pair.name && pair.name !== '')
559
+ ) {
560
+ valid = false;
561
+ reasons.push('Each class must have a name');
562
+ }
563
+ // Get a list of unique values
564
+ const uniqueValues: any[] = uniqWith(
565
+ value,
566
+ (cur, prev) => `${cur.prefix}${cur.name}` === `${prev.prefix}${prev.name}`
567
+ );
568
+ // Check if there are any duplicates
569
+ if (size(uniqueValues) !== size(value)) {
570
+ valid = false;
571
+ reasons.push('Class names must be unique');
572
+ }
573
+
574
+ return valid ? validResult() : invalidResult(undefined, reasons);
575
+ }
305
576
  case 'number': {
306
577
  return resultFromBoolean(
307
578
  !isNaN(value) && (getTypeFromValue(value) === 'float' || getTypeFromValue(value) === 'int'),
@@ -318,15 +589,28 @@ export const _validateField = (
318
589
  !isNaN(value) && (getTypeFromValue(value) === 'float' || getTypeFromValue(value) === 'int'),
319
590
  'Value must be a float'
320
591
  );
592
+ case 'select-array':
321
593
  case 'multi-select':
322
594
  case 'array':
323
595
  case 'file-tree':
324
596
  return resultFromBoolean(value && value.length > 0, 'At least one value is required');
325
- case 'cron':
597
+ case 'cron': {
326
598
  if (!value) {
327
599
  return invalidResult('Cron value is required');
328
600
  }
329
- return resultFromBoolean(isValidCron(value, { alias: true }), 'Cron expression is invalid');
601
+ const expression = cronExpressionFromValue(value);
602
+ // A value that is neither a cron string nor a schedule hash is INVALID,
603
+ // not a crash: `isValidCron` calls `.trim()` on whatever it is given, so
604
+ // passing it a hash threw a TypeError out of validation and took the
605
+ // whole form down through the host's error boundary.
606
+ if (expression === undefined) {
607
+ return invalidResult('Cron expression is invalid');
608
+ }
609
+ return resultFromBoolean(
610
+ isValidCron(expression, { alias: true }),
611
+ 'Cron expression is invalid'
612
+ );
613
+ }
330
614
  case 'date':
331
615
  return resultFromBoolean(
332
616
  value !== undefined &&
@@ -396,7 +680,13 @@ export const _validateField = (
396
680
  for (let i = 0; i < parsedValue.length; i++) {
397
681
  const itemResult = validateFieldWithResult(
398
682
  (field.ui_element_type as string) || (field.element_type as string),
399
- parsedValue[i].value,
683
+ // An element arrives bare OR in a `{value, type}` envelope: the
684
+ // envelope is what the editor writes, the bare form is what storage
685
+ // holds. Reaching straight for `.value` failed every stored element
686
+ // as empty, so a reloaded alert rule reported its delivery action
687
+ // invalid while showing all six options set -- and threw outright on
688
+ // a null element.
689
+ getListElementValue(parsedValue[i]),
400
690
  field
401
691
  );
402
692
 
@@ -408,18 +698,370 @@ export const _validateField = (
408
698
 
409
699
  return validResult();
410
700
  }
411
- case 'auto':
412
- case 'any': {
413
- let yamlCorrect = true;
414
- let parsedData: any;
415
- try {
416
- parsedData = jsyaml.load(value);
417
- } catch (e) {
418
- yamlCorrect = false;
701
+ case 'list-of-hashes': {
702
+ const parsedValue: any = maybeParseYaml(value);
703
+
704
+ if (!parsedValue || !isArray(parsedValue)) {
705
+ return invalidResult('Value must be a list of hashes');
706
+ }
707
+
708
+ for (let i = 0; i < parsedValue.length; i++) {
709
+ const item = parsedValue[i];
710
+
711
+ if (!size(item)) {
712
+ return invalidResult(`Hash at index ${i} must not be empty`);
713
+ }
714
+
715
+ const itemResult = validateFieldWithResult('hash', item);
716
+
717
+ if (!itemResult.isValid) {
718
+ return withContext(itemResult, `Hash at index ${i} is invalid`);
719
+ }
720
+ }
721
+
722
+ return validResult();
723
+ }
724
+ case 'mapper-code': {
725
+ if (!value) {
726
+ return invalidResult('Mapper code is required');
727
+ }
728
+ // Split the value
729
+ const [code, method] = value.split('::');
730
+ // Both fields need to be strings & filled
731
+ const codeResult = validateFieldWithResult('string', code);
732
+
733
+ if (!codeResult.isValid) {
734
+ return withContext(codeResult, 'Mapper code is invalid');
735
+ }
736
+
737
+ const methodResult = validateFieldWithResult('string', method);
738
+
739
+ if (!methodResult.isValid) {
740
+ return withContext(methodResult, 'Mapper method is invalid');
741
+ }
742
+
743
+ return validResult();
744
+ }
745
+ case 'var-action': {
746
+ const varAction: TVariableActionValue = value;
747
+
748
+ if (
749
+ varAction?.var_type !== 'localvar' &&
750
+ varAction?.var_type !== 'globalvar' &&
751
+ varAction?.var_type !== 'autovar'
752
+ ) {
753
+ return invalidResult('Variable type is invalid');
754
+ }
755
+
756
+ const nameResult = validateFieldWithResult('string', varAction.var_name, {
757
+ has_to_have_value: true,
758
+ });
759
+
760
+ if (!nameResult.isValid) {
761
+ return withContext(nameResult, 'Variable name is invalid');
762
+ }
763
+
764
+ const actionResult = validateFieldWithResult('string', varAction.action_type, {
765
+ has_to_have_value: true,
766
+ });
767
+
768
+ if (!actionResult.isValid) {
769
+ return withContext(actionResult, 'Variable action type is invalid');
770
+ }
771
+
772
+ // If the action type is transaction, the transaction_action needs to be set
773
+ if (varAction.action_type === 'transaction') {
774
+ const transactionResult = validateFieldWithResult('string', varAction.transaction_action, {
775
+ has_to_have_value: true,
776
+ });
777
+
778
+ if (!transactionResult.isValid) {
779
+ return withContext(transactionResult, 'Transaction action is invalid');
780
+ }
781
+
782
+ return validResult();
783
+ }
784
+
785
+ // If the variable data is missing
786
+ if (!field?.variableData?.value) {
787
+ return invalidResult('Variable data is missing');
788
+ }
789
+
790
+ // Get the variable data
791
+ const variableData: TVariableActionValue = {
792
+ ...value,
793
+ ...field.variableData.value,
794
+ };
795
+
796
+ return withContext(
797
+ validateFieldWithResult(varAction.action_type, variableData, field),
798
+ 'Variable action is invalid'
799
+ );
800
+ }
801
+ case 'type-selector':
802
+ case 'data-provider':
803
+ case 'api-call':
804
+ case 'search-single':
805
+ case 'send-message':
806
+ case 'search':
807
+ case 'update':
808
+ case 'delete':
809
+ case 'create': {
810
+ const newValue: IProviderType | null = maybeBuildOptionProvider(value);
811
+
812
+ if (!newValue) {
813
+ return invalidResult('Provider value is invalid');
814
+ }
815
+
816
+ // Api call only supports requests / response
817
+ if (type === 'api-call' && !value.supports_request) {
818
+ return invalidResult('API call must support requests');
819
+ }
820
+
821
+ // If the provider is from FSM variables, it needs pass this
822
+ if (
823
+ field?.isVariable &&
824
+ !(
825
+ newValue.supports_read ||
826
+ newValue.supports_create ||
827
+ newValue.supports_update ||
828
+ newValue.supports_delete ||
829
+ newValue.supports_request ||
830
+ newValue.supports_messages ||
831
+ newValue.transaction_management
832
+ )
833
+ ) {
834
+ return invalidResult('Variable provider must support at least one action');
835
+ }
836
+
837
+ // Send message only supports messages
838
+ if (
839
+ type === 'send-message' &&
840
+ (!newValue.supports_messages || !newValue.message_id || !newValue.message)
841
+ ) {
842
+ return invalidResult('Send message provider must include message id and content');
843
+ }
844
+
845
+ if (newValue.message_id) {
846
+ const messageIdResult = validateFieldWithResult('string', newValue.message_id);
847
+
848
+ if (!messageIdResult.isValid) {
849
+ return withContext(messageIdResult, 'Message id is invalid');
850
+ }
851
+
852
+ if (!newValue.message) {
853
+ return invalidResult('Message content is missing');
854
+ }
855
+
856
+ const messageResult = validateFieldWithResult(
857
+ newValue.message.type,
858
+ newValue.message.value
859
+ );
860
+
861
+ if (!messageResult.isValid) {
862
+ return withContext(messageResult, 'Message content is invalid');
863
+ }
864
+ }
865
+
866
+ if (newValue.use_args) {
867
+ if (newValue.args?.type !== 'nothing') {
868
+ if (!newValue.args) {
869
+ return invalidResult('Arguments are missing');
870
+ }
871
+
872
+ const argsResult = validateFieldWithResult(
873
+ newValue.args.type === 'hash' ? 'system-options' : newValue.args.type,
874
+ newValue.args.value
875
+ );
876
+
877
+ if (!argsResult.isValid) {
878
+ return withContext(argsResult, 'Arguments are invalid');
879
+ }
880
+ }
881
+ }
882
+
883
+ if (
884
+ (type === 'search-single' || type === 'search') &&
885
+ size(newValue.search_args) !== 0 &&
886
+ !validateFieldWithResult('system-options-with-operators', newValue.search_args).isValid
887
+ ) {
888
+ return invalidResult('Search arguments are invalid');
889
+ }
890
+
891
+ const isUpdateOrCreate = type === 'update' || type === 'create';
892
+
893
+ if (isUpdateOrCreate) {
894
+ const areNormalArgsInvalid =
895
+ `${type}_args` in newValue &&
896
+ (size(newValue[`${type}_args`]) === 0 ||
897
+ !validateFieldWithResult('system-options', newValue[`${type}_args`]).isValid);
898
+
899
+ const areFreeFormArgsInvalid =
900
+ `${type}_args_freeform` in newValue &&
901
+ (size(newValue[`${type}_args_freeform`]) === 0 ||
902
+ !validateFieldWithResult('list-of-hashes', newValue[`${type}_args_freeform`]).isValid);
903
+
904
+ if (`${type}_args` in newValue && areNormalArgsInvalid && areFreeFormArgsInvalid) {
905
+ return invalidResult('Update/create arguments are invalid');
906
+ }
907
+
908
+ if (`${type}_args_freeform` in newValue && areFreeFormArgsInvalid && areNormalArgsInvalid) {
909
+ return invalidResult('Update/create arguments are invalid');
910
+ }
911
+ }
912
+
913
+ if (newValue?.type === 'factory') {
914
+ if (newValue.optionsChanged) {
915
+ return invalidResult('Factory options are not saved');
916
+ }
917
+
918
+ let options = true;
919
+
920
+ if (newValue.options) {
921
+ options = validateFieldWithResult('system-options', newValue.options).isValid;
922
+ }
923
+
924
+ // Type path and name are required
925
+ return resultFromBoolean(
926
+ !!(newValue.type && newValue.name && options),
927
+ 'Factory provider is incomplete'
928
+ );
929
+ }
930
+
931
+ if (newValue.record_requires_search_options && newValue.searchOptionsChanged) {
932
+ return invalidResult('Search options must be saved');
419
933
  }
420
934
 
421
- if (!yamlCorrect) {
422
- return invalidResult('Value is not valid YAML');
935
+ if (
936
+ newValue.search_options &&
937
+ !validateFieldWithResult('system-options', newValue.search_options).isValid
938
+ ) {
939
+ return invalidResult('Search options are invalid');
940
+ }
941
+
942
+ return resultFromBoolean(
943
+ !!(newValue.type && newValue.name),
944
+ 'Provider type and name required'
945
+ );
946
+ }
947
+ case 'context-selector': {
948
+ if (isString(value)) {
949
+ const cont: string[] = value.split(':');
950
+ const contextResult = validateFieldWithResult('string', cont[0]);
951
+
952
+ if (!contextResult.isValid) {
953
+ return withContext(contextResult, 'Context selector prefix is invalid');
954
+ }
955
+
956
+ const nameResult = validateFieldWithResult('string', cont[1]);
957
+
958
+ if (!nameResult.isValid) {
959
+ return withContext(nameResult, 'Context selector name is invalid');
960
+ }
961
+
962
+ return validResult();
963
+ }
964
+ return resultFromBoolean(
965
+ !!value?.iface_kind && !!value?.name,
966
+ 'Context selector requires iface_kind and name'
967
+ );
968
+ }
969
+ case 'service-event': {
970
+ const typeResult = validateFieldWithResult('type-selector', value);
971
+
972
+ if (!typeResult.isValid) {
973
+ return withContext(typeResult, 'Service event type is invalid');
974
+ }
975
+
976
+ if (
977
+ !size(value.handlers) ||
978
+ !every(
979
+ value.handlers,
980
+ (handler: any) => (handler.type === 'fsm' || handler.type === 'method') && handler.value
981
+ )
982
+ ) {
983
+ return invalidResult('Service event handlers are invalid');
984
+ }
985
+
986
+ return validResult();
987
+ }
988
+ case 'service-events': {
989
+ if (!isArray(value) || !size(value)) {
990
+ return invalidResult('At least one service event is required');
991
+ }
992
+
993
+ for (let i = 0; i < value.length; i++) {
994
+ const serviceEventResult = validateFieldWithResult('service-event', value[i]);
995
+
996
+ if (!serviceEventResult.isValid) {
997
+ return withContext(serviceEventResult, `Service event ${i} is invalid`);
998
+ }
999
+ }
1000
+
1001
+ return validResult();
1002
+ }
1003
+ case 'service-webhook': {
1004
+ if (
1005
+ !validateFieldWithResult('string', value?.name).isValid ||
1006
+ !validateFieldWithResult('string', value?.['rest-method']).isValid ||
1007
+ !validateFieldWithResult('string', value?.auth).isValid
1008
+ ) {
1009
+ return invalidResult('Webhook name, method and auth are required');
1010
+ }
1011
+
1012
+ if (value.handler) {
1013
+ if (value.handler.type !== 'fsm' && value.handler.type !== 'method') {
1014
+ return invalidResult('Webhook handler type is invalid');
1015
+ }
1016
+
1017
+ const handlerResult = validateFieldWithResult('string', value.handler.value);
1018
+
1019
+ if (!handlerResult.isValid) {
1020
+ return withContext(handlerResult, 'Webhook handler value is invalid');
1021
+ }
1022
+ }
1023
+
1024
+ return validResult();
1025
+ }
1026
+ case 'service-webhooks': {
1027
+ if (!isArray(value) || !size(value)) {
1028
+ return invalidResult('At least one webhook is required');
1029
+ }
1030
+
1031
+ for (let i = 0; i < value.length; i++) {
1032
+ const webhookResult = validateFieldWithResult('service-webhook', value[i]);
1033
+
1034
+ if (!webhookResult.isValid) {
1035
+ return withContext(webhookResult, `Service webhook ${i} is invalid`);
1036
+ }
1037
+ }
1038
+
1039
+ return validResult();
1040
+ }
1041
+ case 'auto':
1042
+ case 'any': {
1043
+ // Only a STRING has YAML in it. An `auto` field's value is usually
1044
+ // already the structure it describes -- a hash the form's hash editor
1045
+ // wrote, a list from a list editor -- and `jsyaml.load()` stringifies
1046
+ // whatever it is handed first: an object becomes `"[object Object]"`,
1047
+ // which YAML then reads as the flow sequence `["object Object"]`. So the
1048
+ // auto-detected type came back `list`, the value was validated as a list,
1049
+ // and every hash stored in an `auto` field was reported *"Value must be a
1050
+ // list"* -- an invalid marker on a value that is fine. A list fared no
1051
+ // better: `[1, 2]` stringified to `"1,2"` and detected as a string.
1052
+ //
1053
+ // Every other branch that accepts either shape already parses through
1054
+ // `maybeParseYaml`, which passes a non-string straight through; this one
1055
+ // reached for `jsyaml` directly. It cannot use `maybeParseYaml` as-is
1056
+ // because that reports a YAML error as an absent value, and "not valid
1057
+ // YAML" and "empty" are different answers here.
1058
+ let parsedData: any = value;
1059
+ if (isString(value)) {
1060
+ try {
1061
+ parsedData = jsyaml.load(value);
1062
+ } catch (e) {
1063
+ return invalidResult('Value is not valid YAML');
1064
+ }
423
1065
  }
424
1066
 
425
1067
  if (parsedData) {
@@ -431,7 +1073,187 @@ export const _validateField = (
431
1073
 
432
1074
  return invalidResult('Value is empty');
433
1075
  }
434
- case 'options': {
1076
+ case 'processor': {
1077
+ if (!value || !value['processor-input-type'] || !value['processor-output-type']) {
1078
+ return invalidResult('Processor input and output types are required');
1079
+ }
1080
+ // Validate the input and output types
1081
+ if (value?.['processor-input-type']) {
1082
+ const inputResult = validateFieldWithResult(
1083
+ 'type-selector',
1084
+ value?.['processor-input-type']
1085
+ );
1086
+
1087
+ if (!inputResult.isValid) {
1088
+ return withContext(inputResult, 'Processor input type is invalid');
1089
+ }
1090
+ }
1091
+
1092
+ if (value?.['processor-output-type']) {
1093
+ const outputResult = validateFieldWithResult(
1094
+ 'type-selector',
1095
+ value?.['processor-output-type']
1096
+ );
1097
+
1098
+ if (!outputResult.isValid) {
1099
+ return withContext(outputResult, 'Processor output type is invalid');
1100
+ }
1101
+ }
1102
+
1103
+ return validResult();
1104
+ }
1105
+ case 'processor-mappings': {
1106
+ // processor-mappings is an array of field mappings
1107
+ // Each mapping must have an outputPath (the target field being mapped)
1108
+ // and either an inputPath (source field) or options configured
1109
+ if (!isArray(value)) {
1110
+ return canBeNull ? validResult() : invalidResult('Processor mappings must be a list');
1111
+ }
1112
+
1113
+ for (let i = 0; i < value.length; i++) {
1114
+ const mapping = value[i];
1115
+
1116
+ if (!mapping || !isObject(mapping)) {
1117
+ return invalidResult(`Mapping at index ${i} is invalid`);
1118
+ }
1119
+
1120
+ // outputPath is always required - it identifies which output field is being mapped
1121
+ const outputPathResult = validateFieldWithResult('string', mapping.outputPath);
1122
+
1123
+ if (!outputPathResult.isValid) {
1124
+ return withContext(outputPathResult, `Mapping ${i} output path is invalid`);
1125
+ }
1126
+
1127
+ // A mapping must have either an inputPath or options (or both)
1128
+ const hasInputPath = mapping.inputPath && mapping.inputPath !== '';
1129
+ const hasOptions = mapping.options && size(mapping.options) > 0;
1130
+
1131
+ if (!hasInputPath && !hasOptions) {
1132
+ return invalidResult(
1133
+ `Mapping for "${mapping.outputPath}" must have an input path or options configured`
1134
+ );
1135
+ }
1136
+ }
1137
+
1138
+ return validResult();
1139
+ }
1140
+ case 'tool-catalog': {
1141
+ // tool-catalog is a flat string[] of selectors; each selector is one of:
1142
+ // "*" — all tools from every source
1143
+ // "system:*" — all Qorus system tools
1144
+ // "system:<tool>" — a specific system tool
1145
+ // "<connection>" — all tools from a connection
1146
+ // "<connection>/<tool>" — a specific tool from a connection
1147
+ if (!isArray(value)) {
1148
+ return canBeNull ? validResult() : invalidResult('Tool catalog must be a list');
1149
+ }
1150
+
1151
+ for (let i = 0; i < value.length; i++) {
1152
+ const selector = value[i];
1153
+ if (typeof selector !== 'string' || selector.length === 0) {
1154
+ return invalidResult(`Tool selector at index ${i} must be a non-empty string`);
1155
+ }
1156
+ }
1157
+
1158
+ return validResult();
1159
+ }
1160
+ case 'fsm-list': {
1161
+ if (!isArray(value)) {
1162
+ return invalidResult('FSM list must be an array');
1163
+ }
1164
+
1165
+ for (let i = 0; i < value.length; i++) {
1166
+ const itemResult = validateFieldWithResult('string', value[i]?.name);
1167
+
1168
+ if (!itemResult.isValid) {
1169
+ return withContext(itemResult, `FSM entry ${i} name is invalid`);
1170
+ }
1171
+ }
1172
+
1173
+ return validResult();
1174
+ }
1175
+ case 'api-manager': {
1176
+ if (!value) {
1177
+ return invalidResult('API manager value is missing');
1178
+ }
1179
+
1180
+ const factoryResult = validateFieldWithResult('string', value.factory);
1181
+
1182
+ if (!factoryResult.isValid) {
1183
+ return withContext(factoryResult, 'API manager factory is invalid');
1184
+ }
1185
+
1186
+ const endpointsResult = validateFieldWithResult('api-endpoints', value.endpoints);
1187
+
1188
+ if (!endpointsResult.isValid) {
1189
+ return withContext(endpointsResult, 'API manager endpoints are invalid');
1190
+ }
1191
+
1192
+ return validResult();
1193
+ }
1194
+ case 'api-endpoints': {
1195
+ if (!isArray(value) || !size(value)) {
1196
+ return invalidResult('At least one API endpoint is required');
1197
+ }
1198
+
1199
+ for (let i = 0; i < value.length; i++) {
1200
+ const endpointResult = validateFieldWithResult('string', value[i]?.value);
1201
+
1202
+ if (!endpointResult.isValid) {
1203
+ return withContext(endpointResult, `Endpoint ${i} value is invalid`);
1204
+ }
1205
+
1206
+ const authorizationResult = validateFieldWithResult(
1207
+ 'api-endpoint-authorization',
1208
+ value[i]?.authorization,
1209
+ undefined,
1210
+ true
1211
+ );
1212
+
1213
+ if (!authorizationResult.isValid) {
1214
+ return withContext(authorizationResult, `Endpoint ${i} authorization is invalid`);
1215
+ }
1216
+ }
1217
+
1218
+ return validResult();
1219
+ }
1220
+ case 'api-endpoint-authorization': {
1221
+ if (!value) {
1222
+ return canBeNull ? validResult() : invalidResult('Authorization override is missing');
1223
+ }
1224
+
1225
+ if (typeof value !== 'object' || isArray(value)) {
1226
+ return invalidResult('Authorization override must be an object');
1227
+ }
1228
+
1229
+ if (value.mode !== undefined && !['merge', 'replace'].includes(value.mode)) {
1230
+ return invalidResult('Authorization mode must be merge or replace');
1231
+ }
1232
+
1233
+ if (value.allow_anonymous !== undefined && typeof value.allow_anonymous !== 'boolean') {
1234
+ return invalidResult('Authorization allow_anonymous must be a boolean');
1235
+ }
1236
+
1237
+ if (value.roles !== undefined) {
1238
+ if (!isArray(value.roles)) {
1239
+ return invalidResult('Authorization roles must be a list');
1240
+ }
1241
+
1242
+ for (let i = 0; i < value.roles.length; i++) {
1243
+ const roleResult = validateFieldWithResult('string', value.roles[i]);
1244
+
1245
+ if (!roleResult.isValid) {
1246
+ return withContext(roleResult, `Authorization role ${i} is invalid`);
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ return validResult();
1252
+ }
1253
+ case 'options':
1254
+ case 'pipeline-options':
1255
+ case 'mapper-options':
1256
+ case 'system-options': {
435
1257
  const getIsValid = (
436
1258
  options?: TQorusForm,
437
1259
  optionSchema: IQorusFormSchema = {}
@@ -444,47 +1266,51 @@ export const _validateField = (
444
1266
  return validResult();
445
1267
  }
446
1268
 
447
- if (optionSchema) {
448
- for (const [option, optionData] of Object.entries(optionSchema)) {
449
- if (
450
- optionData.required &&
451
- (!options?.[option] || options?.[option]?.value === undefined)
452
- ) {
453
- return invalidResult(`Option ${option} is required`);
454
- }
455
-
456
- if (
457
- !options?.[option]?.value &&
458
- optionData.required_groups &&
459
- !validateOptionWithRequiredGroups(options, optionSchema, optionData.required_groups)
460
- ) {
461
- return invalidResult(`Option ${option} requires a value`);
462
- }
463
- }
1269
+ // Check if all required options are resolved by the current values
1270
+ const [unresolvedRequiredOption] = getUnresolvedRequiredOptions(optionSchema, options);
1271
+
1272
+ if (unresolvedRequiredOption) {
1273
+ return unresolvedRequiredOption.validation
1274
+ ? withContext(
1275
+ unresolvedRequiredOption.validation,
1276
+ `Option ${unresolvedRequiredOption.name} is invalid`
1277
+ )
1278
+ : invalidResult(
1279
+ getUnresolvedRequiredOptionReason(unresolvedRequiredOption, optionSchema)
1280
+ );
464
1281
  }
465
1282
 
466
1283
  for (const option of Object.keys(options)) {
1284
+ const optionData = options[option];
1285
+ const optionType = ((typeof optionData === 'object' ? optionData?.type : undefined) ||
1286
+ (optionSchema?.[option] as TQorusFormFieldSchema)?.ui_type ||
1287
+ (optionSchema?.[option] as TQorusFormFieldSchema)?.type) as string;
1288
+ const optionValue =
1289
+ optionData && typeof optionData === 'object' ? optionData.value : optionData;
1290
+
467
1291
  if (
468
- optionSchema?.[option]?.depends_on &&
469
- !hasAllDependenciesFullfilled(optionSchema[option].depends_on, options, optionSchema)
1292
+ !(optionSchema?.[option] as TQorusFormFieldSchema)?.required &&
1293
+ !isValueDefined(optionType, optionValue)
470
1294
  ) {
471
- return invalidResult(`Option ${option} dependencies are not fulfilled`);
1295
+ continue;
472
1296
  }
473
1297
 
474
1298
  if (
475
- optionSchema?.[option]?.preselected &&
476
- !optionSchema?.[option]?.required &&
477
- !options[option]?.value
1299
+ (optionSchema?.[option] as TQorusFormFieldSchema)?.depends_on &&
1300
+ !hasAllDependenciesFullfilled(
1301
+ (optionSchema[option] as TQorusFormFieldSchema).depends_on,
1302
+ options,
1303
+ optionSchema
1304
+ )
478
1305
  ) {
479
- continue;
1306
+ return invalidResult(`Option ${option} dependencies are not fulfilled`);
480
1307
  }
481
1308
 
482
- const optionData = options[option];
483
1309
  const optionResult =
484
1310
  typeof optionData !== 'object'
485
1311
  ? validateFieldWithResult(getTypeFromValue(optionData), optionData)
486
1312
  : validateFieldWithResult(optionData.type, optionData.value, {
487
- ...omit(optionSchema?.[option] as any, []),
1313
+ ...(optionSchema?.[option] as any),
488
1314
  optionSchema,
489
1315
  options,
490
1316
  isFunction: optionData.is_expression,
@@ -512,9 +1338,55 @@ export const _validateField = (
512
1338
 
513
1339
  return getIsValid(value, field?.optionSchema);
514
1340
  }
1341
+ case 'system-options-with-operators': {
1342
+ const isValid = (val: TQorusForm): IValidationResult => {
1343
+ if (!val || size(val) === 0) {
1344
+ if (canBeNull) {
1345
+ return validResult();
1346
+ }
1347
+
1348
+ return invalidResult('Options with operators are required');
1349
+ }
1350
+
1351
+ for (const option of Object.keys(val)) {
1352
+ const optionData = val[option];
1353
+ const optionResult =
1354
+ typeof optionData !== 'object'
1355
+ ? validateFieldWithResult(getTypeFromValue(optionData), optionData)
1356
+ : validateFieldWithResult(optionData.type, optionData.value);
1357
+
1358
+ if (!optionResult.isValid) {
1359
+ return withContext(optionResult, `Option ${option} is invalid`);
1360
+ }
1361
+
1362
+ if (
1363
+ !optionData.op ||
1364
+ !fixOperatorValue(optionData.op).every(
1365
+ (operator) => validateFieldWithResult('string', operator).isValid
1366
+ )
1367
+ ) {
1368
+ return invalidResult(`Operators for option ${option} are invalid`);
1369
+ }
1370
+ }
1371
+
1372
+ return validResult();
1373
+ };
1374
+
1375
+ if (isArray(value)) {
1376
+ for (let i = 0; i < value.length; i++) {
1377
+ const optionResult = isValid(value[i]);
1378
+
1379
+ if (!optionResult.isValid) {
1380
+ return withContext(optionResult, `Option set ${i} is invalid`);
1381
+ }
1382
+ }
1383
+
1384
+ return validResult();
1385
+ }
1386
+
1387
+ return isValid(value);
1388
+ }
515
1389
  case 'byte-size': {
516
- // Ported from qorus-ide validations (`sizeUnit` instead of the IDE's
517
- // `size`, which would shadow the lodash import here).
518
1390
  if (typeof value !== 'string') return invalidResult('Byte size must be a string');
519
1391
 
520
1392
  const [bytes, sizeUnit] = splitByteSize(value);
@@ -544,7 +1416,8 @@ export const _validateField = (
544
1416
 
545
1417
  return validResult();
546
1418
  }
547
- case 'url': {
1419
+ case 'url':
1420
+ case 'uri': {
548
1421
  const protocolResult = validateFieldWithResult('string', getProtocol(value));
549
1422
 
550
1423
  if (!protocolResult.isValid) {
@@ -560,8 +1433,7 @@ export const _validateField = (
560
1433
  return validResult();
561
1434
  }
562
1435
  case 'schema-definition': {
563
- // No IDE counterpart (the editor validates inline there); this is the
564
- // minimal NEW_FIELD.md shape check so malformed definitions can't
1436
+ // The minimal NEW_FIELD.md shape check, so malformed definitions can't
565
1437
  // silently save through FormEngine.
566
1438
  if (!isObject(value)) {
567
1439
  return invalidResult('Schema definition must be an object');
@@ -572,65 +1444,102 @@ export const _validateField = (
572
1444
  return validResult();
573
1445
  }
574
1446
  case 'expression': {
575
- // Ported from qorus-ide validations `'expression'` case. The catalogue
576
- // is supplied via `field.expressions` (the ExpressionBuilder passes it),
577
- // not a global store as in the IDE.
578
1447
  const castedValue = value as {
579
1448
  value?: { exp?: string; args?: any[] };
580
1449
  is_expression?: boolean;
581
1450
  type?: string;
582
1451
  };
583
- const expressions: any[] = (field as { expressions?: any[] })?.expressions ?? [];
1452
+ const expressions = readExpressionCatalogue(field);
584
1453
 
585
- // Expressions not loaded yet skip validation until they're available.
586
- if (!size(expressions)) {
587
- return validResult();
588
- }
1454
+ // Expressions validation is complicated and needs to follow the following rules
1455
+ // 1. If the expression is empty, it is invalid
1456
+ // 2. If the expression has a type, it means its a normal argument (sub expressions don't have type)
1457
+ // 3. If the expression has no type and `exp` is present, it is a sub expression and must be validated as such
1458
+ // 4. If there are no `args` we need to check if the expression actually has any required arguments or what is the `min_args` property
1459
+
1460
+ // 1. If the expression is empty
1461
+ //
1462
+ // Checked BEFORE the catalogue guard below, and deliberately: an
1463
+ // expression with no value, or with no operation chosen, is incomplete on
1464
+ // its own terms. Nothing in the catalogue can make it valid, so making
1465
+ // these two answers wait for a network fetch meant the same option
1466
+ // reported valid or invalid depending on whether that fetch had landed —
1467
+ // the field's own "show invalid only" filter listed a different set of
1468
+ // rows on a fast machine than on a slow one.
589
1469
  if (!castedValue) {
590
1470
  return invalidResult('Expression value is empty');
591
1471
  }
1472
+
592
1473
  if (!castedValue.value?.exp) {
593
1474
  return invalidResult('Expression operation is required');
594
1475
  }
595
- const expressionDefinition = expressions.find((expr) => expr.name === castedValue.value?.exp);
1476
+
1477
+ // Everything past this point needs the expression's definition, which
1478
+ // only the catalogue can supply. Until it loads there is nothing to check
1479
+ // against, so an expression that names an operation is left alone rather
1480
+ // than reported against a catalogue we do not have.
1481
+ if (!size(expressions)) {
1482
+ return validResult();
1483
+ }
1484
+
1485
+ // Get the expression definition from the catalogue
1486
+ const expressionDefinition = expressions.find(
1487
+ (expr: any) => expr.name === castedValue.value?.exp
1488
+ );
1489
+
596
1490
  if (!expressionDefinition) {
597
1491
  return invalidResult(`Expression definition ${String(castedValue.value?.exp)} not found`);
598
1492
  }
1493
+
1494
+ // If the expression has no required args, we can consider it valid
599
1495
  const hasRequiredArgs = expressionDefinition.args?.some((arg: any) => arg.required);
1496
+
600
1497
  if (!hasRequiredArgs && size(castedValue.value?.args || []) === 0) {
601
1498
  return validResult();
602
1499
  }
1500
+
1501
+ // If there are no args provided but the expression requires some (normal expressions require at least 1 arg
1502
+ // because we checked for required args earlier), or min_args is set
603
1503
  if (size(castedValue.value?.args || []) < (expressionDefinition.min_args || 1)) {
604
1504
  return invalidResult('Not enough arguments provided');
605
1505
  }
1506
+
1507
+ // Now we need to validate each argument, either as a normal value or as a sub-expression
606
1508
  const args = castedValue.value?.args ?? [];
1509
+
607
1510
  for (let index = 0; index < args.length; index++) {
608
1511
  const argValue: any = args[index];
609
1512
  const argDefinition = expressionDefinition.varargs
610
1513
  ? expressionDefinition.args[0]
611
1514
  : expressionDefinition.args[index];
1515
+
612
1516
  if (!argValue?.value && !argDefinition?.required) {
613
1517
  continue;
614
1518
  }
1519
+
615
1520
  if (argValue?.is_expression) {
616
1521
  const result = validateFieldWithResult('expression', argValue, {
617
1522
  expressions,
618
1523
  has_to_have_value: argDefinition?.required,
619
- } as any);
1524
+ });
1525
+
620
1526
  if (!result.isValid) {
621
1527
  return withContext(
622
1528
  result,
623
1529
  `Sub-expression for argument ${index + 1} ("${argDefinition?.display_name}") is invalid`
624
1530
  );
625
1531
  }
1532
+
626
1533
  continue;
627
1534
  }
1535
+
628
1536
  const result = validateFieldWithResult(argValue.type, argValue.value, {
629
1537
  expressions,
630
1538
  allowed_values: argDefinition?.allowed_values,
631
1539
  element_allowed_values: argDefinition?.element_allowed_values,
632
1540
  has_to_have_value: argDefinition?.required,
633
- } as any);
1541
+ });
1542
+
634
1543
  if (!result.isValid) {
635
1544
  return withContext(
636
1545
  result,
@@ -638,6 +1547,7 @@ export const _validateField = (
638
1547
  );
639
1548
  }
640
1549
  }
1550
+
641
1551
  return validResult();
642
1552
  }
643
1553
  case 'nothing':
@@ -647,7 +1557,6 @@ export const _validateField = (
647
1557
  }
648
1558
  };
649
1559
 
650
- // Memoized version
651
1560
  /**
652
1561
  * Read one arg out of a hash, whichever of the two legitimate shapes it is in.
653
1562
  *
@@ -667,6 +1576,7 @@ export const _validateField = (
667
1576
  export const readArgValue = (arg: any): any =>
668
1577
  isObject(arg) && 'value' in (arg as object) ? (arg as { value: unknown }).value : arg;
669
1578
 
1579
+ // Memoized version
670
1580
  export const validateFieldWithResult: (
671
1581
  type: string,
672
1582
  value?: any,
@@ -742,7 +1652,10 @@ export const getValueOrDefaultValue = (value: any, defaultValue: any, canBeNull?
742
1652
  return undefined;
743
1653
  };
744
1654
 
745
- export const getTypeFromValue = (value: any): string => {
1655
+ /** Deliberately un-annotated: the inferred literal union is what callers assign
1656
+ * into their own narrower type aliases, and widening it to `string` breaks
1657
+ * them. */
1658
+ export const getTypeFromValue = (value: any) => {
746
1659
  switch (true) {
747
1660
  case isNull(value):
748
1661
  case isUndefined(value):
@@ -775,7 +1688,8 @@ export const validateOptionWithRequiredGroups = (
775
1688
  const optionsInGroups = getOptionsFromRequiredGroups(schema, groups);
776
1689
  return optionsInGroups.some((option) =>
777
1690
  isValueDefined(
778
- (schema[option]?.ui_type as string) || (schema[option]?.type as string),
1691
+ ((schema[option] as TQorusFormFieldSchema)?.ui_type as string) ||
1692
+ ((schema[option] as TQorusFormFieldSchema)?.type as string),
779
1693
  options?.[option]?.value
780
1694
  )
781
1695
  );
@@ -784,6 +1698,179 @@ export const validateOptionWithRequiredGroups = (
784
1698
  return true;
785
1699
  };
786
1700
 
1701
+ /** Why a required option is not satisfied by the current values. */
1702
+ export type TUnresolvedRequiredOptionReason =
1703
+ /** No value at all — neither typed, nor defaulted, nor preselected. */
1704
+ | 'missing'
1705
+ /** The option's dependencies are not fulfilled, so its value cannot be final yet. */
1706
+ | 'dependency'
1707
+ /** A value is present but does not validate against the option's type / schema. */
1708
+ | 'invalid';
1709
+
1710
+ export interface IUnresolvedRequiredOption {
1711
+ /** The option's key in the schema. */
1712
+ name: string;
1713
+ reason: TUnresolvedRequiredOptionReason;
1714
+ /** The failing validation, present only for the `invalid` reason. */
1715
+ validation?: IValidationResult;
1716
+ }
1717
+
1718
+ export const getUnresolvedRequiredOptionReason = (
1719
+ { name, reason }: IUnresolvedRequiredOption,
1720
+ optionSchema?: IQorusFormSchema
1721
+ ): string => {
1722
+ switch (reason) {
1723
+ case 'dependency':
1724
+ return `Option ${name} dependencies are not fulfilled`;
1725
+ case 'invalid':
1726
+ return `Option ${name} is invalid`;
1727
+ default:
1728
+ return (optionSchema?.[name] as TQorusFormFieldSchema)?.required
1729
+ ? `Option ${name} is required`
1730
+ : `Option ${name} requires a value`;
1731
+ }
1732
+ };
1733
+
1734
+ /**
1735
+ * List every required option that the given values do not resolve.
1736
+ *
1737
+ * `required` on an option schema means "the effective configuration must carry
1738
+ * this option" — NOT "the user has to type it in". An option whose schema
1739
+ * default or preselected value already resolves it needs no user input at all,
1740
+ * which is why callers pass values that have been through `fixOptions()`
1741
+ * (defaults, preselected values and rich-text default envelopes applied)
1742
+ * rather than the raw values the server returned.
1743
+ *
1744
+ * This is the single owner of the "which required options still need input"
1745
+ * question: `validateField('options', …)` uses it for form validity, and
1746
+ * qorus-ide's `ConnectionManagementModal` uses it to decide whether a
1747
+ * connection can be created — and its OAuth2 flow started — without showing a
1748
+ * form at all.
1749
+ */
1750
+ export const getUnresolvedRequiredOptions = (
1751
+ optionSchema: IQorusFormSchema = {},
1752
+ options: TQorusForm = {}
1753
+ ): IUnresolvedRequiredOption[] => {
1754
+ const unresolved: IUnresolvedRequiredOption[] = [];
1755
+
1756
+ for (const [name, schemaEntry] of Object.entries(optionSchema ?? {})) {
1757
+ const optionData = schemaEntry as TQorusFormFieldSchema;
1758
+
1759
+ if (!optionData?.required && !size(optionData?.required_groups)) {
1760
+ continue;
1761
+ }
1762
+
1763
+ const optionField = options?.[name];
1764
+ const optionValue =
1765
+ optionField && typeof optionField === 'object' ? optionField.value : optionField;
1766
+ const optionType = ((typeof optionField === 'object' ? optionField?.type : undefined) ||
1767
+ optionData.ui_type ||
1768
+ optionData.type) as string;
1769
+
1770
+ // A dependent option cannot be considered resolved while its dependencies
1771
+ // are not: the schema it will be validated against — and with it any
1772
+ // default the server would send — is not final yet.
1773
+ if (
1774
+ optionData.depends_on &&
1775
+ !hasAllDependenciesFullfilled(optionData.depends_on, options, optionSchema)
1776
+ ) {
1777
+ unresolved.push({ name, reason: 'dependency' });
1778
+ continue;
1779
+ }
1780
+
1781
+ if (!isValueDefined(optionType, optionValue)) {
1782
+ // An option required only through a group is resolved by any sibling in
1783
+ // that group carrying a value.
1784
+ if (
1785
+ optionData.required ||
1786
+ !validateOptionWithRequiredGroups(options, optionSchema, optionData.required_groups)
1787
+ ) {
1788
+ unresolved.push({ name, reason: 'missing' });
1789
+ }
1790
+
1791
+ continue;
1792
+ }
1793
+
1794
+ const validation =
1795
+ typeof optionField !== 'object'
1796
+ ? validateFieldWithResult(getTypeFromValue(optionField), optionField)
1797
+ : validateFieldWithResult(optionField.type, optionField.value, {
1798
+ ...(optionData as any),
1799
+ optionSchema,
1800
+ options,
1801
+ isFunction: optionField.is_expression,
1802
+ });
1803
+
1804
+ if (!validation.isValid) {
1805
+ unresolved.push({ name, reason: 'invalid', validation });
1806
+ }
1807
+ }
1808
+
1809
+ return unresolved;
1810
+ };
1811
+
1812
+ /**
1813
+ * Split a dependency entry into the sibling it names and how its value is compared.
1814
+ *
1815
+ * Three forms: `name` (the sibling must simply have a value), `name=value` (it must
1816
+ * have exactly that value) and `name!=value` (it must have a value, and not that
1817
+ * one). The negative form is what lets a field say it applies to every variant of a
1818
+ * record but one — spelling that as an `any-of` list of every other variant is both
1819
+ * unreadable in the "Depends on" lock and silently wrong the moment a variant is
1820
+ * added.
1821
+ *
1822
+ * `!=` deliberately requires the sibling to be answered: "not X" is a statement about
1823
+ * an answer, so an unanswered sibling does not satisfy it and the field stays locked.
1824
+ */
1825
+ export const parseDependency = (
1826
+ dependency: string
1827
+ ): { name: string; op?: '=' | '!='; value?: string } => {
1828
+ const neqIdx = dependency.indexOf('!=');
1829
+ if (neqIdx !== -1) {
1830
+ return {
1831
+ name: dependency.substring(0, neqIdx),
1832
+ op: '!=',
1833
+ value: dependency.substring(neqIdx + 2),
1834
+ };
1835
+ }
1836
+
1837
+ const eqIdx = dependency.indexOf('=');
1838
+ if (eqIdx !== -1) {
1839
+ return {
1840
+ name: dependency.substring(0, eqIdx),
1841
+ op: '=',
1842
+ value: dependency.substring(eqIdx + 1),
1843
+ };
1844
+ }
1845
+
1846
+ return { name: dependency };
1847
+ };
1848
+
1849
+ /** Whether one dependency entry holds against the current form values. */
1850
+ const isDependencyFulfilled = (
1851
+ dependency: string,
1852
+ options: TQorusForm,
1853
+ optionsSchema?: IQorusFormSchema
1854
+ ): boolean => {
1855
+ const { name, op, value } = parseDependency(dependency);
1856
+
1857
+ if (op) {
1858
+ const optValue = options?.[name]?.value;
1859
+ if (optValue == null) {
1860
+ return false;
1861
+ }
1862
+ return op === '=' ? String(optValue) === value : String(optValue) !== value;
1863
+ }
1864
+
1865
+ return options?.[name]
1866
+ ? validateField(options[name].type, options[name].value, {
1867
+ ...(optionsSchema?.[name] as unknown as IFieldValidationProps),
1868
+ options,
1869
+ optionSchema: optionsSchema,
1870
+ })
1871
+ : true;
1872
+ };
1873
+
787
1874
  export const hasAllDependenciesFullfilled = (
788
1875
  dependencies: string[] | string[][],
789
1876
  options: TQorusForm,
@@ -794,38 +1881,19 @@ export const hasAllDependenciesFullfilled = (
794
1881
  }
795
1882
 
796
1883
  return dependencies.every((dependency: string | string[]) => {
1884
+ // A nested list is an ANY. It used to look each entry up as a whole form-field
1885
+ // name, so a `name=value` entry inside one found no field and returned `true`
1886
+ // unconditionally — the any-of group was satisfied by anything at all, while
1887
+ // CompactRow's lock rendered those same entries as real comparisons. Both halves
1888
+ // now go through one parser, so the grammar cannot mean two things.
797
1889
  if (isArray(dependency)) {
798
- return (dependency as string[]).some((dep) => {
799
- return options?.[dep]
800
- ? validateField(options[dep].type, options[dep].value, {
801
- ...(optionsSchema?.[dep] as unknown as IFieldValidationProps),
802
- options,
803
- optionSchema: optionsSchema,
804
- })
805
- : true;
806
- });
807
- }
808
-
809
- if (isString(dependency)) {
810
- const eqIdx = (dependency as string).indexOf('=');
811
- if (eqIdx !== -1) {
812
- const depName = (dependency as string).substring(0, eqIdx);
813
- const depValue = (dependency as string).substring(eqIdx + 1);
814
- const optValue = options?.[depName]?.value;
815
- return optValue != null && String(optValue) === depValue;
816
- }
1890
+ return (dependency as string[]).some((dep) =>
1891
+ isDependencyFulfilled(dep, options, optionsSchema)
1892
+ );
817
1893
  }
818
1894
 
819
- return options?.[dependency as string]
820
- ? validateField(
821
- options[dependency as string].type,
822
- options[dependency as string].value,
823
- {
824
- ...(optionsSchema?.[dependency as string] as unknown as IFieldValidationProps),
825
- options,
826
- optionSchema: optionsSchema,
827
- }
828
- )
1895
+ return isString(dependency)
1896
+ ? isDependencyFulfilled(dependency as string, options, optionsSchema)
829
1897
  : true;
830
1898
  });
831
1899
  };