@qoretechnologies/reqraft 0.10.53 → 0.10.54-pr.115.ge2d364c

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 (215) hide show
  1. package/.claude/CLAUDE.md +25 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
  4. package/README.md +1 -1
  5. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  6. package/design/RENDER_EXPRESSION_TRANSPORT.md +18 -0
  7. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  8. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  9. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  10. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  11. package/dist/components/dpqlEditor/index.d.ts +4 -0
  12. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  13. package/dist/components/dpqlEditor/index.js +5 -1
  14. package/dist/components/dpqlEditor/index.js.map +1 -1
  15. package/dist/components/dpqlEditor/types.d.ts +29 -2
  16. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  17. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  18. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  19. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  20. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  21. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  22. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  23. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  24. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  25. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  26. package/dist/components/form/engine/CompactRow.js +186 -21
  27. package/dist/components/form/engine/CompactRow.js.map +1 -1
  28. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  29. package/dist/components/form/engine/CompactToolbar.js +5 -2
  30. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  31. package/dist/components/form/engine/FormEngine.d.ts +159 -11
  32. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  33. package/dist/components/form/engine/FormEngine.js +376 -75
  34. package/dist/components/form/engine/FormEngine.js.map +1 -1
  35. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  36. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  37. package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
  38. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  39. package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
  40. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  41. package/dist/components/form/engine/OptionFieldMessages.js +61 -11
  42. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  43. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  44. package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
  45. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  46. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  47. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  48. package/dist/components/form/engine/readFirst.d.ts +42 -1
  49. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  50. package/dist/components/form/engine/readFirst.js +74 -7
  51. package/dist/components/form/engine/readFirst.js.map +1 -1
  52. package/dist/components/form/engine/rendererTypes.d.ts +13 -0
  53. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  54. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  55. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  56. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  57. package/dist/components/form/engine/rowMenuContext.js +103 -0
  58. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  59. package/dist/components/form/engine/typeChoices.d.ts +37 -0
  60. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  61. package/dist/components/form/engine/typeChoices.js +25 -0
  62. package/dist/components/form/engine/typeChoices.js.map +1 -0
  63. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  64. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  65. package/dist/components/form/expressions/ExpressionField.js +181 -33
  66. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  67. package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
  68. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  69. package/dist/components/form/expressions/argumentPresence.js +203 -0
  70. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  71. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  72. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  73. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  74. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  75. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  76. package/dist/components/form/expressions/builder/index.js +19 -57
  77. package/dist/components/form/expressions/builder/index.js.map +1 -1
  78. package/dist/components/form/expressions/dpqlMockLsp.d.ts +17 -0
  79. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
  80. package/dist/components/form/expressions/dpqlMockLsp.js +75 -9
  81. package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
  82. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  83. package/dist/components/form/expressions/renderExpressionToText.js +94 -19
  84. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  85. package/dist/components/form/fields/Field.d.ts.map +1 -1
  86. package/dist/components/form/fields/Field.js +3 -1
  87. package/dist/components/form/fields/Field.js.map +1 -1
  88. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  89. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  90. package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
  91. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  92. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  93. package/dist/components/form/fields/auto/AutoFormField.js +13 -2
  94. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  95. package/dist/components/form/fields/rich-text/RichText.d.ts +11 -0
  96. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  97. package/dist/components/form/fields/rich-text/RichText.js +84 -5
  98. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  99. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  100. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  101. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  102. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  103. package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
  104. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  105. package/dist/components/form/fields/template/TemplateField.js +507 -40
  106. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  107. package/dist/components/form/index.d.ts +1 -0
  108. package/dist/components/form/index.d.ts.map +1 -1
  109. package/dist/components/form/index.js +1 -0
  110. package/dist/components/form/index.js.map +1 -1
  111. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +1 -0
  112. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
  113. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +6 -1
  114. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
  115. package/dist/helpers/dpqlDetection.d.ts +58 -0
  116. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  117. package/dist/helpers/dpqlDetection.js +164 -0
  118. package/dist/helpers/dpqlDetection.js.map +1 -0
  119. package/dist/helpers/optionUiTypes.d.ts +1 -1
  120. package/dist/helpers/options.d.ts +37 -0
  121. package/dist/helpers/options.d.ts.map +1 -1
  122. package/dist/helpers/options.js +78 -1
  123. package/dist/helpers/options.js.map +1 -1
  124. package/dist/helpers/templateItems.d.ts +47 -0
  125. package/dist/helpers/templateItems.d.ts.map +1 -0
  126. package/dist/helpers/templateItems.js +88 -0
  127. package/dist/helpers/templateItems.js.map +1 -0
  128. package/dist/helpers/templates.d.ts +21 -0
  129. package/dist/helpers/templates.d.ts.map +1 -1
  130. package/dist/helpers/templates.js +49 -3
  131. package/dist/helpers/templates.js.map +1 -1
  132. package/dist/helpers/validations.d.ts.map +1 -1
  133. package/dist/helpers/validations.js +56 -9
  134. package/dist/helpers/validations.js.map +1 -1
  135. package/dist/hooks/useArgSchema.d.ts +2 -1
  136. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  137. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  138. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  139. package/dist/hooks/useFetch/useFetch.js +47 -9
  140. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  141. package/dist/hooks/useTemplates.d.ts +20 -0
  142. package/dist/hooks/useTemplates.d.ts.map +1 -1
  143. package/dist/hooks/useTemplates.js +34 -2
  144. package/dist/hooks/useTemplates.js.map +1 -1
  145. package/dist/index.d.ts +2 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +5 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/stories/storyNetwork.d.ts +36 -0
  150. package/dist/stories/storyNetwork.d.ts.map +1 -0
  151. package/dist/stories/storyNetwork.js +82 -0
  152. package/dist/stories/storyNetwork.js.map +1 -0
  153. package/dist/utils/fetch.d.ts +151 -1
  154. package/dist/utils/fetch.d.ts.map +1 -1
  155. package/dist/utils/fetch.js +256 -44
  156. package/dist/utils/fetch.js.map +1 -1
  157. package/dist/utils/lspClient.d.ts +6 -0
  158. package/dist/utils/lspClient.d.ts.map +1 -1
  159. package/dist/utils/lspClient.js +62 -11
  160. package/dist/utils/lspClient.js.map +1 -1
  161. package/dist/utils/websocket.d.ts +6 -0
  162. package/dist/utils/websocket.d.ts.map +1 -1
  163. package/dist/utils/websocket.js +15 -7
  164. package/dist/utils/websocket.js.map +1 -1
  165. package/package.json +2 -2
  166. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +0 -2
  167. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  168. package/src/components/dpqlEditor/index.ts +4 -0
  169. package/src/components/dpqlEditor/types.ts +26 -2
  170. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  171. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  172. package/src/components/form/engine/CompactRow.tsx +231 -11
  173. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  174. package/src/components/form/engine/FormEngine.stories.tsx +331 -30
  175. package/src/components/form/engine/FormEngine.tsx +504 -87
  176. package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
  177. package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
  178. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
  179. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  180. package/src/components/form/engine/readFirst.ts +86 -7
  181. package/src/components/form/engine/rendererTypes.ts +12 -0
  182. package/src/components/form/engine/rowMenuContext.ts +159 -0
  183. package/src/components/form/engine/typeChoices.ts +55 -0
  184. package/src/components/form/expressions/ExpressionField.stories.tsx +828 -31
  185. package/src/components/form/expressions/ExpressionField.tsx +234 -29
  186. package/src/components/form/expressions/argumentPresence.ts +200 -0
  187. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  188. package/src/components/form/expressions/builder/index.tsx +31 -75
  189. package/src/components/form/expressions/dpqlMockLsp.ts +81 -1
  190. package/src/components/form/expressions/renderExpressionToText.ts +106 -17
  191. package/src/components/form/fields/Field.tsx +3 -2
  192. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
  193. package/src/components/form/fields/auto/AutoFormField.tsx +12 -2
  194. package/src/components/form/fields/rich-text/RichText.tsx +112 -19
  195. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  196. package/src/components/form/fields/template/TemplateField.tsx +667 -95
  197. package/src/components/form/index.tsx +1 -0
  198. package/src/components/log/Log.stories.tsx +2 -1
  199. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +6 -1
  200. package/src/helpers/dpqlDetection.ts +209 -0
  201. package/src/helpers/options.ts +70 -0
  202. package/src/helpers/templateItems.ts +81 -0
  203. package/src/helpers/templates.ts +51 -2
  204. package/src/helpers/validations.ts +59 -9
  205. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  206. package/src/hooks/useFetch/useFetch.tsx +53 -7
  207. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  208. package/src/hooks/useTemplates.ts +41 -0
  209. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  210. package/src/index.tsx +14 -1
  211. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  212. package/src/stories/storyNetwork.ts +92 -0
  213. package/src/utils/fetch.ts +311 -21
  214. package/src/utils/lspClient.ts +68 -11
  215. package/src/utils/websocket.ts +20 -9
