@qoretechnologies/reqraft 0.10.55-pr.124.g43fa138 → 0.10.55

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 (375) 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_COMPACT.md +34 -0
  6. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  7. package/design/FORM_ENGINE_FIELD_MIGRATION.md +155 -1
  8. package/design/IDE_INTEGRATION.md +13 -0
  9. package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
  10. package/design/SELECT_DIALOG_FLOOR.md +86 -0
  11. package/design/SMART_EDITOR_UX.md +21 -0
  12. package/design/UNAVAILABLE_ALLOWED_VALUES.md +226 -0
  13. package/dist/components/composer/Composer.d.ts +8 -3
  14. package/dist/components/composer/Composer.d.ts.map +1 -1
  15. package/dist/components/composer/Composer.js.map +1 -1
  16. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  17. package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
  18. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  19. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
  20. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  21. package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
  22. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  23. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  24. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  25. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  26. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  27. package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
  28. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  29. package/dist/components/dpqlEditor/dpqlTags.js +5 -1
  30. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  31. package/dist/components/dpqlEditor/index.d.ts +4 -0
  32. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  33. package/dist/components/dpqlEditor/index.js +5 -1
  34. package/dist/components/dpqlEditor/index.js.map +1 -1
  35. package/dist/components/dpqlEditor/types.d.ts +42 -2
  36. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  37. package/dist/components/dpqlEditor/types.js +0 -4
  38. package/dist/components/dpqlEditor/types.js.map +1 -1
  39. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  40. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  41. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  42. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  43. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  44. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  45. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  46. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  47. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  48. package/dist/components/form/engine/CompactRow.js +414 -128
  49. package/dist/components/form/engine/CompactRow.js.map +1 -1
  50. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  51. package/dist/components/form/engine/CompactToolbar.js +5 -2
  52. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  53. package/dist/components/form/engine/FormEngine.d.ts +273 -22
  54. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  55. package/dist/components/form/engine/FormEngine.js +669 -192
  56. package/dist/components/form/engine/FormEngine.js.map +1 -1
  57. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  58. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  59. package/dist/components/form/engine/FormFieldsSkeleton.js +41 -0
  60. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  61. package/dist/components/form/engine/OptionFieldMessages.d.ts +108 -2
  62. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  63. package/dist/components/form/engine/OptionFieldMessages.js +203 -13
  64. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  65. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  66. package/dist/components/form/engine/_structuredData/SchemaDataView.js +68 -5
  67. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  68. package/dist/components/form/engine/compactRowContext.d.ts +15 -1
  69. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  70. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  71. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  72. package/dist/components/form/engine/compactRowStyles.js +55 -29
  73. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  74. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  75. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  76. package/dist/components/form/engine/optionActions.d.ts +37 -1
  77. package/dist/components/form/engine/optionActions.d.ts.map +1 -1
  78. package/dist/components/form/engine/optionActions.js +71 -1
  79. package/dist/components/form/engine/optionActions.js.map +1 -1
  80. package/dist/components/form/engine/optionsContext.d.ts +22 -0
  81. package/dist/components/form/engine/optionsContext.d.ts.map +1 -0
  82. package/dist/components/form/engine/optionsContext.js +20 -0
  83. package/dist/components/form/engine/optionsContext.js.map +1 -0
  84. package/dist/components/form/engine/readFirst.d.ts +42 -1
  85. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  86. package/dist/components/form/engine/readFirst.js +76 -8
  87. package/dist/components/form/engine/readFirst.js.map +1 -1
  88. package/dist/components/form/engine/rendererTypes.d.ts +42 -3
  89. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  90. package/dist/components/form/engine/rendererTypes.js +3 -1
  91. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  92. package/dist/components/form/engine/rowFocus.d.ts +30 -0
  93. package/dist/components/form/engine/rowFocus.d.ts.map +1 -0
  94. package/dist/components/form/engine/rowFocus.js +93 -0
  95. package/dist/components/form/engine/rowFocus.js.map +1 -0
  96. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  97. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  98. package/dist/components/form/engine/rowMenuContext.js +103 -0
  99. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  100. package/dist/components/form/engine/rowOpenPicker.d.ts +27 -0
  101. package/dist/components/form/engine/rowOpenPicker.d.ts.map +1 -0
  102. package/dist/components/form/engine/rowOpenPicker.js +30 -0
  103. package/dist/components/form/engine/rowOpenPicker.js.map +1 -0
  104. package/dist/components/form/engine/typeChoices.d.ts +42 -0
  105. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  106. package/dist/components/form/engine/typeChoices.js +33 -0
  107. package/dist/components/form/engine/typeChoices.js.map +1 -0
  108. package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
  109. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
  110. package/dist/components/form/expressions/DpqlRendering.js +53 -0
  111. package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
  112. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  113. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  114. package/dist/components/form/expressions/ExpressionField.js +203 -45
  115. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  116. package/dist/components/form/expressions/argumentPresence.d.ts +84 -0
  117. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  118. package/dist/components/form/expressions/argumentPresence.js +217 -0
  119. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  120. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  121. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  122. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  123. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  124. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  125. package/dist/components/form/expressions/builder/index.js +21 -65
  126. package/dist/components/form/expressions/builder/index.js.map +1 -1
  127. package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
  128. package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
  129. package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
  130. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  131. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  132. package/dist/components/form/expressions/mockExpressions.js +8 -4
  133. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  134. package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
  135. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  136. package/dist/components/form/expressions/renderExpressionToText.js +95 -19
  137. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  138. package/dist/components/form/expressions/types.d.ts +17 -2
  139. package/dist/components/form/expressions/types.d.ts.map +1 -1
  140. package/dist/components/form/expressions/useExpressions.d.ts.map +1 -1
  141. package/dist/components/form/expressions/useExpressions.js +5 -2
  142. package/dist/components/form/expressions/useExpressions.js.map +1 -1
  143. package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
  144. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  145. package/dist/components/form/expressions/useRenderExpression.js +50 -126
  146. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  147. package/dist/components/form/fields/Field.d.ts +21 -0
  148. package/dist/components/form/fields/Field.d.ts.map +1 -1
  149. package/dist/components/form/fields/Field.js +54 -8
  150. package/dist/components/form/fields/Field.js.map +1 -1
  151. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  152. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  153. package/dist/components/form/fields/allowed-values/AllowedValues.js +52 -8
  154. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  155. package/dist/components/form/fields/auto/AutoFormField.d.ts +7 -1
  156. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  157. package/dist/components/form/fields/auto/AutoFormField.js +91 -28
  158. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  159. package/dist/components/form/fields/date/Date.d.ts +1 -1
  160. package/dist/components/form/fields/date/Date.d.ts.map +1 -1
  161. package/dist/components/form/fields/date/Date.js +16 -2
  162. package/dist/components/form/fields/date/Date.js.map +1 -1
  163. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  164. package/dist/components/form/fields/long-string/LongString.js +14 -2
  165. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  166. package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
  167. package/dist/components/form/fields/multi-select/MultiSelectFormField.js +50 -4
  168. package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
  169. package/dist/components/form/fields/number/Number.d.ts +12 -1
  170. package/dist/components/form/fields/number/Number.d.ts.map +1 -1
  171. package/dist/components/form/fields/number/Number.js +6 -3
  172. package/dist/components/form/fields/number/Number.js.map +1 -1
  173. package/dist/components/form/fields/rich-text/RichText.d.ts +47 -2
  174. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  175. package/dist/components/form/fields/rich-text/RichText.js +153 -12
  176. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  177. package/dist/components/form/fields/select/Select.d.ts.map +1 -1
  178. package/dist/components/form/fields/select/Select.js +120 -20
  179. package/dist/components/form/fields/select/Select.js.map +1 -1
  180. package/dist/components/form/fields/select/SelectCollection.d.ts +166 -9
  181. package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
  182. package/dist/components/form/fields/select/SelectCollection.js +200 -7
  183. package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
  184. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  185. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  186. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  187. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  188. package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
  189. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  190. package/dist/components/form/fields/template/TemplateField.js +606 -58
  191. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  192. package/dist/components/form/fields/template/TemplateText.d.ts +36 -0
  193. package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -0
  194. package/dist/components/form/fields/template/TemplateText.js +80 -0
  195. package/dist/components/form/fields/template/TemplateText.js.map +1 -0
  196. package/dist/components/form/index.d.ts +2 -0
  197. package/dist/components/form/index.d.ts.map +1 -1
  198. package/dist/components/form/index.js +4 -0
  199. package/dist/components/form/index.js.map +1 -1
  200. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  201. package/dist/components/smartEditor/SmartEditor.js +29 -8
  202. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  203. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  204. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  205. package/dist/components/smartEditor/useLspSession.js +16 -2
  206. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  207. package/dist/components/supportTicket/TicketReplyBox.d.ts +8 -4
  208. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  209. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  210. package/dist/helpers/common.d.ts.map +1 -1
  211. package/dist/helpers/common.js +7 -3
  212. package/dist/helpers/common.js.map +1 -1
  213. package/dist/helpers/dpqlDetection.d.ts +58 -0
  214. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  215. package/dist/helpers/dpqlDetection.js +164 -0
  216. package/dist/helpers/dpqlDetection.js.map +1 -0
  217. package/dist/helpers/optionUiTypes.d.ts +31 -3
  218. package/dist/helpers/optionUiTypes.d.ts.map +1 -1
  219. package/dist/helpers/optionUiTypes.js +36 -1
  220. package/dist/helpers/optionUiTypes.js.map +1 -1
  221. package/dist/helpers/options.d.ts +57 -0
  222. package/dist/helpers/options.d.ts.map +1 -1
  223. package/dist/helpers/options.js +97 -1
  224. package/dist/helpers/options.js.map +1 -1
  225. package/dist/helpers/templateItems.d.ts +47 -0
  226. package/dist/helpers/templateItems.d.ts.map +1 -0
  227. package/dist/helpers/templateItems.js +88 -0
  228. package/dist/helpers/templateItems.js.map +1 -0
  229. package/dist/helpers/templateText.d.ts +80 -0
  230. package/dist/helpers/templateText.d.ts.map +1 -0
  231. package/dist/helpers/templateText.js +291 -0
  232. package/dist/helpers/templateText.js.map +1 -0
  233. package/dist/helpers/templates.d.ts +30 -12
  234. package/dist/helpers/templates.d.ts.map +1 -1
  235. package/dist/helpers/templates.js +91 -33
  236. package/dist/helpers/templates.js.map +1 -1
  237. package/dist/helpers/validations.d.ts +35 -8
  238. package/dist/helpers/validations.d.ts.map +1 -1
  239. package/dist/helpers/validations.js +162 -25
  240. package/dist/helpers/validations.js.map +1 -1
  241. package/dist/hooks/useArgSchema.d.ts +2 -1
  242. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  243. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  244. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  245. package/dist/hooks/useFetch/useFetch.js +47 -9
  246. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  247. package/dist/hooks/useTemplates.d.ts +20 -0
  248. package/dist/hooks/useTemplates.d.ts.map +1 -1
  249. package/dist/hooks/useTemplates.js +34 -2
  250. package/dist/hooks/useTemplates.js.map +1 -1
  251. package/dist/index.d.ts +2 -1
  252. package/dist/index.d.ts.map +1 -1
  253. package/dist/index.js +5 -1
  254. package/dist/index.js.map +1 -1
  255. package/dist/utils/fetch.d.ts +151 -1
  256. package/dist/utils/fetch.d.ts.map +1 -1
  257. package/dist/utils/fetch.js +256 -44
  258. package/dist/utils/fetch.js.map +1 -1
  259. package/dist/utils/lspClient.d.ts +6 -0
  260. package/dist/utils/lspClient.d.ts.map +1 -1
  261. package/dist/utils/lspClient.js +62 -11
  262. package/dist/utils/lspClient.js.map +1 -1
  263. package/dist/utils/websocket.d.ts +6 -0
  264. package/dist/utils/websocket.d.ts.map +1 -1
  265. package/dist/utils/websocket.js +15 -7
  266. package/dist/utils/websocket.js.map +1 -1
  267. package/package.json +10 -6
  268. package/src/components/composer/Composer.tsx +7 -2
  269. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  270. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  271. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  272. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  273. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  274. package/src/components/dpqlEditor/index.ts +4 -0
  275. package/src/components/dpqlEditor/types.ts +38 -2
  276. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  277. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  278. package/src/components/form/engine/CompactRow.tsx +487 -160
  279. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  280. package/src/components/form/engine/FormEngine.stories.tsx +1269 -201
  281. package/src/components/form/engine/FormEngine.tsx +922 -270
  282. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +331 -0
  283. package/src/components/form/engine/FormFieldsSkeleton.tsx +77 -0
  284. package/src/components/form/engine/OptionFieldMessages.tsx +300 -14
  285. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +94 -3
  286. package/src/components/form/engine/compactRowContext.ts +15 -6
  287. package/src/components/form/engine/compactRowStyles.ts +56 -2
  288. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  289. package/src/components/form/engine/optionActions.ts +89 -1
  290. package/src/components/form/engine/optionsContext.ts +24 -0
  291. package/src/components/form/engine/readFirst.ts +88 -8
  292. package/src/components/form/engine/rendererTypes.ts +48 -4
  293. package/src/components/form/engine/rowFocus.ts +101 -0
  294. package/src/components/form/engine/rowMenuContext.ts +159 -0
  295. package/src/components/form/engine/rowOpenPicker.ts +27 -0
  296. package/src/components/form/engine/typeChoices.ts +63 -0
  297. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  298. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  299. package/src/components/form/expressions/ExpressionField.tsx +269 -55
  300. package/src/components/form/expressions/argumentPresence.ts +213 -0
  301. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +38 -21
  302. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  303. package/src/components/form/expressions/builder/index.tsx +39 -81
  304. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  305. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  306. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  307. package/src/components/form/expressions/mockExpressions.ts +8 -4
  308. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  309. package/src/components/form/expressions/types.ts +17 -2
  310. package/src/components/form/expressions/useExpressions.ts +5 -1
  311. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  312. package/src/components/form/fields/Field.tsx +102 -20
  313. package/src/components/form/fields/allowed-values/AllowedValues.tsx +106 -44
  314. package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
  315. package/src/components/form/fields/auto/AutoFormField.tsx +97 -21
  316. package/src/components/form/fields/date/Date.tsx +17 -1
  317. package/src/components/form/fields/long-string/LongString.tsx +15 -3
  318. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +65 -6
  319. package/src/components/form/fields/number/Number.tsx +25 -0
  320. package/src/components/form/fields/rich-text/RichText.tsx +248 -30
  321. package/src/components/form/fields/select/Select.stories.tsx +392 -0
  322. package/src/components/form/fields/select/Select.tsx +164 -27
  323. package/src/components/form/fields/select/SelectCollection.tsx +350 -6
  324. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  325. package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
  326. package/src/components/form/fields/template/TemplateField.tsx +825 -131
  327. package/src/components/form/fields/template/TemplateText.tsx +115 -0
  328. package/src/components/form/index.tsx +4 -0
  329. package/src/components/log/Log.stories.tsx +2 -1
  330. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +3 -3
  331. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  332. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  333. package/src/components/smartEditor/useLspSession.ts +21 -3
  334. package/src/components/supportTicket/TicketReplyBox.tsx +8 -4
  335. package/src/components/ticketThread/TicketThread.stories.tsx +1 -0
  336. package/src/helpers/common.ts +7 -3
  337. package/src/helpers/dpqlDetection.ts +209 -0
  338. package/src/helpers/optionUiTypes.ts +38 -0
  339. package/src/helpers/options.ts +96 -0
  340. package/src/helpers/templateItems.ts +81 -0
  341. package/src/helpers/templateText.ts +350 -0
  342. package/src/helpers/templates.ts +94 -41
  343. package/src/helpers/validations.ts +203 -51
  344. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  345. package/src/hooks/useFetch/useFetch.tsx +53 -7
  346. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  347. package/src/hooks/useTemplates.ts +41 -0
  348. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  349. package/src/index.tsx +14 -1
  350. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  351. package/src/stories/storyNetwork.ts +92 -0
  352. package/src/utils/fetch.ts +311 -21
  353. package/src/utils/lspClient.ts +68 -11
  354. package/src/utils/websocket.ts +20 -9
  355. package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts +0 -73
  356. package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts.map +0 -1
  357. package/dist/components/form/engine/__fixtures__/basicFormOptions.js +0 -317
  358. package/dist/components/form/engine/__fixtures__/basicFormOptions.js.map +0 -1
  359. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts +0 -41
  360. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts.map +0 -1
  361. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js +0 -45
  362. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js.map +0 -1
  363. package/dist/components/form/expressions/dpqlMockLsp.d.ts +0 -3
  364. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +0 -1
  365. package/dist/components/form/expressions/dpqlMockLsp.js +0 -91
  366. package/dist/components/form/expressions/dpqlMockLsp.js.map +0 -1
  367. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +0 -2
  368. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +0 -1
  369. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +0 -9
  370. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +0 -1
  371. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +0 -55
  372. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +0 -1
  373. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +0 -126
  374. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +0 -1
  375. package/dist/stories/Data/fileFieldArgSchema.json +0 -86
