@qoretechnologies/reqraft 0.10.55-pr.124.g77dd26c → 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 -211
  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
@@ -29,7 +29,7 @@ import {
29
29
  } from '@qoretechnologies/reqore/dist/components/Textarea';
30
30
  import { IQorusFormFieldSchemaBase, TQorusType } from '@qoretechnologies/ts-toolkit';
31
31
  import { size } from 'lodash';
32
- import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
32
+ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
33
33
  import { useUpdateEffect } from 'react-use';
34
34
  import {
35
35
  filterTemplatesByType as templatesFilterFunc,
@@ -40,7 +40,14 @@ import {
40
40
  describeTemplateReference,
41
41
  isValueTemplate,
42
42
  } from '../../../../helpers/templates';
43
+ import { classifyTypedText, mightBeDpqlExpression } from '../../../../helpers/dpqlDetection';
44
+ import {
45
+ renderTemplateItemDescriptions,
46
+ templateItemsToShow,
47
+ } from '../../../../helpers/templateItems';
48
+ import { useMarkdownRenderer } from '../../../Description/markdownRendererContext';
43
49
  import { getTypeFromValue } from '../../../../helpers/validations';
50
+ import { useDpqlProbe } from '../../../dpqlEditor/useDpqlProbe';
44
51
  import { useQorusTypes } from '../../../../hooks/useQorusTypes';
45
52
  import { useWhyDidYouUpdate } from '../../../../hooks/useWhyDidYouUpdate';
46
53
  import { ExpressionBuilder, IExpressionBuilderProps } from '../../expressions/builder';
@@ -57,11 +64,28 @@ import LongStringFormField from '../long-string/LongString';
57
64
  import NumberFormField from '../number/Number';
58
65
  import { ReadOnlyTemplateTag } from './ReadOnlyTemplateTag';
59
66
  import { RichTextFormField } from '../rich-text/RichText';
67
+ import { richtextToString } from '../../../../helpers/common';
68
+ import { isSingleLineStringType } from '../../../../helpers/singleLineString';
69
+ import { isUntypedOptionType } from '../../../../helpers/optionUiTypes';
70
+ import {
71
+ IRowMenuRegistration,
72
+ RowMenuContext,
73
+ useRowMenu,
74
+ useRowMenuPublisher,
75
+ } from '../../engine/rowMenuContext';
60
76
 
61
77
  // Re-export template utilities for consumers
62
78
  export { getTemplateKey, getTemplateValue, isValueTemplate };
63
79
  export type { IQorusFormType as IQorusType };
64
80
 
81
+ /**
82
+ * How long the author must pause before typed text is sent to the DPQL
83
+ * probe. Long enough that a probe is not sent for every prefix of what is
84
+ * being typed, short enough that the offer appears while they are still
85
+ * looking at the field.
86
+ */
87
+ const DPQL_DETECT_DEBOUNCE_MS = 400;
88
+
65
89
  export const TemplatesListProps: IReqoreDropdownProps = {
66
90
  useTargetWidth: true,
67
91
  handler: 'focus',
@@ -166,6 +190,14 @@ export interface ITemplateFieldProps extends Partial<
166
190
  * out of `rest` and never spread onto a leaf input.
167
191
  */
168
192
  extraActions?: IExpressionBuilderProps['extraActions'];
193
+ /**
194
+ * SEAM (reqraft): the host's per-`ui_type` editors, forwarded to the field
195
+ * component and to the expression editor's operand rows. Declared here — not
196
+ * left to the index signature — for the same reason as `extraActions` above:
197
+ * so it is destructured out of `rest` and never spread onto a leaf input or a
198
+ * Reqore layout component, both of which pass unknown props to the DOM.
199
+ */
200
+ componentOverrides?: Record<string, React.FC<any>>;
169
201
  [key: string]: any;
170
202
  default_value?: unknown;
171
203
  }
@@ -300,16 +332,19 @@ const MenuActionsSection = memo(
300
332
  actions,
301
333
  closePopover,
302
334
  size,
335
+ startExpanded,
303
336
  ...rest
304
337
  }: {
305
338
  actions: ITemplateMenuActions;
306
339
  closePopover?: () => void;
307
340
  size?: IReqoreButtonProps['size'];
341
+ /** @see `loneSectionStartsExpanded` — the menu's only group opens itself. */
342
+ startExpanded?: boolean;
308
343
  }) => (
309
344
  <ReqoreMenuSection
310
345
  label={actions.label}
311
346
  icon={actions.icon}
312
- isCollapsed
347
+ isCollapsed={!startExpanded}
313
348
  transparent
314
349
  className='template-menu-actions'
315
350
  size={size}
@@ -326,20 +361,28 @@ export const CustomMenuItems = memo(
326
361
  closePopover,
327
362
  setIsTemplate,
328
363
  setTemplateValue,
364
+ startExpanded,
329
365
  ...rest
330
366
  }: {
331
367
  items: TCustomTemplateItems | undefined;
332
368
  closePopover?: () => void;
333
369
  setIsTemplate: React.Dispatch<React.SetStateAction<boolean>>;
334
370
  setTemplateValue: React.Dispatch<React.SetStateAction<string | null>>;
371
+ /** @see `loneSectionStartsExpanded` — the menu's only group opens itself. */
372
+ startExpanded?: boolean;
335
373
  }) => {
336
374
  return (
337
- <ReqoreMenuSection label='Set Custom Value' isCollapsed transparent icon='Text' {...rest}>
375
+ <ReqoreMenuSection
376
+ label='Set Custom Value'
377
+ isCollapsed={!startExpanded}
378
+ transparent
379
+ icon='Text'
380
+ {...rest}
381
+ >
338
382
  {items.map((menuItem, index) =>
339
- 'isDivider' in menuItem ? (
383
+ 'isDivider' in menuItem ?
340
384
  <ReqoreMenuDivider key={index} {...menuItem} />
341
- ) : (
342
- <ReqoreButton
385
+ : <ReqoreButton
343
386
  {...(rest as any)}
344
387
  {...menuItem}
345
388
  key={index}
@@ -351,7 +394,6 @@ export const CustomMenuItems = memo(
351
394
  });
352
395
  }}
353
396
  />
354
- )
355
397
  )}
356
398
  </ReqoreMenuSection>
357
399
  );