@@ -24,6 +24,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
24
24
  import styled from 'styled-components';
25
25
  import { moveItem } from '../../../../helpers/common';
26
26
  import { areQorusTypesCompatible, getArgumentType } from '../../../../helpers/expressions';
27
+ import { addMissingExpressionArgs } from '../argumentPresence';
27
28
  import { findTemplate } from '../../../../helpers/templates';
28
29
  import { validateField, validateFieldWithResult } from '../../../../helpers/validations';
29
30
  import { IQorusTypeObject, useQorusTypes } from '../../../../hooks/useQorusTypes';
@@ -76,6 +77,18 @@ export interface IExpressionBuilderProps {
76
77
  expressions?: IExpressionSchema[];
77
78
  expressionsUrl?: string;
78
79
  serverHandled?: boolean;
80
+ /**
81
+ * SEAM (reqraft): the host's per-`ui_type` editors, forwarded to the operand
82
+ * fields below.
83
+ *
84
+ * Without them an operand whose type is one of the CONSUMER's own ui_types
85
+ * falls through `AutoFormField`'s switch to the literal "Unknown type!" tag.
86
+ * That is what an assertion's `Value` did the moment it was turned into an
87
+ * expression: the field is `test-reference`, an IDE ui_type whose editor the
88
+ * IDE registers through `componentOverrides`, and the builder rendered its
89
+ * operands without them.
90
+ */
91
+ componentOverrides?: Record<string, React.FC<any>>;
79
92
  size?: string;
80
93
  /**
81
94
  * SEAM (reqraft): extra hover actions prepended to each expression card —
@@ -129,6 +142,7 @@ export const Expression = ({
129
142
  expressionsUrl,
130
143
  serverHandled,
131
144
  extraActions,
145
+ componentOverrides,
132
146
  ...props
133
147
  }: IExpressionProps) => {
134
148
  const types = useQorusTypes();
@@ -264,58 +278,8 @@ export const Expression = ({
264
278
  );
265
279
 
266
280
  const addMissingArgs = useCallback(
267
- (expression: string, args: IExpression[] = []): IExpression[] => {
268
- const newArgs = [...args];
269
- // Check if this expression has variable arguments
270
- const selectedExpression = expressions.value?.find((exp) => exp.name === expression);
271
-
272
- if (selectedExpression.varargs) {
273
- if (selectedExpression.subtype === 2) {
274
- newArgs.push({
275
- is_expression: true,
276
- value: {
277
- args: [],
278
- },
279
- });
280
- } else {
281
- // Add the number of arguments equal to min_args - 1
282
- newArgs.push(
283
- ...Array.from({ length: selectedExpression.min_args - 1 }, () => ({
284
- type:
285
- selectedExpression.args[0].ui_type === 'richtext' ||
286
- selectedExpression.args[0].ui_type === 'number'
287
- ? selectedExpression.args[0].ui_type
288
- : undefined,
289
- }))
290
- );
291
- }
292
- } else {
293
- // Check if any of the arguments have a default value
294
- selectedExpression.args.forEach((arg, index) => {
295
- if (
296
- newArgs[index] === undefined ||
297
- size(newArgs[index]) === 0 ||
298
- (arg.default_value && 'value' in newArgs[index] === false)
299
- ) {
300
- if (arg.default_value) {
301
- newArgs[index] = {
302
- value: arg.default_value,
303
- type: arg.ui_type,
304
- };
305
- } else {
306
- newArgs[index] = {
307
- type:
308
- arg.ui_type === 'richtext' || arg.ui_type === 'number' || arg.ui_type === 'bool'
309
- ? arg.ui_type
310
- : undefined,
311
- };
312
- }
313
- }
314
- });
315
- }
316
-
317
- return newArgs;
318
- },
281
+ (expression: string, args: IExpression[] = []): IExpression[] =>
282
+ addMissingExpressionArgs(expressions.value as never, expression, args as never) as never,
319
283
  [JSON.stringify(expressions.value)]
320
284
  );
321
285
 
@@ -501,13 +465,7 @@ export const Expression = ({
501
465
  );
502
466
 
503
467
  const updateArg = useCallback(
504
- (
505
- val: any,
506
- index: number = 0,
507
- type?: string,
508
- isFunction?: boolean,
509
- isRequired?: boolean
510
- ) => {
468
+ (val: any, index: number = 0, type?: string, isFunction?: boolean) => {
511
469
  const args = clone(value.value.args);
512
470
  const newVal = clone(val);
513
471
 
@@ -515,12 +473,18 @@ export const Expression = ({
515
473
  newVal.args = addMissingArgs(newVal.exp, newVal.args);
516
474
  }
517
475
 
476
+ /* `required` is NOT written here. It is a schema key — it describes the
477
+ catalogue's declaration of the argument, not the author's value, and
478
+ nothing reads it back off a stored argument (validation takes it from
479
+ the schema). Persisting it made a cleared argument indistinguishable
480
+ from real data: the value dropped out as `undefined`, `required: true`
481
+ stayed, and what was saved to the draft was `{type, required}` — an
482
+ argument DEFINITION sitting where its value should be. */
518
483
  args[index] = {
519
484
  ...args[index],
520
485
  value: newVal,
521
486
  type: !isFunction ? type || args[index]?.type : undefined,
522
487
  is_expression: isFunction,
523
- required: isRequired,
524
488
  };
525
489
 
526
490
  onValueChange(
@@ -683,16 +647,10 @@ export const Expression = ({
683
647
  const handleFirstParamChange = useCallback(
684
648
  (_name, value, type, isFunction) => {
685
649
  if (isFunction || (type !== 'any' && type !== 'auto')) {
686
- updateArg(
687
- value,
688
- 0,
689
- isFunction ? undefined : type,
690
- isFunction,
691
- selectedExpression?.args?.[0]?.required
692
- );
650
+ updateArg(value, 0, isFunction ? undefined : type, isFunction);
693
651
  }
694
652
  },
695
- [updateArg, JSON.stringify(selectedExpression)]
653
+ [updateArg]
696
654
  );
697
655
 
698
656
  const buildCustomTemplateItems = useCallback(
@@ -1013,6 +971,7 @@ export const Expression = ({
1013
971
  >
1014
972
  <TemplateField
1015
973
  component={auto}
974
+ componentOverrides={componentOverrides}
1016
975
  // SEAM (reqraft): an operand that becomes an expression mounts
1017
976
  // its own builder through this TemplateField — hand the host's
1018
977
  // actions on, or nesting silently drops them (the group
@@ -1132,6 +1091,7 @@ export const Expression = ({
1132
1091
  <TemplateField
1133
1092
  minimal
1134
1093
  component={auto}
1094
+ componentOverrides={componentOverrides}
1135
1095
  // SEAM (reqraft): same as the first operand above.
1136
1096
  extraActions={extraActions}
1137
1097
  noSoft
@@ -1166,13 +1126,7 @@ export const Expression = ({
1166
1126
  !!arg.default_value
1167
1127
  }
1168
1128
  onChange={((_name, value, type, isFunction) => {
1169
- updateArg(
1170
- value,
1171
- index + 1,
1172
- isFunction ? undefined : type,
1173
- isFunction,
1174
- arg.required
1175
- );
1129
+ updateArg(value, index + 1, isFunction ? undefined : type, isFunction);
1176
1130
  }) as any}
1177
1131
  fluid={false}
1178
1132
  fixed={true}
@@ -1248,6 +1202,7 @@ export const ExpressionBuilder = ({
1248
1202
  expressionsUrl,
1249
1203
  serverHandled,
1250
1204
  extraActions,
1205
+ componentOverrides,
1251
1206
  reorder,
1252
1207
  }: IExpressionBuilderProps) => {
1253
1208
  const templates = useTemplates(!isChild, localTemplates);
@@ -1432,6 +1387,7 @@ export const ExpressionBuilder = ({
1432
1387
  id={level === 0 && index === 0 ? 'expression-builder' : undefined}
1433
1388
  serverHandled={serverHandled}
1434
1389
  extraActions={extraActions}
1390
+ componentOverrides={componentOverrides}
1435
1391
  reorder={reorder}
1436
1392
  />
1437
1393
  </ReqoreErrorBoundary>
@@ -19,8 +19,41 @@ interface IMockExprNode {
19
19
  is_expression?: boolean;
20
20
  }
21
21
 
22
+ /**
23
+ * Every `dpql/parse` the mock answered, in order, for diagnosis.
24
+ *
25
+ * The type-analysis stories depend on the front end SENDING `target_type` —
26
+ * the mock returns analysis only when it is present, exactly as the server
27
+ * behaves. When one of those stories fails there are two very different
28
+ * causes, and the rendered DOM cannot tell them apart: the request never
29
+ * carried a target, or it did and the answer was not rendered. This records
30
+ * which, and the story reports it in its assertion message so the answer
31
+ * survives into a CI log.
32
+ */
33
+ export interface IDpqlMockParseCall {
34
+ text: string;
35
+ target?: string;
36
+ analysed: boolean;
37
+ }
38
+ export const dpqlMockParseCalls: IDpqlMockParseCall[] = [];
39
+
40
+ /** The template namespaces the server offers after a `$`. */
41
+ const DPQL_MOCK_TEMPLATE_ITEMS = [
42
+ { label: '$data:', insertText: '$data:', kind: 1, detail: 'template' },
43
+ { label: '$config:', insertText: '$config:', kind: 1, detail: 'template' },
44
+ { label: '$static:', insertText: '$static:', kind: 1, detail: 'template' },
45
+ { label: '$timestamp:', insertText: '$timestamp:', kind: 1, detail: 'template' },
46
+ ];
47
+
48
+ /** The record fields the server offers after an `@`. */
49
+ const DPQL_MOCK_FIELD_ITEMS = [
50
+ { label: '@name', insertText: '@name', kind: 5, detail: 'string' },
51
+ { label: '@status', insertText: '@status', kind: 5, detail: 'string' },
52
+ ];
53
+
22
54
  /** Start the mock LSP; returns a teardown function. */
23
55
  export const startDpqlMockLsp = (): (() => void) => {
56
+ dpqlMockParseCalls.length = 0;
24
57
  const lsp = createMockLspServer(MOCK_LSP_URL, {
25
58
  capabilities: {
26
59
  textDocumentSync: { openClose: true, change: 2 },
@@ -29,7 +62,25 @@ export const startDpqlMockLsp = (): (() => void) => {
29
62
  // Unknown requests get an empty success so nothing hangs.
30
63
  defaultResult: {},
31
64
  handlers: {
32
- 'textDocument/completion': () => ({ isIncomplete: false, items: [] }),
65
+ /* Position-aware, as the real server is: `$` opens the template
66
+ namespaces, `@` the record fields. Answering the same list whatever
67
+ precedes the cursor would make a story asserting the `$` menu pass
68
+ for a shell that asked in the wrong context — which is the only thing
69
+ such a story is there to catch. (`DpqlEditor`'s own stories carry the
70
+ full catalogue; this is the minimal subset.) */
71
+ 'textDocument/completion': (msg, server) => {
72
+ const position = msg.params?.position ?? { line: 0, character: 0 };
73
+ const line = server.documentText.split('\n')[position.line] ?? '';
74
+ const head = line.slice(0, position.character);
75
+ const sigil = head.match(/[@$][\w:.{}]*$/)?.[0]?.[0] ?? '';
76
+ if (sigil === '$') {
77
+ return { isIncomplete: false, items: DPQL_MOCK_TEMPLATE_ITEMS };
78
+ }
79
+ if (sigil === '@') {
80
+ return { isIncomplete: false, items: DPQL_MOCK_FIELD_ITEMS };
81
+ }
82
+ return { isIncomplete: false, items: [] };
83
+ },
33
84
 
34
85
  'textDocument/semanticTokens/full': () => ({ data: [] }),
35
86
 
@@ -39,6 +90,34 @@ export const startDpqlMockLsp = (): (() => void) => {
39
90
  // deterministic and verifiable.
40
91
  'dpql/parse': (msg) => {
41
92
  const text = (msg.params?.text ?? '').trim();
93
+ // Type analysis is returned ONLY when the request carried a
94
+ // `target_type`, exactly as the server behaves. That makes it a real
95
+ // check rather than a fixture: a front end that stops sending the
96
+ // target gets no analysis, and the stories asserting the message go
97
+ // red instead of quietly passing.
98
+ const target = msg.params?.target_type as string | undefined;
99
+ const inferred = 'string';
100
+ const analysis =
101
+ target ?
102
+ {
103
+ inferred_type: inferred,
104
+ target_type: target,
105
+ type_compatible: target === inferred || target === 'auto',
106
+ auto_coercible: true,
107
+ // "text used as a number" is the attempted-but-not-guaranteed
108
+ // conversion; "text used as text" needs no conversion at all.
109
+ coercion_may_fail: target === 'int' || target === 'float',
110
+ suggested_fix:
111
+ target === inferred || target === 'auto' ?
112
+ undefined
113
+ : { text: `to${target[0].toUpperCase()}${target.slice(1)}(${text})` },
114
+ }
115
+ : {};
116
+ dpqlMockParseCalls.push({
117
+ text,
118
+ target,
119
+ analysed: !!target,
120
+ });
42
121
  return {
43
122
  success: true,
44
123
  expression: {
@@ -51,6 +130,7 @@ export const startDpqlMockLsp = (): (() => void) => {
51
130
  ],
52
131
  },
53
132
  },
133
+ ...analysis,
54
134
  diagnostics: [],
55
135
  };
56
136
  },
@@ -10,6 +10,67 @@ import { IExpression, IExpressionSchema, IExpressionValue } from './types';
10
10
  const isOperatorSymbol = (symbol?: string): boolean =>
11
11
  !!symbol && /^[^\w\s]+$/.test(symbol);
12
12
 
13
+ /**
14
+ * Operator precedence, lower binds tighter.
15
+ *
16
+ * Mirrors `DpqlOperatorPrecedence` in the Qore DataProvider module, which is
17
+ * the source of truth — the server renders the same ASTs through
18
+ * `DataProvider::renderExpression`, and an offline fallback that parenthesised
19
+ * differently would make the same expression read two ways depending on
20
+ * whether the instance happened to be reachable.
21
+ */
22
+ const PRECEDENCE: Record<string, number> = {
23
+ '*': 1,
24
+ '/': 1,
25
+ '%': 1,
26
+ '+': 2,
27
+ '-': 2,
28
+ '!': 3,
29
+ '==': 4,
30
+ '!=': 4,
31
+ '<': 4,
32
+ '<=': 4,
33
+ '>': 4,
34
+ '>=': 4,
35
+ '&&': 5,
36
+ '||': 6,
37
+ };
38
+
39
+ /** Anything that is not an operator; nothing binds tighter. */
40
+ const PREC_PRIMARY = 0;
41
+ /** Looser than every operator, so a value rendered in it is never wrapped. */
42
+ const PREC_LOOSEST = 7;
43
+
44
+ /**
45
+ * Operators whose grouping cannot change the result.
46
+ *
47
+ * `+` is deliberately absent: it concatenates when an operand is a string, so
48
+ * `1 + (2 + "a")` is `"12a"` where `1 + 2 + "a"` is `"3a"`. Dropping its
49
+ * parentheses would not be a cosmetic simplification.
50
+ */
51
+ const ASSOCIATIVE = new Set(['&&', '||']);
52
+
53
+ const precedenceOf = (exp?: string): number => PRECEDENCE[exp ?? ''] ?? PREC_PRIMARY;
54
+
55
+ /**
56
+ * The context an operator's argument is rendered in — the same rule as
57
+ * `DpqlSerializer::getArgumentContext()`.
58
+ *
59
+ * The first argument is where left-associative parsing puts a same-precedence
60
+ * child back, so it needs no parentheses; a later one does, or `1 - (2 - 3)`
61
+ * reads as `1 - 2 - 3`, which is a different expression.
62
+ */
63
+ const argumentContext = (exp: string, argn: number): number => {
64
+ const prec = precedenceOf(exp);
65
+ return !argn || ASSOCIATIVE.has(exp) ? prec : prec - 1;
66
+ };
67
+
68
+ /** An argument reaches this as a bare value or as a `{type, value}` envelope. */
69
+ const readArgValue = (arg: unknown): unknown =>
70
+ arg !== null && typeof arg === 'object' && 'value' in (arg as object)
71
+ ? (arg as { value: unknown }).value
72
+ : arg;
73
+
13
74
  const renderLiteral = (value: unknown): string => {
14
75
  if (value === undefined || value === null) return 'null';
15
76
  if (typeof value === 'string') return JSON.stringify(value);
@@ -17,31 +78,59 @@ const renderLiteral = (value: unknown): string => {
17
78
  return String(value);
18
79
  };
19
80
 
20
- /**
21
- * Recursively render an expression AST to readable text — the client-side
22
- * approximation, used as the fallback when the LSP is unreachable.
23
- */
24
- export const renderExpressionToText = (
81
+ /** Renders `value` and wraps it only if it binds looser than `contextPrec`. */
82
+ const renderInContext = (
25
83
  value: IExpressionValue | undefined,
26
- expressions: IExpressionSchema[] = []
84
+ expressions: IExpressionSchema[],
85
+ contextPrec: number
27
86
  ): string => {
28
87
  if (!value?.exp) return '';
88
+ /* A `value` expression IS its literal.
89
+
90
+ It is the wrapper the parser puts around a bare literal — typing `null`,
91
+ or `42`, into the text editor produces one — and it carries no symbol, so
92
+ rendering it through the generic paths printed the wrapper instead of the
93
+ value: `value(42)` with a catalogue that names it, `(42)` with one that
94
+ supplies its empty symbol. The server renders the literal alone, and a
95
+ summary that disagrees with the preview beside it is worse than either. */
96
+ if (value.exp === 'value' && (value.args ?? []).length === 1) {
97
+ const only = (value.args ?? [])[0] as IExpression;
98
+ if (!only?.is_expression) {
99
+ return renderLiteral(readArgValue(only));
100
+ }
101
+ }
29
102
  const schema = expressions.find((e) => e.name === value.exp);
30
103
  const symbol = schema?.symbol ?? value.exp;
31
- const args = (value.args ?? []).map((arg: IExpression) =>
104
+ const args = value.args ?? [];
105
+ // Logical group (subtype 2) → join with the symbol.
106
+ const isLogicalGroup = schema?.subtype === 2 || value.exp === '&&' || value.exp === '||';
107
+ // Binary operator → infix. Everything else renders as a call, whose own
108
+ // parentheses and commas already delimit its arguments.
109
+ const isInfix = isLogicalGroup || (args.length === 2 && isOperatorSymbol(symbol));
110
+
111
+ const parts = args.map((arg: IExpression, argn: number) =>
32
112
  arg?.is_expression && arg.value?.exp
33
- ? `(${renderExpressionToText(arg.value, expressions)})`
113
+ ? renderInContext(
114
+ arg.value,
115
+ expressions,
116
+ isInfix ? argumentContext(value.exp as string, argn) : PREC_LOOSEST
117
+ )
34
118
  : renderLiteral(arg?.value)
35
119
  );
36
120
 
37
- // Logical group (subtype 2) → join with the symbol.
38
- if (schema?.subtype === 2 || value.exp === '&&' || value.exp === '||') {
39
- return args.join(` ${symbol} `);
121
+ if (!isInfix) {
122
+ return `${symbol}(${parts.join(', ')})`;
40
123
  }
41
- // Binary operator → infix.
42
- if (args.length === 2 && isOperatorSymbol(symbol)) {
43
- return `${args[0]} ${symbol} ${args[1]}`;
44
- }
45
- // Otherwise function form.
46
- return `${symbol}(${args.join(', ')})`;
124
+
125
+ const text = isLogicalGroup ? parts.join(` ${symbol} `) : `${parts[0]} ${symbol} ${parts[1]}`;
126
+ return precedenceOf(value.exp) > contextPrec ? `(${text})` : text;
47
127
  };
128
+
129
+ /**
130
+ * Recursively render an expression AST to readable text — the client-side
131
+ * approximation, used as the fallback when the LSP is unreachable.
132
+ */
133
+ export const renderExpressionToText = (
134
+ value: IExpressionValue | undefined,
135
+ expressions: IExpressionSchema[] = []
136
+ ): string => renderInContext(value, expressions, PREC_LOOSEST);
@@ -2,7 +2,6 @@ import {
2
2
  IReqoreLabelProps,
3
3
  ReqoreCheckbox,
4
4
  ReqoreControlGroup,
5
- ReqoreSkeleton,
6
5
  ReqoreSpan,
7
6
  } from '@qoretechnologies/reqore';
8
7
  import { IWithReqoreSize } from '@qoretechnologies/reqore/dist/types/global';
@@ -38,6 +37,7 @@ import { IDataSchemaDefinition } from './schema-definition/types';
38
37
  // Direct import — circular dep (FormEngine → TemplateField → FormField → FormEngine) is safe
39
38
  // because modules are all loaded before any component renders
40
39
  import { FormEngine } from '../engine/FormEngine';
40
+ import { FormFieldsSkeleton } from '../engine/FormFieldsSkeleton';
41
41
 
42
42
  /** Soft Qore types (softint, softstring, …) render as their concrete base —
43
43
  * the soft/hard distinction is value-level, not UI. */
@@ -345,7 +345,8 @@ export const FormField = <T extends TFormFieldType>({
345
345
 
346
346
  case 'hash': {
347
347
  if (argSchemaLoading) {
348
- return <ReqoreSkeleton height='80px' />;
348
+ // A hash renders a nested FORM, so it waits in the shape of one.
349
+ return <FormFieldsSkeleton rows={2} />;
349
350
  }
350
351
  if (finalArgSchema) {
351
352
  return (
@@ -40,13 +40,23 @@ const DISALLOW_COMPACT_AO_TYPES = ['connection'];
40
40
  * Whether a creatable field's value is carried by the chip picker rather than
41
41
  * by a raw editor with a suggestion list beside it.
42
42
  *
43
- * `string` only, and only with something to suggest. Every other shape a
43
+ * String-valued only, and only with something to suggest. Every other shape a
44
44
  * creatable field can declare either does not fit a one-line chip (a document
45
45
  * — `long-string`, `binary`, `data`; a hash; a list) or would come back out of
46
46
  * one as a string (a number, a date), and a field with nothing to offer is
47
47
  * better served by the editor it has always had. Those keep today's rendering:
48
48
  * the raw editor, with the saved-and-suggested picker under it.
49
49
  *
50
+ * `select-string` counts, and leaving it out was the bug. A picker field
51
+ * declares its storage type (`*string`) and its `ui_type` (`select-string`),
52
+ * and by the time a schema reaches here the two have been flattened to one:
53
+ * `type` arrives as `select-string`. So this returned false for every such
54
+ * field and NEITHER renderer stood down — the picker drew the value, and the
55
+ * saved-and-suggested list drew the same candidates again underneath it. A
56
+ * test's Service and Method fields showed 183 and 7 items twice over, in two
57
+ * controls that set one value. A select-string's value IS a string and fits the
58
+ * chip, which is the only thing this predicate is really asking.
59
+ *
50
60
  * Both the renderer of the picker (`FieldAllowedValues`) and the renderer of
51
61
  * the raw editor (`AutoFormField`, `FormField`) ask this, so that exactly one
52
62
  * of them draws the value.
@@ -55,7 +65,8 @@ export const rendersCreatableValueSelect = (
55
65
  type?: string,
56
66
  allowCreation?: boolean,
57
67
  items?: unknown[]
58
- ): boolean => !!allowCreation && type === 'string' && count(items) > 0;
68
+ ): boolean =>
69
+ !!allowCreation && (type === 'string' || type === 'select-string') && count(items) > 0;
59
70
 
60
71
  export const FieldAllowedValuesCheckGroup = memo(
61
72
  ({
@@ -7,7 +7,6 @@ import {
7
7
  ReqoreControlGroup,
8
8
  ReqoreErrorBoundary,
9
9
  ReqoreMessage,
10
- ReqoreSpinner,
11
10
  ReqoreTag,
12
11
  } from '@qoretechnologies/reqore';
13
12
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
@@ -53,6 +52,7 @@ import { IDataSchemaDefinition } from '../schema-definition/types';
53
52
  import { ISelectFormFieldItem, SelectFormField } from '../select/Select';
54
53
  import { StringFormField } from '../string/String';
55
54
  import { UrlFormField } from '../url/Url';
55
+ import { FormFieldsSkeleton } from '../../engine/FormFieldsSkeleton';
56
56
 
57
57
  /** UI superset of `TQorusType` — the IDE's `Field/systemOptions` `IQorusType`. */
58
58
  export type IQorusType = TQorusType | string;
@@ -395,6 +395,13 @@ function AutoField<T = any>({
395
395
  arg_schema={arg_schema}
396
396
  element_type={element_type}
397
397
  ui_element_type={ui_element_type}
398
+ // Same reason as the three above: it is destructured out of `rest` so the
399
+ // primitive renderers do not spread it onto a DOM node, which also took
400
+ // it away from host editors — and a host editor is exactly what
401
+ // `inherit_props` exists to feed. A field declaring it got the values
402
+ // resolved and then had nowhere to send them, so every consumer
403
+ // downstream saw `undefined` and silently did nothing.
404
+ inheritedFromParent={inheritedFromParent}
398
405
  name={name}
399
406
  value={value}
400
407
  onChange={(val: any, emittedType?: IQorusType, emittedIsFunction?: boolean) => {
@@ -457,7 +464,10 @@ function AutoField<T = any>({
457
464
  }
458
465
 
459
466
  if (arg_schema && !finalArgSchema) {
460
- return <ReqoreSpinner size='small'>Loading field data...</ReqoreSpinner>;
467
+ /* One ROW, because this stands in for one field. It was a spinner reading
468
+ "Loading field data…", which is a different picture from every other
469
+ wait on the page and announces a delay the reader cannot act on. */
470
+ return <FormFieldsSkeleton rows={1} />;
461
471
  }
462
472
 
463
473
  const renderAllowedValues = (currentType: IQorusType) => {