@@ -3,30 +3,42 @@
3
3
  // Owns the `IExpression` value and offers two views of the same AST —
4
4
  // Visual (the builder, Phase 3) and Text (DPQL, this phase). The DPQL text
5
5
  // editor bridges to the AST via the LSP `dpql/parse` (text → AST on edit)
6
- // and `dpql/serialize` (AST → text on entering Text mode); a live "Parsed"
7
- // line renders the current AST (`dpql/renderExpression`).
6
+ // and `dpql/serialize` (AST → text on entering Text mode); a debounced
7
+ // "Preview" box renders the current AST (`dpql/renderExpression`) when that
8
+ // rendering differs from the text the author typed.
8
9
  import {
9
10
  ReqoreButton,
10
11
  ReqoreControlGroup,
11
12
  ReqoreMessage,
13
+ ReqoreVerticalSpacer,
12
14
  } from '@qoretechnologies/reqore';
13
15
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
14
16
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
17
  import { DpqlEditor, IDpqlEditorRef } from '../../dpqlEditor';
18
+ import { dpqlDisplayedText } from '../../dpqlEditor/dpqlHelpers';
16
19
  import { ExpressionBuilder, IExpressionBuilderProps } from './builder';
20
+ import { DpqlRendering } from './DpqlRendering';
17
21
  import { IExpression, IExpressionSchema, IExpressionValue, TExpressionReorder } from './types';