@@ -370,6 +412,15 @@ export const TemplateDropdownSelector = memo(
370
412
  size,
371
413
  ...rest
372
414
  }: ITemplateDropdownSelectorProps) => {
415
+ /* The LAST hop before Reqore, which is the only place a drawn description
416
+ may be made: `filteredTemplates` upstream is a `JSON.stringify` memo key,
417
+ and an element in it throws on its own circular owner. */
418
+ const renderMarkdown = useMarkdownRenderer();
419
+ const shownItems = useMemo(
420
+ () => renderTemplateItemDescriptions(templateItemsToShow(items), renderMarkdown),
421
+ [items, renderMarkdown]
422
+ );
423
+
373
424
  // One resolver for every surface that names a reference — the picker chip
374
425
  // here, the read-only tag, and the compact row's expression summary. The
375
426
  // row and this editor must agree: a reference that reads as its path when
@@ -403,7 +454,7 @@ export const TemplateDropdownSelector = memo(
403
454
  minimal
404
455
  compact
405
456
  onItemSelect={onItemSelect}
406
- items={items}
457
+ items={shownItems}
407
458
  label={label}
408
459
  leftIconProps={leftIconProps}
409
460
  caretPosition='right'
@@ -411,7 +462,7 @@ export const TemplateDropdownSelector = memo(
411
462
  size={size}
412
463
  {...TemplatesListProps}
413
464
  />
414
- {allowCustomValues || value ? (
465
+ {allowCustomValues || value ?
415
466
  <ReqoreButton
416
467
  customTheme={TemplatesListProps.listCustomTheme}
417
468
  fixed
@@ -423,15 +474,48 @@ export const TemplateDropdownSelector = memo(
423
474
  size={size}
424
475
  onClick={onRemoveClick}
425
476
  />
426
- ) : null}
477
+ : null}
427
478
  </ReqoreControlGroup>
428
479
  </ReqoreControlGroup>
429
480
  );
430
481
  }
431
482
  );
432
483
 
433
- export const TemplateField = memo(
484
+ /**
485
+ * `ui_type`s whose OWN editor renders templates as chips inline.
486
+ *
487
+ * For these the template SELECTOR must not take over: it replaces a control the
488
+ * author can type in with one they can only pick from. A Qorus test assertion's
489
+ * `Value` is the case this was written for — the IDE's own `auto.tsx` already
490
+ * renders it as template rich text (type freely, references become chips), and
491
+ * the same field reached through this form engine offered a dropdown and no way
492
+ * to type at all.
493
+ *
494
+ * `richtext` is reqraft's own; a consumer adds its types through the
495
+ * `templateAwareUiTypes` prop, exactly as it adds `rendererOnlyUiTypes`.
496
+ */
497
+ export const BuiltInTemplateAwareUiTypes = ['richtext'];
498
+
499
+ export const isTemplateAwareUiType = (uiType?: string, extra?: string[]): boolean =>
500
+ !!uiType && [...BuiltInTemplateAwareUiTypes, ...(extra ?? [])].includes(uiType);
501
+
502
+ /**
503
+ * One entry of the "set a value of this type" menu an untyped field offers.
504
+ *
505
+ * `onClick` is typed by what the caller actually invokes it with — the chosen
506
+ * value and a reset callback — rather than as `Function`, which accepts a class
507
+ * declaration as readily as a handler and gives no help at the call site.
508
+ */
509
+ interface IUntypedFieldMenuItem {
510
+ label?: unknown;
511
+ description?: unknown;
512
+ isDivider?: boolean;
513
+ onClick?: (value: unknown, reset: () => void) => void;
514
+ }
515
+
516
+ const TemplateFieldImpl = memo(
434
517
  ({
518
+ rowMenu,
435
519
  value,
436
520
  name,
437
521
  onChange,
@@ -442,6 +526,7 @@ export const TemplateField = memo(
442
526
  allowFunctions,
443
527
  allowTextExpressions,
444
528
  extraActions,
529
+ componentOverrides,
445
530
  allowCustomValues = true,
446
531
  filterTemplatesByType = true,
447
532
  filterTemplatesFunc,
@@ -458,7 +543,7 @@ export const TemplateField = memo(
458
543
  reorder,
459
544
  label,
460
545
  ...rest
461
- }: ITemplateFieldProps) => {
546
+ }: ITemplateFieldProps & { rowMenu?: IRowMenuRegistration }) => {
462
547
  const qorusTypes = useQorusTypes();
463
548
  const functions = useExpressions({
464
549
  allow: !!allowFunctions,
@@ -467,9 +552,22 @@ export const TemplateField = memo(
467
552
  });
468
553
  const type = rest.ui_type || rest.type || rest.defaultType;
469
554
 
555
+ /* The type an EXPRESSION must return, and is stored with, is a DATA type.
556
+ `type` above deliberately prefers `ui_type`, because that is what decides
557
+ which editor to render — but a ui_type says how a value is EDITED, not
558
+ what it is. The IDE's reference fields carry `ui_type: 'test-reference'`
559
+ (a rich-text editor that turns `$.` paths into chips), and feeding that
560
+ to the expression builder asked it for an expression RETURNING
561
+ `test-reference`: no expression returns one, so every choice was refused
562
+ with *"the expected return type is test-reference"*, and the ui_type was
563
+ then stored on the saved expression as its declared type.
564
+
565
+ The declared data type is the constraint; the ui_type is only a fallback
566
+ for a field that declares no type at all. */
567
+ const expressionDataType = rest.type || rest.defaultType || rest.ui_type;
568
+
470
569
  const filteredTemplates = useMemo<IReqoreFormTemplates>(():
471
- | IReqoreFormTemplates
472
- | undefined => {
570
+ IReqoreFormTemplates | undefined => {
473
571
  if (!allowTemplates) {
474
572
  return undefined;
475
573
  }
@@ -484,7 +582,20 @@ export const TemplateField = memo(
484
582
  result = filterTemplatesFunc(result);
485
583
  }
486
584
 
487
- return result;
585
+ /* A lone category is opened, once, HERE — where this control resolves the
586
+ templates every one of its pickers then reads: the "Select Template"
587
+ dropdown, the in-editor `$` list, the numeric field's focus dropdown and
588
+ the expression builder's argument picker. Applying it per picker meant
589
+ each new one had to remember, and the ones that forgot made the author
590
+ click through a header naming the only category on offer to reach the
591
+ only values on offer.
592
+
593
+ LAST, after both filters. `filterTemplatesFunc` is given the grouped
594
+ shape on purpose — the Qog expression builder filters top-level items by
595
+ `metadata.dataRole`, which is a property of the CATEGORY — so flattening
596
+ before it would hand it values and quietly change what it keeps. This
597
+ only changes what is finally shown. */
598
+ return { ...result, items: templateItemsToShow(result?.items) };
488
599
  }, [
489
600
  JSON.stringify(templates),
490
601
  type,
@@ -509,24 +620,74 @@ export const TemplateField = memo(
509
620
  // Only while the field is EMPTY. A field already holding a literal must
510
621
  // open showing that literal — flipping to the template view would hide a
511
622
  // value the author put there and make it look lost.
512
- // ...and only when there is actually something to pick. A field with no
513
- // templates on offer would otherwise open on an EMPTY picker, which is a
514
- // worse place to start than the type picker it replaced.
515
- const typeIsAnyLike = type === 'any' || type === 'auto';
516
- const isEmptyValue = value === undefined || value === null || value === '';
623
+ //
624
+ // A field that may hold a custom value opens there whether or not templates
625
+ // are on offer, and whether or not they have ARRIVED: template mode gives it
626
+ // a typable editor, which offers the list once there is one. Waiting for the
627
+ // list decided the landing from data that is async — on a cold load the
628
+ // type-filtered list did not exist at mount, so the field fell to the type
629
+ // picker and stayed there. A pick-only field (no custom values) is in
630
+ // template mode regardless, by the `!allowCustomValues` term.
631
+ const typeIsAnyLike = isUntypedOptionType(type);
632
+ /* An EMPTY field, for the purpose of offering the template selector.
633
+
634
+ `null` is deliberately not empty. It is the DPQL null literal — a value
635
+ an author writes to say "this is nothing", and the only way to assert
636
+ that a call returned no value. Counted as emptiness here, an argument
637
+ holding it was treated as a field the author had just cleared: with a
638
+ type of `any` and templates on offer, the selector-restore effect below
639
+ flipped the field into template mode with a null template value, and
640
+ that reported the field as cleared — so writing `null` in the Text tab
641
+ and switching to Visual silently erased it, leaving `{type: "any"}` in
642
+ the draft and the message "Value for argument 1 ("any") is invalid:
643
+ Value is empty".
644
+
645
+ `undefined` is the cleared state; `null` is a value. Same rule as
646
+ `expressions/argumentPresence`, which states it canonically. */
647
+ const isEmptyValue = value === undefined || value === '';
517
648
  const hasTemplatesOnOffer = !!size(filteredTemplates?.items);
518
- const [isTemplate, setIsTemplate] = useState<boolean>(
519
- (isDefaultTemplate ||
520
- isValueTemplate(value) ||
521
- !allowCustomValues ||
522
- (typeIsAnyLike && isEmptyValue && hasTemplatesOnOffer)) &&
649
+ /* The field's own editor already renders templates inline, so the selector
650
+ is not merely unnecessary here — it is a downgrade, swapping a typable
651
+ control for a pick-only one. Derived rather than folded into the state
652
+ below so every `setIsTemplate` path keeps working untouched; they simply
653
+ stop having anything to say for these fields. */
654
+ const editorHandlesTemplates =
655
+ !!allowTemplates && isTemplateAwareUiType(type as string, (rest as any).templateAwareUiTypes);
656
+
657
+ const hasOnlyAllowedValues = useMemo(
658
+ () => !!size(rest.allowed_values) && !rest.allowed_values_creatable,
659
+ [rest.allowed_values, rest.allowed_values_creatable]
660
+ );
661
+
662
+ /** Where this field lands when it is empty: an untyped one in template mode — see above. */
663
+ const emptyLandsOnTemplates =
664
+ typeIsAnyLike && (hasTemplatesOnOffer || (!!allowCustomValues && !hasOnlyAllowedValues));
665
+ const opensOnTemplates = isEmptyValue && emptyLandsOnTemplates;
666
+
667
+ const [isTemplateState, setIsTemplate] = useState<boolean>(
668
+ (isDefaultTemplate || isValueTemplate(value) || !allowCustomValues || opensOnTemplates) &&
523
669
  allowTemplates
524
670
  );
671
+
672
+ const isTemplate = editorHandlesTemplates ? false : isTemplateState;
525
673
  const [internalIsFunction, setInternalIsFunction] = useState<boolean>(
526
674
  !!isDefaultFunction && !!allowFunctions
527
675
  );
528
676
  const [templateValue, setTemplateValue] = useState<string | null>(value);
529
677
 
678
+ // Typed text that turned out to be an expression the author may accept.
679
+ const [dpqlOffer, setDpqlOffer] = useState<{ text: string; expression: any } | null>(null);
680
+ // The literal this field held before typed text switched it into
681
+ // expression mode — kept so the switch is undoable, and so the editor
682
+ // opens on the Text view the author was already writing in.
683
+ const [expressionFromText, setExpressionFromText] = useState<string | null>(null);
684
+ // Texts the author has said no to. Without this, dismissing an offer (or
685
+ // undoing a switch) would re-detect the same text on the next render and
686
+ // ask again forever.
687
+ const declinedTexts = useRef<Set<string>>(new Set());
688
+ const detectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
689
+ const probeDpql = useDpqlProbe();
690
+
530
691
  const effectiveIsFunction = isFunction || internalIsFunction;
531
692
 
532
693
  useWhyDidYouUpdate(`Template field ${name}`, {
@@ -542,6 +703,63 @@ export const TemplateField = memo(
542
703
  }
543
704
  }, [JSON.stringify(value)]);
544
705
 
706
+ /* Clearing an untyped field returns it to the template selector, which is
707
+ where mounting it empty already lands.
708
+
709
+ The rule above that opens an `any`-like field on the template selector is
710
+ a `useState` INITIALISER, so it only ever ran on mount. Clearing a value
711
+ in place does not remount anything, so the field fell through to the TYPE
712
+ picker and demanded `string`/`int`/`hash` before it would let the author
713
+ name a value they had already captured -- the very question that rule
714
+ exists to stop asking. Reloading the page fixed it, which is the tell:
715
+ same field, same empty value, different landing, because only one of the
716
+ two paths ran the rule.
717
+
718
+ Watched as a TRANSITION from a value to no value, not as "the value is
719
+ empty": an author who picks `Set Custom Value` on an empty field is
720
+ asking for the type picker, and re-asserting the template view on every
721
+ render while the field sat empty would take it away again immediately. */
722
+ const hadValue = useRef(!isEmptyValue);
723
+ /* A clear is REMEMBERED rather than acted on in the same render.
724
+
725
+ While the field holds a value its resolved type is that VALUE's type — a
726
+ reference reads as `test-reference`, not `auto` — and clearing reverts it
727
+ to the schema's `auto` one render LATER than the value empties. Acting
728
+ only on the transition therefore ran while `typeIsAnyLike` was still
729
+ false and did nothing, and by the time the type settled the transition
730
+ had passed. Measured in the live IDE at the moment of the clear:
731
+ `{ wasCleared: true, typeIsAnyLike: false, type: "test-reference",
732
+ hasTemplatesOnOffer: true }`, then `typeIsAnyLike: true` on the very next
733
+ render with `wasCleared` already false. */
734
+ const restoreSelectorWhenSettled = useRef(false);
735
+ /** Set by the template control's own `×`, which is an author saying "not a
736
+ * template" — that clear must not be answered with the selector again. */
737
+ const suppressSelectorRestore = useRef(false);
738
+
739
+ useEffect(() => {
740
+ if (hadValue.current && isEmptyValue) {
741
+ restoreSelectorWhenSettled.current = !suppressSelectorRestore.current;
742
+ suppressSelectorRestore.current = false;
743
+ }
744
+ if (!isEmptyValue) {
745
+ // A value arrived: whatever the author did, they are not sitting on an
746
+ // empty field waiting to be offered the list.
747
+ restoreSelectorWhenSettled.current = false;
748
+ }
749
+ hadValue.current = !isEmptyValue;
750
+
751
+ if (
752
+ restoreSelectorWhenSettled.current &&
753
+ opensOnTemplates &&
754
+ allowTemplates &&
755
+ !effectiveIsFunction
756
+ ) {
757
+ restoreSelectorWhenSettled.current = false;
758
+ setTemplateValue(null);
759
+ setIsTemplate(true);
760
+ }
761
+ }, [isEmptyValue, opensOnTemplates, allowTemplates, effectiveIsFunction]);
762
+
545
763
  useEffect(() => {
546
764
  if (allowCustomValues && isTemplate && value && !isValueTemplate(value)) {
547
765
  setIsTemplate(false);
@@ -562,6 +780,17 @@ export const TemplateField = memo(
562
780
  return;
563
781
  }
564
782
 
783
+ // A template reference with an OPERATOR on it (`$local:count + 1`) is
784
+ // an expression, not a template. The loose check above passes it —
785
+ // it starts with `$` and has a colon — so without this every such
786
+ // string was swallowed into template mode, where the arithmetic is
787
+ // dead text and no affordance can reach it. A token standing alone
788
+ // (`$local:count`, `$data:{1.field}`) has no operator and still
789
+ // flips, which is the case template mode is for.
790
+ if (!isCompleteTemplateToken(value) && mightBeDpqlExpression(value)) {
791
+ return;
792
+ }
793
+
565
794
  setIsTemplate(true);
566
795
  setTemplateValue(value);
567
796
  }
@@ -574,11 +803,6 @@ export const TemplateField = memo(
574
803
  }
575
804
  }, [JSON.stringify(templateValue)]);
576
805
 
577
- const hasOnlyAllowedValues = useMemo(
578
- () => !!size(rest.allowed_values) && !rest.allowed_values_creatable,
579
- [rest.allowed_values, rest.allowed_values_creatable]
580
- );
581
-
582
806
  const showTemplateToggle = allowCustomValues && allowTemplates && !rest.arg_schema;
583
807
 
584
808
  // Only a BRACED context ref (`$data:{…}` — machine-written, nobody types
@@ -590,8 +814,23 @@ export const TemplateField = memo(
590
814
  // the rich-text string mode.
591
815
  const templateValueIsBracedToken = isBracedTemplateToken(templateValue);
592
816
 
817
+ /* An UNTYPED field can be typed into as well as picked from.
818
+
819
+ Template mode has a typable editor whenever the field's type can hold
820
+ arbitrary text, and `any`/`auto` can — it is the type that has not been
821
+ narrowed yet, not a type that excludes text. Restricting this to
822
+ `string` meant an empty untyped field opened on template mode with no
823
+ editor to offer, so it fell through to the pick-only "Select Template"
824
+ dropdown: a control that can only choose from a list, on exactly the
825
+ fields where an author most often needs to write something the list
826
+ cannot hold — a deeper walk, a literal, or an expression.
827
+
828
+ Reported against a Qorus assertion's Expected Value, and it was never
829
+ specific to that field: every empty untyped field with templates on
830
+ offer got the same downgrade. The editor still offers the same templates
831
+ on focus, so nothing is lost by being able to type as well. */
593
832
  const templateSupportsCustomValues =
594
- allowCustomValues && type === 'string' && !hasOnlyAllowedValues;
833
+ allowCustomValues && (type === 'string' || typeIsAnyLike) && !hasOnlyAllowedValues;
595
834
  const showTemplatesDropdown =
596
835
  allowTemplates && (!allowCustomValues || (isTemplate && !templateSupportsCustomValues));
597
836
  const hasOnlyExpressions = !allowCustomValues && !allowTemplates && allowFunctions;
@@ -612,18 +851,28 @@ export const TemplateField = memo(
612
851
  const componentTypeProp = componentFromType ? { type } : {};
613
852
  const fieldAriaLabel = rest['aria-label'] ?? label ?? rest.display_name ?? name;
614
853
 
615
-
616
854
  const handleTemplateFieldChange = useCallback(
617
855
  (_name: string, val: string) => {
618
856
  if (!val) {
619
- setIsTemplate(false);
857
+ /* Emptying the text leaves the field where an empty field lands. For
858
+ an untyped field that is THIS editor: switching to custom mode here
859
+ mounted a different editor in its place, so deleting the last
860
+ character lost the cursor. */
861
+ if (!(emptyLandsOnTemplates && allowTemplates)) {
862
+ setIsTemplate(false);
863
+ }
620
864
  setTemplateValue(null);
621
865
  onChange(name, undefined);
622
866
  } else {
623
867
  setTemplateValue(val);
624
868
  }
625
869
  },
626
- [name, onChange]
870
+ [name, onChange, emptyLandsOnTemplates, allowTemplates]
871
+ );
872
+
873
+ const handleTemplateTextChange = useCallback(
874
+ (val: unknown) => handleTemplateFieldChange(name, typeof val === 'string' ? val : ''),
875
+ [handleTemplateFieldChange, name]
627
876
  );
628
877
 
629
878
  const handleSelectTemplateFromList = useCallback(
@@ -631,24 +880,38 @@ export const TemplateField = memo(
631
880
  (item) => {
632
881
  // If the template type is richtext, we need to wrap the template value in the richtext template format
633
882
  const value =
634
- item.badge === 'richtext'
635
- ? ([
636
- {
637
- type: 'paragraph',
638
- children: [
639
- {
640
- children: [{ text: '' }],
641
- label: item.label,
642
- type: 'tag',
643
- value: item.value,
644
- metadata: item.metadata,
645
- },
646
- ],
647
- },
648
- ] as IReqoreRichTextEditorProps['value'])
649
- : item.value;
650
-
651
- onChange(name, value, hasOnlyAllowedValues ? (type as TQorusType) : (item.badge as TQorusType));
883
+ item.badge === 'richtext' ?
884
+ ([
885
+ {
886
+ type: 'paragraph',
887
+ /* The empty text nodes on either side are load-bearing. A
888
+ chip is an inline VOID: it holds no text of its own, so the
889
+ only places a cursor can go are the text nodes AROUND it.
890
+ Slate treats that as an invariant and repairs it during
891
+ normalisation, which runs on edits but NOT on a document
892
+ handed in whole as a controlled value — which is what this
893
+ is. Without them the picked template renders as a chip that
894
+ cannot be typed after, so it can never be extended by hand. */
895
+ children: [
896
+ { text: '' },
897
+ {
898
+ children: [{ text: '' }],
899
+ label: item.label,
900
+ type: 'tag',
901
+ value: item.value,
902
+ metadata: item.metadata,
903
+ },
904
+ { text: '' },
905
+ ],
906
+ },
907
+ ] as IReqoreRichTextEditorProps['value'])
908
+ : item.value;
909
+
910
+ onChange(
911
+ name,
912
+ value,
913
+ hasOnlyAllowedValues ? (type as TQorusType) : (item.badge as TQorusType)
914
+ );
652
915
  },
653
916
  [name, onChange]
654
917
  );
@@ -658,7 +921,15 @@ export const TemplateField = memo(
658
921
  // IDE-only, so the menu item is dropped (`allowSaving` is inert).
659
922
 
660
923
  const handleRemoveTemplateClick = useCallback(() => {
924
+ /* This is the `×` ON the template control, and it means "I do not want a
925
+ template here" — on a field whose menu offers no `Set Custom Value` it
926
+ is the ONLY way to reach a literal. So it still drops to the custom
927
+ value editor, and it tells the empty-field rule below to keep its hands
928
+ off this particular clear: that rule watches the value going away, and
929
+ this handler clears the value too, so without the flag it would send
930
+ the author straight back to the selector they just dismissed. */
661
931
  if (allowCustomValues) {
932
+ suppressSelectorRestore.current = true;
662
933
  setIsTemplate(false);
663
934
  }
664
935
 
@@ -697,6 +968,25 @@ export const TemplateField = memo(
697
968
  value,
698
969
  ]);
699
970
 
971
+ /* Leaving template mode from the ⋮ — the counterpart of "Use Template".
972
+ Template mode's editor has no `×` of its own, so without this the author
973
+ who switched to a template had no way back and no menu to ask with. */
974
+ const handleUseCustomValueClick = useCallback(() => {
975
+ setIsTemplate(false);
976
+ /* A reference cannot stay in the custom editor: the field reads it as a
977
+ template again and switches straight back. Anything else is text the
978
+ author wrote, and it survives the switch. */
979
+ if (isValueTemplate(value as string) || isCompleteTemplateToken(templateValue)) {
980
+ /* Only where the value is actually cleared. The flag is read by the
981
+ "emptied" effect, which never runs when the text survives — setting
982
+ it unconditionally left it armed to swallow the NEXT genuine clear's
983
+ selector restore. */
984
+ suppressSelectorRestore.current = true;
985
+ setTemplateValue(null);
986
+ onChange?.(name, undefined);
987
+ }
988
+ }, [name, onChange, value, templateValue]);
989
+
700
990
  const handleTemplateToggleClick = useCallback(() => {
701
991
  setInternalIsFunction(false);
702
992
  onChange(name, undefined, undefined, false);
@@ -708,46 +998,327 @@ export const TemplateField = memo(
708
998
  (expressionValue: IExpression | undefined, remove: boolean) => {
709
999
  if (remove) {
710
1000
  setInternalIsFunction(false);
1001
+ // The expression is gone, so there is no longer a switch to undo.
1002
+ setExpressionFromText(null);
711
1003
  }
712
- onChange(name, expressionValue?.value, type as TQorusType, !remove);
1004
+ onChange(name, expressionValue?.value, expressionDataType as TQorusType, !remove);
1005
+ },
1006
+ [name, onChange, expressionDataType, value]
1007
+ );
1008
+
1009
+ // ─── Text typed into a plain field that is really a DPQL expression ───
1010
+ //
1011
+ // A field that accepts expressions still gets DPQL TYPED into it, in the
1012
+ // ordinary editor, by an author who never opened the expression view.
1013
+ // What happens then depends on the field's own type, and the asymmetry is
1014
+ // deliberate: text that could stand as a literal here is only ever
1015
+ // OFFERED, because silently rewriting the string `a + b` into a
1016
+ // concatenation would destroy a value the author meant. Text that could
1017
+ // NOT be a literal here was already an error the moment it was typed, so
1018
+ // switching costs nothing and explains the error.
1019
+ //
1020
+ // The server decides whether the text is an expression at all — see
1021
+ // `helpers/dpqlDetection` for why a successful parse alone means nothing.
1022
+ const validationField = useMemo(
1023
+ () => ({
1024
+ validation_regex: rest.validation_regex,
1025
+ has_to_be_valid_identifier: rest.has_to_be_valid_identifier,
1026
+ has_to_have_value: rest.has_to_have_value,
1027
+ rules: rest.rules,
1028
+ arg_schema: rest.arg_schema,
1029
+ }),
1030
+ [
1031
+ rest.validation_regex,
1032
+ rest.has_to_be_valid_identifier,
1033
+ rest.has_to_have_value,
1034
+ JSON.stringify(rest.rules),
1035
+ JSON.stringify(rest.arg_schema),
1036
+ ]
1037
+ );
1038
+
1039
+ const enterExpressionFromText = useCallback(
1040
+ (text: string, expression: any) => {
1041
+ setDpqlOffer(null);
1042
+ setExpressionFromText(text);
1043
+ setInternalIsFunction(true);
1044
+ setIsTemplate(false);
1045
+ setTemplateValue(null);
1046
+ // `dpql/parse` answers with the field-ready envelope
1047
+ // (`{is_expression, value}`); this field stores the inner AST and
1048
+ // signals the flag through `onChange`'s fourth argument, exactly as
1049
+ // `handleExpressionChange` does.
1050
+ onChange?.(name, expression?.value ?? expression, expressionDataType as TQorusType, true);
713
1051
  },
714
- [name, onChange, type, value]
1052
+ [name, onChange, expressionDataType]
715
1053
  );
716
1054
 
1055
+ const undoExpressionFromText = useCallback(() => {
1056
+ if (expressionFromText === null) {
1057
+ return;
1058
+ }
1059
+
1060
+ declinedTexts.current.add(expressionFromText);
1061
+ setInternalIsFunction(false);
1062
+ setExpressionFromText(null);
1063
+ onChange?.(name, expressionFromText, type as TQorusType, false);
1064
+ }, [expressionFromText, name, onChange, type]);
1065
+
1066
+ const declineDpqlOffer = useCallback(() => {
1067
+ if (dpqlOffer) {
1068
+ declinedTexts.current.add(dpqlOffer.text);
1069
+ }
1070
+
1071
+ setDpqlOffer(null);
1072
+ }, [dpqlOffer]);
1073
+
1074
+ const acceptDpqlOffer = useCallback(() => {
1075
+ if (dpqlOffer) {
1076
+ enterExpressionFromText(dpqlOffer.text, dpqlOffer.expression);
1077
+ }
1078
+ }, [dpqlOffer, enterExpressionFromText]);
1079
+
1080
+ // Held in a ref so the debounce below does not depend on it. `onChange`
1081
+ // comes from the host form and can be a fresh function on any render;
1082
+ // with the callback in the effect's deps, a re-render caused by a
1083
+ // DIFFERENT field would restart this field's timer, and a form the
1084
+ // author is typing in re-renders constantly.
1085
+ const enterExpressionRef = useRef(enterExpressionFromText);
1086
+ useEffect(() => {
1087
+ enterExpressionRef.current = enterExpressionFromText;
1088
+ }, [enterExpressionFromText]);
1089
+
1090
+ // Detection only runs on a field that could hold an expression and is
1091
+ // currently showing a plain editor — never on a read-only field, a
1092
+ // fixed-value list, or one already in template or expression mode.
1093
+ const canDetectDpql =
1094
+ !!allowFunctions &&
1095
+ !!allowTextExpressions &&
1096
+ !effectiveIsFunction &&
1097
+ !isTemplate &&
1098
+ !hasOnlyAllowedValues &&
1099
+ !rest.readonly &&
1100
+ !rest.readOnly &&
1101
+ !rest.disabled;
1102
+
1103
+ useEffect(() => {
1104
+ if (detectTimer.current) {
1105
+ clearTimeout(detectTimer.current);
1106
+ detectTimer.current = null;
1107
+ }
1108
+
1109
+ /* A richtext field is still text. It hands its value back as a Slate
1110
+ DOCUMENT rather than a flattened string — deliberately, so a chosen
1111
+ reference cannot fuse with text typed beside it — and reading only
1112
+ `typeof value === 'string'` here silently switched detection off for
1113
+ every one of them the moment the field changed shape. A test
1114
+ assertion's Value is exactly that field, and `1 + 2` typed into it
1115
+ stopped being offered as an expression.
1116
+
1117
+ `richtextToString` flattens a tag to its raw value, so a document
1118
+ holding only a chosen reference reads as `$.result` — no operator, so
1119
+ `mightBeDpqlExpression` rejects it below and the server is never
1120
+ asked. */
1121
+ const text =
1122
+ typeof value === 'string' ? value
1123
+ : Array.isArray(value) ? richtextToString(value as never)
1124
+ : undefined;
1125
+
1126
+ if (
1127
+ !canDetectDpql ||
1128
+ !text ||
1129
+ declinedTexts.current.has(text) ||
1130
+ !mightBeDpqlExpression(text)
1131
+ ) {
1132
+ setDpqlOffer(null);
1133
+ return undefined;
1134
+ }
1135
+
1136
+ let cancelled = false;
1137
+
1138
+ detectTimer.current = setTimeout(() => {
1139
+ detectTimer.current = null;
1140
+
1141
+ void probeDpql(text)
1142
+ .then((parsed) => {
1143
+ if (cancelled) {
1144
+ return;
1145
+ }
1146
+
1147
+ const outcome = classifyTypedText({ text, type, field: validationField, parsed });
1148
+
1149
+ if (outcome === 'switch') {
1150
+ enterExpressionRef.current(text, parsed.expression);
1151
+ } else if (outcome === 'offer') {
1152
+ setDpqlOffer({ text, expression: parsed.expression });
1153
+ } else {
1154
+ setDpqlOffer(null);
1155
+ }
1156
+ })
1157
+ .catch(() => {
1158
+ // Detection is an offer of help, never a reason for a field to
1159
+ // break: an unreachable instance or a classifier that threw leaves
1160
+ // the author's text exactly where they put it.
1161
+ if (!cancelled) {
1162
+ setDpqlOffer(null);
1163
+ }
1164
+ });
1165
+ }, DPQL_DETECT_DEBOUNCE_MS);
1166
+
1167
+ return () => {
1168
+ cancelled = true;
1169
+
1170
+ if (detectTimer.current) {
1171
+ clearTimeout(detectTimer.current);
1172
+ detectTimer.current = null;
1173
+ }
1174
+ };
1175
+ }, [value, canDetectDpql, type, validationField, probeDpql]);
1176
+
1177
+ const canOfferExpression =
1178
+ allowFunctions && !hasOnlyAllowedValues && !rest.readonly && !internalIsFunction;
1179
+ const canOfferTemplate = showTemplateToggle && !isTemplate;
1180
+ /* The way back out, published on the same terms the field's own ⋮ draws it.
1181
+ A field inside a form ROW draws no menu of its own — it publishes into
1182
+ the row's — so leaving this out of the published list meant template mode
1183
+ was a one-way door on the surface most options are edited from. */
1184
+ const canOfferCustomValue = showTemplateToggle && isTemplate && templateSupportsCustomValues;
1185
+
1186
+ const publishedItems = useMemo(
1187
+ () => [
1188
+ ...(canOfferExpression && !functions.loading
1189
+ ? [
1190
+ {
1191
+ label: 'Use Expression',
1192
+ icon: 'Functions' as const,
1193
+ tooltip: 'Run a function on this value',
1194
+ onClick: handleSelectFunctionChange,
1195
+ },
1196
+ ]
1197
+ : []),
1198
+ ...(canOfferTemplate
1199
+ ? [
1200
+ {
1201
+ label: 'Use Template',
1202
+ icon: 'MoneyDollarCircleLine' as const,
1203
+ tooltip: 'Use a template',
1204
+ onClick: handleTemplateToggleClick,
1205
+ },
1206
+ ]
1207
+ : []),
1208
+ ...(canOfferCustomValue
1209
+ ? [
1210
+ {
1211
+ label: 'Use Custom Value',
1212
+ icon: 'EditLine' as const,
1213
+ tooltip: 'Write the value here instead of choosing a template',
1214
+ onClick: handleUseCustomValueClick,
1215
+ },
1216
+ ]
1217
+ : []),
1218
+ /* The "set a value of this type" choices an untyped field offers.
1219
+ Dividers are dropped: they grouped items in a menu this field drew
1220
+ itself, and in the row's shared menu they would divide other
1221
+ people's. */
1222
+ ...((menuItems ?? []) as IUntypedFieldMenuItem[])
1223
+ .filter((item) => !('isDivider' in item))
1224
+ .map((item) => ({
1225
+ label: item.label as string,
1226
+ description: item.description as string | undefined,
1227
+ onClick: () =>
1228
+ item.onClick?.(undefined, () => {
1229
+ setIsTemplate(false);
1230
+ setTemplateValue(null);
1231
+ }),
1232
+ })),
1233
+ ],
1234
+ [
1235
+ canOfferExpression,
1236
+ functions.loading,
1237
+ canOfferTemplate,
1238
+ canOfferCustomValue,
1239
+ menuItems,
1240
+ handleSelectFunctionChange,
1241
+ handleTemplateToggleClick,
1242
+ handleUseCustomValueClick,
1243
+ ]
1244
+ );
1245
+
1246
+ /* Keyed on WHICH items are offered, never on the items: they carry
1247
+ handlers, so they are a new array every render and a row comparing them
1248
+ by value would loop. */
1249
+ const publishedKey = [
1250
+ canOfferExpression && !functions.loading ? 'expression' : '',
1251
+ canOfferTemplate ? 'template' : '',
1252
+ canOfferCustomValue ? 'custom-value' : '',
1253
+ `custom:${((menuItems ?? []) as { label?: unknown }[]).map((item) => String(item.label ?? '')).join('|')}`,
1254
+ ].join(',');
1255
+
1256
+ useRowMenuPublisher(rowMenu, publishedKey, publishedItems as never);
1257
+
717
1258
  const renderControls = useCallback(() => {
718
1259
  const showFunctionsDropdown =
719
1260
  allowFunctions && !hasOnlyAllowedValues && !rest.readonly && !internalIsFunction;
720
1261
  const showTemplatesButton = showTemplateToggle && !isTemplate;
1262
+ // The way back out of template mode, where the editor draws no `×`.
1263
+ const showCustomValueButton = showTemplateToggle && isTemplate && templateSupportsCustomValues;
721
1264
  // The "Set value" label promises a way to set one — reorder rows alone don't.
722
- const hasValueRows = showFunctionsDropdown || showTemplatesButton || size(menuItems) > 0;
1265
+ const hasValueRows =
1266
+ showFunctionsDropdown || showTemplatesButton || showCustomValueButton || size(menuItems) > 0;
1267
+
1268
+ /* A lone group opens itself. Two of the menu's groups are collapsed
1269
+ sections, so a menu holding nothing but one of them asked for a click
1270
+ to reach the only thing on offer — on an untyped field, "Set Custom
1271
+ Value" hiding the type rows behind it. Expanded, it still names what
1272
+ its rows are and can still be collapsed. The same rule the template
1273
+ list follows for a lone category, and the row menu already publishes
1274
+ these rows flat. */
1275
+ const loneSectionStartsExpanded =
1276
+ [
1277
+ showFunctionsDropdown,
1278
+ showTemplatesButton,
1279
+ showCustomValueButton,
1280
+ size(menuActions?.items) > 0,
1281
+ size(menuItems) > 0,
1282
+ ].filter(Boolean).length === 1;
1283
+
1284
+ /* ONE menu per control. Where this field sits in a form ROW, the row
1285
+ already renders a ⋮ of its own, and drawing a second one beside it
1286
+ put two menus of slightly different widths on the same control —
1287
+ with the type choices an `any` field offers hidden in the narrower
1288
+ one. The items are published to the row instead (see
1289
+ `rowMenuContext`); outside a row there is nothing to publish into,
1290
+ so the field goes on drawing its own. */
1291
+ if (rowMenu) {
1292
+ return null;
1293
+ }
723
1294
 
724
1295
  if (hasOnlyExpressions) {
725
- return showFunctionsDropdown ? (
726
- functions.loading ? (
727
- <ReqoreSkeleton size={rest.size} />
728
- ) : (
729
- <ReqoreButton
730
- compact
731
- minimal
732
- label='Create New Expression'
733
- className='function-selector'
734
- icon='Functions'
735
- tooltip='This field only accepts expressions'
736
- onClick={() => {
737
- setIsTemplate(false);
738
- setTemplateValue(null);
739
- onChange?.(
740
- name,
741
- {
742
- args: [],
743
- },
744
- undefined,
745
- true
746
- );
747
- }}
748
- />
749
- )
750
- ) : null;
1296
+ return (
1297
+ showFunctionsDropdown ?
1298
+ functions.loading ?
1299
+ <ReqoreSkeleton size={rest.size} />
1300
+ : <ReqoreButton
1301
+ compact
1302
+ minimal
1303
+ label='Create New Expression'
1304
+ className='function-selector'
1305
+ icon='Functions'
1306
+ tooltip='This field only accepts expressions'
1307
+ onClick={() => {
1308
+ setIsTemplate(false);
1309
+ setTemplateValue(null);
1310
+ onChange?.(
1311
+ name,
1312
+ {
1313
+ args: [],
1314
+ },
1315
+ undefined,
1316
+ true
1317
+ );
1318
+ }}
1319
+ />
1320
+ : null
1321
+ );
751
1322
  }
752
1323
 
753
1324
  if (hasValueRows || size(menuActions?.items) > 0 || size(menuTrailingItems) > 0) {
@@ -768,10 +1339,18 @@ export const TemplateField = memo(
768
1339
  transparent: true,
769
1340
  size: rest.size,
770
1341
  fixed: true,
771
- // Centre the trailing menu in its flex line so it lines up with
772
- // sibling action buttons (reqore alignSelf; replaces a reqraft
773
- // `align-self !important` override of this button).
774
- alignSelf: 'center',
1342
+ /* Centre the trailing menu in its flex line so it lines up
1343
+ with sibling action buttons (reqore alignSelf; replaces a
1344
+ reqraft `align-self !important` override of this button).
1345
+
1346
+ Centring is only right beside a ONE-LINE editor. The
1347
+ expression shell is a toolbar with an editor, a type message
1348
+ and a preview stacked under it, so centring put this menu
1349
+ somewhere down the side of that block — level with nothing,
1350
+ over the editor, and a long way from the Undo it belongs
1351
+ beside. There it goes to the top, which is where the shell's
1352
+ own toolbar is. */
1353
+ alignSelf: effectiveIsFunction ? 'flex-start' : 'center',
775
1354
  label: hasInputAffordance || !hasValueRows ? undefined : 'Set value',
776
1355
  style:
777
1356
  hasInputAffordance ?
@@ -797,11 +1376,10 @@ export const TemplateField = memo(
797
1376
  handler='click'
798
1377
  content={
799
1378
  <ReqoreMenu size={rest.size} maxHeight='400px' style={{ overflow: 'auto' }}>
800
- {showFunctionsDropdown ? (
801
- functions.loading ? (
1379
+ {showFunctionsDropdown ?
1380
+ functions.loading ?
802
1381
  <ReqoreSkeleton size={rest.size} />
803
- ) : (
804
- <ReqoreButton
1382
+ : <ReqoreButton
805
1383
  compact
806
1384
  transparent
807
1385
  label='Use Expression'
@@ -810,10 +1388,10 @@ export const TemplateField = memo(
810
1388
  tooltip='Run a function on this value'
811
1389
  onClick={handleSelectFunctionChange}
812
1390
  />
813
- )
814
- ) : null}
815
1391
 
816
- {showTemplatesButton ? (
1392
+ : null}
1393
+
1394
+ {showTemplatesButton ?
817
1395
  <ReqoreButton
818
1396
  transparent
819
1397
  icon='MoneyDollarCircleLine'
@@ -826,23 +1404,43 @@ export const TemplateField = memo(
826
1404
  {' '}
827
1405
  Use Template{' '}
828
1406
  </ReqoreButton>
829
- ) : null}
1407
+ : null}
1408
+
1409
+ {showCustomValueButton ?
1410
+ <ReqoreButton
1411
+ transparent
1412
+ icon='EditLine'
1413
+ className='template-custom-value'
1414
+ tooltip='Write the value here instead of choosing a template'
1415
+ compact
1416
+ size={rest.size}
1417
+ onClick={handleUseCustomValueClick}
1418
+ >
1419
+ {' '}
1420
+ Use Custom Value{' '}
1421
+ </ReqoreButton>
1422
+ : null}
830
1423
 
831
- {size(menuActions?.items) > 0 ? (
832
- <MenuActionsSection actions={menuActions} size={rest.size} />
833
- ) : null}
1424
+ {size(menuActions?.items) > 0 ?
1425
+ <MenuActionsSection
1426
+ actions={menuActions}
1427
+ size={rest.size}
1428
+ startExpanded={loneSectionStartsExpanded}
1429
+ />
1430
+ : null}
834
1431
 
835
- {size(menuItems) > 0 ? (
1432
+ {size(menuItems) > 0 ?
836
1433
  <CustomMenuItems
837
1434
  items={menuItems}
838
1435
  setIsTemplate={setIsTemplate}
839
1436
  setTemplateValue={setTemplateValue}
1437
+ startExpanded={loneSectionStartsExpanded}
840
1438
  />
841
- ) : null}
1439
+ : null}
842
1440
 
843
- {size(menuTrailingItems) > 0 ? (
1441
+ {size(menuTrailingItems) > 0 ?
844
1442
  <MenuTrailingItems items={menuTrailingItems} size={rest.size} />
845
- ) : null}
1443
+ : null}
846
1444
  </ReqoreMenu>
847
1445
  }
848
1446
  />
@@ -855,11 +1453,14 @@ export const TemplateField = memo(
855
1453
  functions.expressions,
856
1454
  handleSelectFunctionChange,
857
1455
  handleTemplateToggleClick,
1456
+ handleUseCustomValueClick,
858
1457
  hasOnlyAllowedValues,
859
1458
  isTemplate,
860
1459
  rest.readonly,
861
1460
  rest.size,
862
1461
  showTemplateToggle,
1462
+ // reaches the menu through `templateSupportsCustomValues`
1463
+ allowCustomValues,
863
1464
  hasOnlyExpressions,
864
1465
  type,
865
1466
  value,
@@ -867,23 +1468,37 @@ export const TemplateField = memo(
867
1468
  menuActions,
868
1469
  menuTrailingItems,
869
1470
  internalIsFunction,
1471
+ effectiveIsFunction,
870
1472
  hasInputAffordance,
1473
+ // the row decides whether this field draws a menu at all
1474
+ rowMenu,
871
1475
  ]);
872
1476
 
873
1477
  // When the type is a list, and it has an element type - that element type is different
874
1478
  // from the field type, so we need to send down the full templates object
875
1479
  // and the actual rendered field will filter it's own templates
876
1480
  // This is a special case only for lists with element types
877
- const componentTemplates = useMemo(
878
- () =>
879
- type === 'list' && (rest.ui_element_type || rest.element_type)
880
- ? templates
881
- : {
882
- ...filteredTemplates,
883
- ...TemplatesListProps,
884
- },
885
- [JSON.stringify(filteredTemplates), rest.ui_element_type, rest.element_type, type]
886
- );
1481
+ /* `undefined` when nothing is on offer, rather than a list with no items:
1482
+ an editor given a list draws the control that opens it, so a field whose
1483
+ templates were all filtered out by type opened an EMPTY menu. */
1484
+ const componentTemplates = useMemo(() => {
1485
+ if (type === 'list' && (rest.ui_element_type || rest.element_type)) {
1486
+ return templates;
1487
+ }
1488
+ return size(filteredTemplates?.items) ?
1489
+ {
1490
+ ...filteredTemplates,
1491
+ ...TemplatesListProps,
1492
+ }
1493
+ : undefined;
1494
+ }, [
1495
+ JSON.stringify(filteredTemplates),
1496
+ // the list-with-element-type branch hands this back untouched
1497
+ templates,
1498
+ rest.ui_element_type,
1499
+ rest.element_type,
1500
+ type,
1501
+ ]);
887
1502
 
888
1503
  if (effectiveIsFunction && !hasOnlyAllowedValues) {
889
1504
  // SEAM (reqraft): `allowTextExpressions` swaps the IDE's bare builder
@@ -899,9 +1514,14 @@ export const TemplateField = memo(
899
1514
  is_expression: true,
900
1515
  value,
901
1516
  }}
1517
+ // The host's per-ui_type editors reach this field through the
1518
+ // rest-spread; the expression shell needs them explicitly or the
1519
+ // builder's operands render "Unknown type!" for any consumer
1520
+ // ui_type (an assertion's `test-reference` Value, for one).
1521
+ componentOverrides={componentOverrides}
902
1522
  localTemplates={templates}
903
1523
  type={type as string}
904
- returnType={(returnType || type) as any}
1524
+ returnType={(returnType || expressionDataType) as any}
905
1525
  onChange={handleExpressionChange}
906
1526
  readOnly={rest.readOnly || rest.disabled}
907
1527
  expressions={rest.expressions}
@@ -909,9 +1529,29 @@ export const TemplateField = memo(
909
1529
  serverHandled={rest.server_expression_handling}
910
1530
  extraActions={extraActions}
911
1531
  size={rest.size}
1532
+ // An author who typed the expression as text is already
1533
+ // writing in that language — dropping them into the visual
1534
+ // builder would make them find their own sentence again.
1535
+ defaultMode={expressionFromText !== null ? 'text' : 'visual'}
912
1536
  reorder={reorder}
913
1537
  />
914
1538
  </ReqoreErrorBoundary>
1539
+ {expressionFromText !== null ?
1540
+ <ReqoreButton
1541
+ fixed
1542
+ compact
1543
+ minimal
1544
+ icon='ArrowGoBackLine'
1545
+ // No `intent` with `minimal`: a Reqore intent is a FILL, and
1546
+ // `muted` as TEXT is 1.3:1 on the app surface.
1547
+ size={rest.size}
1548
+ className='dpql-detected-undo'
1549
+ tooltip={`Keep "${expressionFromText}" as plain text instead`}
1550
+ onClick={undoExpressionFromText}
1551
+ >
1552
+ Undo
1553
+ </ReqoreButton>
1554
+ : null}
915
1555
  {renderControls()}
916
1556
  </ReqoreControlGroup>
917
1557
  );
@@ -925,10 +1565,11 @@ export const TemplateField = memo(
925
1565
  is_expression: true,
926
1566
  value,
927
1567
  }}
1568
+ componentOverrides={componentOverrides}
928
1569
  localTemplates={templates}
929
1570
  level={level}
930
1571
  type={type as string}
931
- returnType={(returnType || type) as any}
1572
+ returnType={(returnType || expressionDataType) as any}
932
1573
  onChange={handleExpressionChange}
933
1574
  readOnly={rest.readOnly || rest.disabled}
934
1575
  expressions={rest.expressions}
@@ -952,7 +1593,15 @@ export const TemplateField = memo(
952
1593
  return <ReadOnlyTemplateTag value={templateValue} templates={templates} size={rest.size} />;
953
1594
  }
954
1595
 
955
- return <Comp value={value} onChange={onChange} name={name} {...rest} />;
1596
+ return (
1597
+ <Comp
1598
+ value={value}
1599
+ onChange={onChange}
1600
+ name={name}
1601
+ {...rest}
1602
+ componentOverrides={componentOverrides}
1603
+ />
1604
+ );
956
1605
  }
957
1606
 
958
1607
  return (
@@ -964,7 +1613,7 @@ export const TemplateField = memo(
964
1613
  stack={false}
965
1614
  verticalAlign='flex-start'
966
1615
  >
967
- {!isTemplate && allowCustomValues ? (
1616
+ {!isTemplate && allowCustomValues ?
968
1617
  <Component
969
1618
  value={value}
970
1619
  allowTemplates={allowTemplates}
@@ -972,32 +1621,60 @@ export const TemplateField = memo(
972
1621
  name={name}
973
1622
  level={level}
974
1623
  {...rest}
1624
+ componentOverrides={componentOverrides}
975
1625
  {...componentTypeProp}
976
1626
  aria-label={fieldAriaLabel}
977
1627
  className={`${className} template-selector`}
978
1628
  templates={componentTemplates}
979
1629
  />
980
- ) : null}
981
-
982
- {isTemplate && templateSupportsCustomValues && !templateValueIsBracedToken ? (
983
- <LongStringField
1630
+ : null}
1631
+
1632
+ {/* Template mode's editor for a value that can also be typed: each
1633
+ reference in it is a chip named as the catalogue names it, while the
1634
+ field still stores the plain string. A textarea spelled a chosen
1635
+ template `$local:name` wherever it was edited — in the Visual
1636
+ builder's operands, in every string field that takes templates. */}
1637
+ {isTemplate && templateSupportsCustomValues && !templateValueIsBracedToken ?
1638
+ <RichTextFormField
984
1639
  className='template-selector'
985
- type='string'
986
- name='templateVal'
987
- level={level}
988
- value={templateValue}
989
- templates={{
990
- ...filteredTemplates,
991
- ...TemplatesListProps,
992
- }}
993
- onChange={handleTemplateFieldChange}
1640
+ valueFormat='text'
1641
+ singleLine={isSingleLineStringType('string')}
1642
+ value={typeof templateValue === 'string' ? templateValue : ''}
1643
+ templates={filteredTemplates}
1644
+ allowTemplates
1645
+ onChange={handleTemplateTextChange}
994
1646
  {...rest}
995
1647
  aria-label={fieldAriaLabel}
996
1648
  />
997
- ) : null}
1649
+ : null}
1650
+
1651
+ {dpqlOffer ?
1652
+ <ReqoreControlGroup fixed stack size={rest.size}>
1653
+ <ReqoreButton
1654
+ compact
1655
+ icon='Functions'
1656
+ intent='info'
1657
+ className='dpql-detected-offer'
1658
+ tooltip={`"${dpqlOffer.text}" reads as an expression. It is also valid text here, so nothing has been changed — use it as an expression?`}
1659
+ onClick={acceptDpqlOffer}
1660
+ >
1661
+ Use as expression
1662
+ </ReqoreButton>
1663
+ <ReqoreButton
1664
+ compact
1665
+ icon='CloseLine'
1666
+ intent='info'
1667
+ className='dpql-detected-dismiss'
1668
+ tooltip='Keep it as plain text'
1669
+ onClick={declineDpqlOffer}
1670
+ />
1671
+ </ReqoreControlGroup>
1672
+ : null}
998
1673
 
999
- {showTemplatesDropdown ||
1000
- (isTemplate && templateSupportsCustomValues && templateValueIsBracedToken) ? (
1674
+ {(
1675
+ showTemplatesDropdown ||
1676
+ (isTemplate && templateSupportsCustomValues && templateValueIsBracedToken)
1677
+ ) ?
1001
1678
  <TemplateDropdownSelector
1002
1679
  allowCustomValues={allowCustomValues}
1003
1680
  templates={templates}
@@ -1009,10 +1686,27 @@ export const TemplateField = memo(
1009
1686
  label={label}
1010
1687
  hasOnlyAllowedValues={hasOnlyAllowedValues}
1011
1688
  />
1012
- ) : null}
1689
+ : null}
1013
1690
 
1014
1691
  {renderControls()}
1015
1692
  </ReqoreControlGroup>
1016
1693
  );
1017
1694
  }
1018
1695
  );
1696
+
1697
+ /**
1698
+ * The row channel belongs to the row's OWN editor: this field. It is read here
1699
+ * and hidden from everything this field renders, so a field nested inside it —
1700
+ * an expression operand, a list item — keeps drawing its own menu instead of
1701
+ * publishing actions into a row menu that cannot say which value they act on.
1702
+ * `undefined` outside a row, where this field draws its own menu too.
1703
+ */
1704
+ export const TemplateField = memo((props: ITemplateFieldProps) => {
1705
+ const rowMenu = useRowMenu();
1706
+
1707
+ return (
1708
+ <RowMenuContext.Provider value={undefined}>
1709
+ <TemplateFieldImpl {...props} rowMenu={rowMenu} />
1710
+ </RowMenuContext.Provider>
1711
+ );
1712
+ });