18
22
  import { useExpressions } from './useExpressions';
19
23
  import { useRenderExpression } from './useRenderExpression';
24
+ import { isUntypedOptionType } from '../../../helpers/optionUiTypes';
20
25
 
21
26
  export type TExpressionMode = 'visual' | 'text';
22
27
 
23
28
  const PARSE_DEBOUNCE_MS = 300;
29
+ /**
30
+ * How long the Preview waits before catching up with the editor.
31
+ *
32
+ * It mirrors the AST, so without this it re-renders on every keystroke — a box
33
+ * appearing, changing and vanishing under the line being typed. Slightly
34
+ * SLOWER than the parse it follows, so the preview settles once after the text
35
+ * has stopped moving rather than twitching on the way there.
36
+ */
37
+ const PREVIEW_DEBOUNCE_MS = 400;
24
38
  /** Retry cadence for seeding the Text editor while its LSP session opens. */
25
39
  const SEED_RETRY_MS = 400;
26
40
  const SEED_MAX_TRIES = 20;
27
41
 
28
- const noop = (): void => undefined;
29
-
30
42
  export interface IExpressionFieldProps {
31
43
  /** The expression value: `{ is_expression:true, value:{ exp, args } }`. */
32
44
  value?: IExpression;
@@ -62,6 +74,12 @@ export interface IExpressionFieldProps {
62
74
  recordType?: string;
63
75
  /** Initial editor mode (default `visual`). */
64
76
  defaultMode?: TExpressionMode;
77
+ /**
78
+ * SEAM (reqraft): the host's per-`ui_type` editors, forwarded to the
79
+ * builder's operand fields. Without them an operand typed with one of the
80
+ * CONSUMER's ui_types renders "Unknown type!".
81
+ */
82
+ componentOverrides?: Record<string, React.FC<any>>;
65
83
  size?: string;
66
84
  /** Forwarded to the builder — operand reordering for varargs expressions. */
67
85
  reorder?: TExpressionReorder;
@@ -83,6 +101,7 @@ export const ExpressionField = memo(
83
101
  recordType,
84
102
  defaultMode = 'visual',
85
103
  size,
104
+ componentOverrides,
86
105
  reorder,
87
106
  }: IExpressionFieldProps) => {
88
107
  const { expressions } = useExpressions({
@@ -92,68 +111,218 @@ export const ExpressionField = memo(
92
111
  const { renderRich } = useRenderExpression();
93
112
 
94
113
  const [mode, setMode] = useState<TExpressionMode>(defaultMode);
95
- const [preview, setPreview] = useState<{ text: string; server: boolean }>({
96
- text: '',
97
- server: false,
98
- });
114
+ /* The server's rendering of the current AST; empty until it has one. There is
115
+ no client-side stand-in — see `useRenderExpression`. */
116
+ const [preview, setPreview] = useState('');
99
117
 
100
118
  // Text mode state. `text` is the DPQL string the editor shows; the AST
101
119
  // (`value`) stays the source of truth, kept in sync via parse-on-edit.
102
120
  const [text, setText] = useState('');
121
+ /* The text the current AST was parsed FROM.
122
+ *
123
+ * The preview renders the AST, and the AST only moves on a SUCCESSFUL
124
+ * parse — so while an edit is mid-flight or simply invalid, the AST still
125
+ * describes the last thing that parsed. Showing it then states something
126
+ * the editor does not contain: deleting the `2` from `1 + 2` left
127
+ * "Unexpected end of input" sitting directly above a Preview confidently
128
+ * reading `1 + 2`. */
129
+ const [astText, setAstText] = useState('');
130
+ /**
131
+ * The server's answer to "can this expression's result satisfy the type
132
+ * this field declares?". `null` when nothing has been asked, or when the
133
+ * field declares no type worth asking about.
134
+ */
135
+ const [typeCheck, setTypeCheck] = useState<{
136
+ compatible: boolean;
137
+ mayFail: boolean;
138
+ inferred?: string;
139
+ target?: string;
140
+ fix?: string;
141
+ } | null>(null);
103
142
  const dpqlRef = useRef<IDpqlEditorRef>(null);
104
143
  const parseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
105
144
 
106
145
  const ast = useMemo<IExpressionValue | undefined>(() => value?.value, [value]);
107
146
 
147
+ /**
148
+ * The type the expression's result has to satisfy, sent with every parse.
149
+ *
150
+ * `auto` and `any` are left out deliberately: they accept anything, so the
151
+ * server would answer "compatible" for everything and the round trip would
152
+ * buy nothing. A field offering a CHOICE of return types has no single
153
+ * answer either, so it asks nothing rather than asking about the wrong one.
154
+ */
155
+ const targetType = useMemo<string | undefined>(() => {
156
+ const declared = Array.isArray(returnType) ? undefined : (returnType ?? type);
157
+ return declared && !isUntypedOptionType(declared) ? declared : undefined;
158
+ }, [returnType, type]);
159
+
160
+ /**
161
+ * Read the type analysis off a parse result, or clear it.
162
+ *
163
+ * "Fits" is read from the whole answer, not `type_compatible` alone. A
164
+ * conversion that exists and cannot fail — a number used as text — is a
165
+ * fit as far as the author is concerned, and reporting it is exactly the
166
+ * noise the message below is written to avoid.
167
+ */
168
+ const readTypeCheck = useCallback(
169
+ (result?: { type_compatible?: boolean; auto_coercible?: boolean;
170
+ coercion_may_fail?: boolean; inferred_type?: string; target_type?: string;
171
+ suggested_fix?: { text: string } }): void => {
172
+ if (!result || result.type_compatible === undefined) {
173
+ setTypeCheck(null);
174
+ return;
175
+ }
176
+ const convertsSafely = !!result.auto_coercible && !result.coercion_may_fail;
177
+ setTypeCheck({
178
+ compatible: !!result.type_compatible || convertsSafely,
179
+ mayFail: !!result.coercion_may_fail,
180
+ inferred: result.inferred_type,
181
+ target: result.target_type,
182
+ fix: result.suggested_fix?.text,
183
+ });
184
+ },
185
+ []
186
+ );
187
+
108
188
  // Keep a readable rendering of the current expression in sync — the
109
- // single live mirror of the AST (server `dpql/renderExpression` when
110
- // reachable, the client-side approximation otherwise).
189
+ // single live mirror of the AST (the server's `dpql/renderExpression`).
190
+ //
191
+ // Debounced: it follows the AST, which follows the text, so rendering it
192
+ // eagerly put a box under the cursor that appeared, changed and vanished
193
+ // while the author was still typing.
111
194
  useEffect(() => {
112
195
  let live = true;
113
- renderRich(ast ?? {}, expressions).then((r) => {
114
- if (live) setPreview({ text: r.text, server: r.server });
115
- });
196
+ const timer = setTimeout(() => {
197
+ renderRich(ast ?? {}).then((r) => {
198
+ if (live) setPreview(r?.text ?? '');
199
+ });
200
+ }, PREVIEW_DEBOUNCE_MS);
116
201
  return () => {
117
202
  live = false;
203
+ clearTimeout(timer);
118
204
  };
119
- }, [renderRich, ast, expressions]);
205
+ }, [renderRich, ast]);
120
206
 
121
207
  // Tracks whether the user has typed since entering Text mode, so a
122
208
  // slow seed response can't clobber their input.
123
209
  const userTypedRef = useRef(false);
124
- const astRef = useRef(ast);
210
+
211
+ /* A cleared value must clear the editor with it.
212
+ *
213
+ * "Clear value" empties the option (`handleValueChange(name, undefined)`),
214
+ * but the DPQL text is local state seeded once on entering Text mode, and
215
+ * the seeding effect returns early for an empty AST — it exists to fill the
216
+ * editor, not to empty it. So the value went and the text stayed: the row
217
+ * reported "This field is required" while still showing `1 + 2`, which
218
+ * reads as the clear having silently failed.
219
+ *
220
+ * Only an AST that HAD content and lost it clears the box. A field that
221
+ * simply starts empty is left alone, so this cannot wipe what an author is
222
+ * part-way through typing before their first parse lands.
223
+ */
224
+ const hadExpression = useRef(false);
125
225
  useEffect(() => {
126
- astRef.current = ast;
226
+ /* GONE, not merely incomplete.
227
+ *
228
+ * The first cut asked whether the AST still had an `exp`, and that is a
229
+ * state the value passes THROUGH while the author types: a half-written
230
+ * expression parses to something without one. So typing `1 + 2` cleared
231
+ * the editor mid-keystroke and the text vanished under the cursor.
232
+ *
233
+ * A clear removes the value itself, so that is what to watch: `value`
234
+ * empty, not an AST that is between shapes. Typing never produces that —
235
+ * `handleDpqlChange` only emits on a SUCCESSFUL parse, so an incomplete
236
+ * expression leaves the previous value in place. */
237
+ if (ast) {
238
+ hadExpression.current = true;
239
+ return;
240
+ }
241
+ if (hadExpression.current) {
242
+ hadExpression.current = false;
243
+ userTypedRef.current = false;
244
+ setText('');
245
+ }
127
246
  }, [ast]);
128
247
 
129
248
  // Text mode: parse the DPQL into the AST (debounced).
249
+ /* The session may not be attached when the debounce fires — the editor
250
+ mounts on the render that flips the mode, and its LSP session comes up
251
+ after that. `dpqlRef.current?.parse?.()` is optional all the way down, so
252
+ an early call resolves `undefined`, `readTypeCheck` CLEARS the analysis,
253
+ and nothing asks again: the author types once into a fresh Text field and
254
+ is told nothing about the type until they happen to type another
255
+ character.
256
+
257
+ The seeding effect below already retries for this exact reason
258
+ ("`serialize` resolves '' until the editor's LSP session is ready"); the
259
+ parse path needed the same care and did not have it. Same budget as
260
+ seeding, so a session that never arrives gives up rather than spinning.
261
+
262
+ Found by CI, not locally: the session is up before the debounce on a
263
+ fast machine, so the story asserting the type message passed here every
264
+ time and failed on the runner. */
130
265
  const handleDpqlChange = useCallback(
131
266
  (next: string) => {
132
267
  userTypedRef.current = true;
133
268
  setText(next);
134
269
  if (parseTimer.current) clearTimeout(parseTimer.current);
135
- parseTimer.current = setTimeout(async () => {
136
- const result = await dpqlRef.current?.parse?.(next);
270
+ let tries = 0;
271
+ const runParse = async (): Promise<void> => {
272
+ const parse = dpqlRef.current?.parse;
273
+ if (!parse) {
274
+ if (++tries < SEED_MAX_TRIES) {
275
+ parseTimer.current = setTimeout(runParse, SEED_RETRY_MS);
276
+ }
277
+ return;
278
+ }
279
+ const result = await parse(next, targetType);
280
+ readTypeCheck(result);
137
281
  if (result?.success && result.expression) {
282
+ setAstText(next);
138
283
  // `dpql/parse` returns the field-ready `{ is_expression, value }`.
139
284
  onChange(result.expression as IExpression);
140
285
  }
141
- }, PARSE_DEBOUNCE_MS);
286
+ };
287
+ parseTimer.current = setTimeout(runParse, PARSE_DEBOUNCE_MS);
142
288
  },
143
- [onChange]
289
+ [onChange, targetType, readTypeCheck]
144
290
  );
145
291
 
146
- // Seed the Text editor from the AST whenever Text mode becomes active
147
- // (including a `defaultMode='text'` mount). This must run as an
148
- // effect: the editor only mounts on the same render that flips the
149
- // mode (so `dpqlRef` is null inside the click handler), and
150
- // `serialize` resolves '' until the editor's LSP session is ready —
151
- // hence the brief retry loop.
292
+ /** Whether this visit to Text mode has already put the AST in the editor. */
293
+ const seededRef = useRef(false);
294
+
295
+ /* Seed the Text editor from the AST whenever Text mode becomes active
296
+ * (including a `defaultMode='text'` mount). This must run as an effect: the
297
+ * editor only mounts on the same render that flips the mode (so `dpqlRef`
298
+ * is null inside the click handler), and `serialize` resolves '' until the
299
+ * editor's LSP session is ready — hence the brief retry loop.
300
+ *
301
+ * It waits for an AST, because the AST can arrive AFTER the mode does. A
302
+ * host flips this shell into Text mode on the very render that accepts an
303
+ * expression, and the value it passes on that render is still the TEXT the
304
+ * author typed — the parsed AST only comes back through the form one render
305
+ * later. Keyed on the mode alone, the effect had already had its only turn
306
+ * by then: it found nothing to serialize, returned, and nothing asked
307
+ * again. The author landed in an empty editor with a Preview of their own
308
+ * expression sitting beside it, which reads as the text having been thrown
309
+ * away.
310
+ *
311
+ * Seeding happens at most ONCE per visit, and that is what makes watching
312
+ * the AST safe: `userTypedRef` guards what is being typed, but the AST
313
+ * MOVES as the author types — every successful parse replaces it — so an
314
+ * effect that re-seeded on each new AST would fight them for the editor. */
152
315
  useEffect(() => {
153
- if (mode !== 'text') return undefined;
154
- const seedAst = astRef.current;
155
- if (!seedAst?.exp) return undefined;
156
- userTypedRef.current = false;
316
+ if (mode !== 'text') {
317
+ // Leaving Text asks the question again on the next visit; the typing
318
+ // flag goes with it, or a stale one would suppress that visit's seed.
319
+ seededRef.current = false;
320
+ userTypedRef.current = false;
321
+ return undefined;
322
+ }
323
+ if (seededRef.current || userTypedRef.current) return undefined;
324
+ if (!ast?.exp) return undefined;
325
+ const seedAst = ast;
157
326
  let cancelled = false;
158
327
  let timer: ReturnType<typeof setTimeout> | null = null;
159
328
  let tries = 0;
@@ -161,7 +330,9 @@ export const ExpressionField = memo(
161
330
  const t = await dpqlRef.current?.serialize?.(seedAst);
162
331
  if (cancelled || userTypedRef.current) return;
163
332
  if (t) {
333
+ seededRef.current = true;
164
334
  setText(t);
335
+ setAstText(t);
165
336
  } else if (++tries < SEED_MAX_TRIES) {
166
337
  timer = setTimeout(seed, SEED_RETRY_MS);
167
338
  }
@@ -171,7 +342,7 @@ export const ExpressionField = memo(
171
342
  cancelled = true;
172
343
  if (timer) clearTimeout(timer);
173
344
  };
174
- }, [mode]);
345
+ }, [mode, ast]);
175
346
 
176
347
  // Switch to Text: the seeding effect above serializes the AST once
177
348
  // the editor's session is up.
@@ -183,13 +354,14 @@ export const ExpressionField = memo(
183
354
  const enterVisualMode = useCallback(async () => {
184
355
  if (parseTimer.current) clearTimeout(parseTimer.current);
185
356
  if (mode === 'text' && text) {
186
- const result = await dpqlRef.current?.parse?.(text);
357
+ const result = await dpqlRef.current?.parse?.(text, targetType);
358
+ readTypeCheck(result);
187
359
  if (result?.success && result.expression) {
188
360
  onChange(result.expression as IExpression);
189
361
  }
190
362
  }
191
363
  setMode('visual');
192
- }, [mode, text, onChange]);
364
+ }, [mode, text, onChange, targetType, readTypeCheck]);
193
365
 
194
366
  useEffect(
195
367
  () => () => {
@@ -198,8 +370,23 @@ export const ExpressionField = memo(
198
370
  []
199
371
  );
200
372
 
373
+ /* What the Preview has to add: `none` until the server's rendering of the
374
+ text now showing exists, `repeats` when it reads exactly as that text does
375
+ (so it is not drawn — see below), `shown` otherwise. Exposed on the field,
376
+ because a Preview correctly left out leaves nothing else to observe. */
377
+ const previewState =
378
+ !preview || astText.trim() !== text.trim() ? 'none'
379
+ : dpqlDisplayedText(preview).trim() === dpqlDisplayedText(text).trim() ? 'repeats'
380
+ : 'shown';
381
+
201
382
  return (
202
- <ReqoreControlGroup vertical fluid gapSize='small' className='expression-field'>
383
+ <ReqoreControlGroup
384
+ vertical
385
+ fluid
386
+ gapSize='small'
387
+ className='expression-field'
388
+ data-preview={mode === 'text' ? previewState : undefined}
389
+ >
203
390
  <ReqoreControlGroup gapSize='small' fluid>
204
391
  <ReqoreButton
205
392
  icon='NodeTree'
@@ -232,27 +419,53 @@ export const ExpressionField = memo(
232
419
  readOnly={readOnly}
233
420
  height='48px'
234
421
  />
235
- {/* An empty query has nothing to parse — the box appears once a
236
- parse result exists rather than sitting there holding a
237
- placeholder token. */}
238
- {preview.text ? (
239
- <ReqoreMessage intent='info' size='small' title='Parsed' flat opaque={false}>
240
- {preview.server ? (
241
- // Server rendering shown through a read-only DpqlEditor:
242
- // template-ref chips + LSP token colours; diagnostics off
243
- // (the rendering is readable text, not parseable DPQL).
244
- <div data-testid='expression-preview' style={{ width: '100%' }}>
245
- <DpqlEditor
246
- value={preview.text}
247
- onChange={noop}
248
- readOnly
249
- showDiagnostics={false}
250
- enableHover={false}
251
- />
252
- </div>
253
- ) : (
254
- <code data-testid='expression-preview'>{preview.text}</code>
255
- )}
422
+ {/* What the server said about the result's type, when the field
423
+ declares one to check against.
424
+
425
+ Nothing is shown when the expression already fits, and nothing
426
+ is shown when the conversion is CERTAIN either — a number used
427
+ as text always works, and a warning about something that cannot
428
+ fail is what teaches people to ignore the ones that can. So this
429
+ appears for exactly two cases: the conversion is impossible
430
+ (danger), or it is attempted and may fail on the day (warning,
431
+ and the run checks the value itself). */}
432
+ {typeCheck && !typeCheck.compatible && (
433
+ <ReqoreMessage
434
+ intent={typeCheck.mayFail ? 'warning' : 'danger'}
435
+ size='small'
436
+ flat
437
+ opaque={false}
438
+ title={typeCheck.mayFail ? 'This may not fit' : 'This does not fit'}
439
+ >
440
+ {`The expression returns ${typeCheck.inferred}, and this field holds ${typeCheck.target}.`}
441
+ {typeCheck.mayFail ?
442
+ ' That conversion is attempted rather than guaranteed, so the value itself is checked when it runs.'
443
+ : ''}
444
+ {typeCheck.fix ? (
445
+ <>
446
+ <ReqoreVerticalSpacer height={6} />
447
+ <DpqlRendering text={typeCheck.fix} data-testid='expression-type-fix' />
448
+ </>
449
+ ) : null}
450
+ </ReqoreMessage>
451
+ )}
452
+
453
+ {/* Shown only when it ADDS something.
454
+
455
+ It is a rendering of the AST, so for most text it repeats the
456
+ line directly above it — `1 + 2` previewing as `1 + 2` is a
457
+ second copy that flickers as you type. It earns the space when
458
+ the rendering DIFFERS: a normalised form, or wording the DPQL
459
+ does not have (`contains "es" (ignore case)`). Both are compared
460
+ as they are DRAWN — a reference is a chip in either, so the quotes
461
+ DPQL writes around one do not count — and ignoring surrounding
462
+ whitespace, which the renderer does not preserve.
463
+
464
+ An empty query has nothing to render either, so the box appears
465
+ once there is a result rather than holding a placeholder. */}
466
+ {previewState === 'shown' ? (
467
+ <ReqoreMessage intent='info' size='small' title='Preview' flat opaque={false}>
468
+ <DpqlRendering text={preview} data-testid='expression-preview' />
256
469
  </ReqoreMessage>
257
470
  ) : null}
258
471
  </>
@@ -268,6 +481,7 @@ export const ExpressionField = memo(
268
481
  readOnly={readOnly}
269
482
  localTemplates={localTemplates ?? { items: [] }}
270
483
  serverHandled={serverHandled}
484
+ componentOverrides={componentOverrides}
271
485
  extraActions={extraActions}
272
486
  size={size}
273
487
  reorder={reorder}
@@ -0,0 +1,213 @@
1
+ // Copyright 2026 Qore Technologies, s.r.o.
2
+ /**
3
+ * Is this expression argument SUPPLIED, or is it still missing?
4
+ *
5
+ * DPQL has a null literal: `null` parses to `{exp: "value", args: [null]}` and
6
+ * round-trips, and an author writing it in the text editor means *"this is the
7
+ * value — nothing"*. It is the only way to assert that a call returned no
8
+ * value, which is how it was reported.
9
+ *
10
+ * The rule, stated once here because three different places had drawn it
11
+ * differently: **an argument that is explicitly null is supplied; one that is
12
+ * absent or `undefined` is still missing.**
13
+ *
14
+ * The builder used to ask `size(arg) === 0`, and lodash answers `0` for `null`
15
+ * as readily as for `{}`. So an explicit null was read as a missing argument
16
+ * and reset from the catalogue's declaration — which is how a saved draft ended
17
+ * up holding `{"type": "any", "required": true}`, an argument DEFINITION where
18
+ * its value should be, with the author's `null` gone and `required` (a schema
19
+ * key, never a value key) left behind to give it away.
20
+ */
21
+
22
+ /**
23
+ * True when `arg` carries no value yet — `undefined`, the `{}` the builder
24
+ * writes when a type change resets an argument, or any envelope with no
25
+ * `value` in it.
26
+ *
27
+ * `null` is NOT missing: it is the null literal.
28
+ *
29
+ * The rule used to be "an object with no keys at all", which let an envelope
30
+ * holding nothing but SCHEMA keys pass as a supplied value. That is how a saved
31
+ * draft kept `{type: 'any', required: true}` — an argument DEFINITION where the
32
+ * author's value belonged — and why re-opening the expression never repaired
33
+ * it: the builder looked at those two keys, concluded the argument was already
34
+ * supplied, and left the definition in place as data. `required` is a schema
35
+ * key; nothing ever reads it back off a stored value.
36
+ *
37
+ * Asking for the `value` instead makes the question the one that was always
38
+ * meant: has the author put something here?
39
+ *
40
+ * @param arg one entry of an expression's `args`
41
+ */
42
+ export const isExpressionArgumentMissing = (arg: unknown): boolean => {
43
+ if (arg === null) {
44
+ return false;
45
+ }
46
+
47
+ if (arg === undefined) {
48
+ return true;
49
+ }
50
+
51
+ if (typeof arg !== 'object') {
52
+ // a raw scalar argument — `0`, `false` and `''` are values too
53
+ return false;
54
+ }
55
+
56
+ return (arg as { value?: unknown }).value === undefined;
57
+ };
58
+
59
+ /**
60
+ * The shape the builder stores an argument in: `{type, value}`.
61
+ *
62
+ * A parse result carries its arguments RAW — `args: [null]`, `args: [3]` —
63
+ * because that is what the grammar produced. The builder's editors read
64
+ * `arg.value`, so a raw argument has to be widened before it is stored, and
65
+ * widening `null` is the whole point: dropped instead, the author's null
66
+ * becomes an argument with no `value` key at all, which every later reader
67
+ * treats as an empty field.
68
+ *
69
+ * An argument that is already an envelope is returned untouched.
70
+ *
71
+ * @param arg one entry of an expression's `args`
72
+ * @param uiType the type the catalogue declares for that position
73
+ */
74
+ export const asExpressionArgumentEnvelope = (arg: unknown, uiType?: string): unknown => {
75
+ if (arg !== null && (arg === undefined || typeof arg === 'object')) {
76
+ return arg;
77
+ }
78
+
79
+ return { value: arg, ...(uiType ? { type: uiType } : {}) };
80
+ };
81
+
82
+ /**
83
+ * Does this value still look like an expression AST?
84
+ *
85
+ * `{exp, args}` is the shape the builder and `dpql/parse` both produce. The
86
+ * check is structural on purpose: the flag that marks an option as holding an
87
+ * expression describes its VALUE, so the value is what should decide whether
88
+ * the flag survives a change that said nothing about it.
89
+ */
90
+ export const looksLikeExpressionAst = (value: unknown): boolean =>
91
+ !!value &&
92
+ typeof value === 'object' &&
93
+ !Array.isArray(value) &&
94
+ typeof (value as { exp?: unknown }).exp === 'string';
95
+
96
+ /**
97
+ * Whether an option should still be marked as holding an expression.
98
+ *
99
+ * The form engine used to delete the flag on ANY change that did not re-assert
100
+ * it, and most changes do not: an editor re-mounting, a revert, a validation
101
+ * pass. The value stayed the AST while the flag went, so the option degraded
102
+ * into a plain hash — which is how an assertion's Expected Value came back
103
+ * from a saved draft as a two-field object showing `exp` and `args`, and why
104
+ * `encodeTestValue` then stored it with `source: "literal"`.
105
+ *
106
+ * @param isFunction what the caller said: `true` sets the flag, `false` clears
107
+ * it deliberately (choosing a concrete type does this), `undefined` is no
108
+ * opinion at all
109
+ * @param value the option's new value
110
+ * @param wasExpression whether the option was marked before this change
111
+ */
112
+ export const shouldMarkAsExpression = (
113
+ isFunction: boolean | undefined,
114
+ value: unknown,
115
+ wasExpression: boolean | undefined
116
+ ): boolean => {
117
+ if (isFunction === true) {
118
+ return true;
119
+ }
120
+
121
+ if (isFunction === false) {
122
+ return false;
123
+ }
124
+
125
+ // No opinion: the flag belongs to the value, so it survives exactly as long
126
+ // as the value is still an expression.
127
+ return !!wasExpression && looksLikeExpressionAst(value);
128
+ };
129
+
130
+ /**
131
+ * Fill in an expression's missing arguments, leaving the supplied ones alone.
132
+ *
133
+ * Extracted from the builder so the decision it makes can be tested directly:
134
+ * a component test of the same behaviour passed just as happily with the defect
135
+ * in place, because the builder normalises on an interaction the harness never
136
+ * performed. A test that cannot fail is worse than none.
137
+ *
138
+ * @param expressions the served expression catalogue
139
+ * @param expression the chosen expression's `name`
140
+ * @param args the arguments so far — raw from a parse, or envelopes from the
141
+ * builder's own editors
142
+ */
143
+ /**
144
+ * An argument slot with nothing in it yet: the catalogue's default when it has
145
+ * one, otherwise an envelope typed for the editors that need a type to mount.
146
+ * `false`, `0` and `''` are defaults too — a truthiness test dropped them.
147
+ */
148
+ const emptyArgumentSlot = (arg: any): any =>
149
+ arg?.default_value != null ?
150
+ { value: arg.default_value, type: arg.ui_type }
151
+ : {
152
+ type:
153
+ arg?.ui_type === 'richtext' || arg?.ui_type === 'number' || arg?.ui_type === 'bool' ?
154
+ arg.ui_type
155
+ : undefined,
156
+ };
157
+
158
+ export const addMissingExpressionArgs = (
159
+ expressions: any[] | undefined,
160
+ expression: string,
161
+ args: any[] = []
162
+ ): any[] => {
163
+ const newArgs = [...args];
164
+ const selected = expressions?.find((exp) => exp?.name === expression);
165
+
166
+ if (!selected) {
167
+ return newArgs;
168
+ }
169
+
170
+ if (selected.varargs) {
171
+ if (selected.subtype === 2) {
172
+ newArgs.push({ is_expression: true, value: { args: [] } });
173
+ return newArgs;
174
+ }
175
+
176
+ /* Every operand follows the one schema argument. Slots are added up to
177
+ `min_args`, and EVERY missing one is filled, not only the new ones: the
178
+ builder leaves an empty slot behind for each argument the previous value
179
+ had no operand for (a fresh builder has none), and an operand editor or
180
+ the validator reading such a slot for its `type` crashed the builder. A
181
+ raw operand from a parse is a value, so it is left alone. */
182
+ const [schema] = selected.args ?? [];
183
+ newArgs.push(...Array.from({ length: (selected.min_args ?? 1) - 1 }, () => undefined));
184
+ newArgs.forEach((slot, index) => {
185
+ if (isExpressionArgumentMissing(slot)) {
186
+ newArgs[index] = emptyArgumentSlot(schema);
187
+ }
188
+ });
189
+
190
+ return newArgs;
191
+ }
192
+
193
+ selected.args.forEach((arg: any, index: number) => {
194
+ /* A RAW argument, as `dpql/parse` produces it — `args: [null]`, `args: [3]`
195
+ — is widened into the `{type, value}` envelope the editors read. Widening
196
+ the null is the point: dropped instead, the author's `null` became an
197
+ argument with no `value` key, and the branch below then filled the gap
198
+ from the catalogue's DECLARATION, which is how a saved draft came back
199
+ holding `{type: "any", required: true}` where its value belonged. */
200
+ newArgs[index] = asExpressionArgumentEnvelope(newArgs[index], arg.ui_type);
201
+
202
+ /* "Missing" already means "carries no value", so an argument waiting for
203
+ its catalogue default is simply a missing one — it needs no separate
204
+ test. */
205
+ if (!isExpressionArgumentMissing(newArgs[index])) {
206
+ return;
207
+ }
208
+
209
+ newArgs[index] = emptyArgumentSlot(arg);
210
+ });
211
+
212
+ return newArgs;
213
+ };