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

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 (291) hide show
  1. package/.claude/CLAUDE.md +25 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
  4. package/README.md +1 -1
  5. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  6. package/design/FORM_ENGINE_FIELD_MIGRATION.md +155 -1
  7. package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
  8. package/design/SMART_EDITOR_UX.md +21 -0
  9. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  10. package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
  11. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  12. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
  13. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  14. package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
  15. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  16. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  17. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  18. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  19. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  20. package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
  21. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  22. package/dist/components/dpqlEditor/dpqlTags.js +5 -1
  23. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  24. package/dist/components/dpqlEditor/index.d.ts +4 -0
  25. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  26. package/dist/components/dpqlEditor/index.js +5 -1
  27. package/dist/components/dpqlEditor/index.js.map +1 -1
  28. package/dist/components/dpqlEditor/types.d.ts +42 -2
  29. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  30. package/dist/components/dpqlEditor/types.js +0 -4
  31. package/dist/components/dpqlEditor/types.js.map +1 -1
  32. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  33. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  34. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  35. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  36. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  37. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  38. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  39. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  40. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  41. package/dist/components/form/engine/CompactRow.js +192 -45
  42. package/dist/components/form/engine/CompactRow.js.map +1 -1
  43. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  44. package/dist/components/form/engine/CompactToolbar.js +5 -2
  45. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  46. package/dist/components/form/engine/FormEngine.d.ts +160 -12
  47. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  48. package/dist/components/form/engine/FormEngine.js +380 -78
  49. package/dist/components/form/engine/FormEngine.js.map +1 -1
  50. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  51. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  52. package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
  53. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  54. package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
  55. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  56. package/dist/components/form/engine/OptionFieldMessages.js +73 -12
  57. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  58. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  59. package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
  60. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  61. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  62. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  63. package/dist/components/form/engine/readFirst.d.ts +42 -1
  64. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  65. package/dist/components/form/engine/readFirst.js +74 -7
  66. package/dist/components/form/engine/readFirst.js.map +1 -1
  67. package/dist/components/form/engine/rendererTypes.d.ts +13 -0
  68. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  69. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  70. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  71. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  72. package/dist/components/form/engine/rowMenuContext.js +103 -0
  73. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  74. package/dist/components/form/engine/typeChoices.d.ts +42 -0
  75. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  76. package/dist/components/form/engine/typeChoices.js +31 -0
  77. package/dist/components/form/engine/typeChoices.js.map +1 -0
  78. package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
  79. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
  80. package/dist/components/form/expressions/DpqlRendering.js +53 -0
  81. package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
  82. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  83. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  84. package/dist/components/form/expressions/ExpressionField.js +203 -45
  85. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  86. package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
  87. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  88. package/dist/components/form/expressions/argumentPresence.js +203 -0
  89. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  90. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  91. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  92. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  93. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  94. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  95. package/dist/components/form/expressions/builder/index.js +20 -58
  96. package/dist/components/form/expressions/builder/index.js.map +1 -1
  97. package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
  98. package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
  99. package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
  100. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  101. package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
  102. package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
  103. package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
  104. package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
  105. package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
  106. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
  107. package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
  108. package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
  109. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  110. package/dist/components/form/expressions/mockExpressions.js +8 -4
  111. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  112. package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
  113. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  114. package/dist/components/form/expressions/renderExpressionToText.js +95 -19
  115. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  116. package/dist/components/form/expressions/types.d.ts +7 -0
  117. package/dist/components/form/expressions/types.d.ts.map +1 -1
  118. package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
  119. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  120. package/dist/components/form/expressions/useRenderExpression.js +50 -126
  121. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  122. package/dist/components/form/fields/Field.d.ts.map +1 -1
  123. package/dist/components/form/fields/Field.js +3 -1
  124. package/dist/components/form/fields/Field.js.map +1 -1
  125. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  126. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  127. package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
  128. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  129. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  130. package/dist/components/form/fields/auto/AutoFormField.js +91 -28
  131. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  132. package/dist/components/form/fields/date/Date.d.ts +1 -1
  133. package/dist/components/form/fields/date/Date.d.ts.map +1 -1
  134. package/dist/components/form/fields/date/Date.js +16 -2
  135. package/dist/components/form/fields/date/Date.js.map +1 -1
  136. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  137. package/dist/components/form/fields/long-string/LongString.js +14 -2
  138. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  139. package/dist/components/form/fields/rich-text/RichText.d.ts +35 -2
  140. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  141. package/dist/components/form/fields/rich-text/RichText.js +153 -12
  142. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  143. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  144. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  145. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  146. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  147. package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
  148. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  149. package/dist/components/form/fields/template/TemplateField.js +604 -58
  150. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  151. package/dist/components/form/index.d.ts +1 -0
  152. package/dist/components/form/index.d.ts.map +1 -1
  153. package/dist/components/form/index.js +1 -0
  154. package/dist/components/form/index.js.map +1 -1
  155. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  156. package/dist/components/smartEditor/SmartEditor.js +29 -8
  157. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  158. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
  159. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
  160. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
  161. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
  162. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  163. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  164. package/dist/components/smartEditor/useLspSession.js +16 -2
  165. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  166. package/dist/helpers/dpqlDetection.d.ts +58 -0
  167. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  168. package/dist/helpers/dpqlDetection.js +164 -0
  169. package/dist/helpers/dpqlDetection.js.map +1 -0
  170. package/dist/helpers/optionUiTypes.d.ts +19 -3
  171. package/dist/helpers/optionUiTypes.d.ts.map +1 -1
  172. package/dist/helpers/optionUiTypes.js +20 -1
  173. package/dist/helpers/optionUiTypes.js.map +1 -1
  174. package/dist/helpers/options.d.ts +37 -0
  175. package/dist/helpers/options.d.ts.map +1 -1
  176. package/dist/helpers/options.js +78 -1
  177. package/dist/helpers/options.js.map +1 -1
  178. package/dist/helpers/templateItems.d.ts +47 -0
  179. package/dist/helpers/templateItems.d.ts.map +1 -0
  180. package/dist/helpers/templateItems.js +88 -0
  181. package/dist/helpers/templateItems.js.map +1 -0
  182. package/dist/helpers/templateText.d.ts +37 -0
  183. package/dist/helpers/templateText.d.ts.map +1 -0
  184. package/dist/helpers/templateText.js +64 -0
  185. package/dist/helpers/templateText.js.map +1 -0
  186. package/dist/helpers/templates.d.ts +19 -11
  187. package/dist/helpers/templates.d.ts.map +1 -1
  188. package/dist/helpers/templates.js +46 -29
  189. package/dist/helpers/templates.js.map +1 -1
  190. package/dist/helpers/validations.d.ts.map +1 -1
  191. package/dist/helpers/validations.js +56 -9
  192. package/dist/helpers/validations.js.map +1 -1
  193. package/dist/hooks/useArgSchema.d.ts +2 -1
  194. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  195. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  196. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  197. package/dist/hooks/useFetch/useFetch.js +47 -9
  198. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  199. package/dist/hooks/useTemplates.d.ts +20 -0
  200. package/dist/hooks/useTemplates.d.ts.map +1 -1
  201. package/dist/hooks/useTemplates.js +34 -2
  202. package/dist/hooks/useTemplates.js.map +1 -1
  203. package/dist/index.d.ts +2 -1
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js +5 -1
  206. package/dist/index.js.map +1 -1
  207. package/dist/stories/storyNetwork.d.ts +36 -0
  208. package/dist/stories/storyNetwork.d.ts.map +1 -0
  209. package/dist/stories/storyNetwork.js +82 -0
  210. package/dist/stories/storyNetwork.js.map +1 -0
  211. package/dist/utils/fetch.d.ts +151 -1
  212. package/dist/utils/fetch.d.ts.map +1 -1
  213. package/dist/utils/fetch.js +256 -44
  214. package/dist/utils/fetch.js.map +1 -1
  215. package/dist/utils/lspClient.d.ts +6 -0
  216. package/dist/utils/lspClient.d.ts.map +1 -1
  217. package/dist/utils/lspClient.js +62 -11
  218. package/dist/utils/lspClient.js.map +1 -1
  219. package/dist/utils/websocket.d.ts +6 -0
  220. package/dist/utils/websocket.d.ts.map +1 -1
  221. package/dist/utils/websocket.js +15 -7
  222. package/dist/utils/websocket.js.map +1 -1
  223. package/package.json +2 -2
  224. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  225. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  226. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  227. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  228. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  229. package/src/components/dpqlEditor/index.ts +4 -0
  230. package/src/components/dpqlEditor/types.ts +38 -2
  231. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  232. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  233. package/src/components/form/engine/CompactRow.tsx +240 -51
  234. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  235. package/src/components/form/engine/FormEngine.stories.tsx +431 -124
  236. package/src/components/form/engine/FormEngine.tsx +508 -90
  237. package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
  238. package/src/components/form/engine/OptionFieldMessages.tsx +103 -12
  239. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
  240. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  241. package/src/components/form/engine/readFirst.ts +86 -7
  242. package/src/components/form/engine/rendererTypes.ts +12 -0
  243. package/src/components/form/engine/rowMenuContext.ts +159 -0
  244. package/src/components/form/engine/typeChoices.ts +61 -0
  245. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  246. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  247. package/src/components/form/expressions/ExpressionField.tsx +266 -54
  248. package/src/components/form/expressions/argumentPresence.ts +200 -0
  249. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +31 -14
  250. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  251. package/src/components/form/expressions/builder/index.tsx +39 -77
  252. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  253. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  254. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  255. package/src/components/form/expressions/mockExpressions.ts +8 -4
  256. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  257. package/src/components/form/expressions/types.ts +7 -0
  258. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  259. package/src/components/form/fields/Field.tsx +3 -2
  260. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
  261. package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
  262. package/src/components/form/fields/auto/AutoFormField.tsx +90 -20
  263. package/src/components/form/fields/date/Date.tsx +17 -1
  264. package/src/components/form/fields/long-string/LongString.tsx +15 -3
  265. package/src/components/form/fields/rich-text/RichText.tsx +235 -30
  266. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  267. package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
  268. package/src/components/form/fields/template/TemplateField.tsx +823 -129
  269. package/src/components/form/index.tsx +1 -0
  270. package/src/components/log/Log.stories.tsx +2 -1
  271. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  272. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  273. package/src/components/smartEditor/useLspSession.ts +21 -3
  274. package/src/helpers/dpqlDetection.ts +209 -0
  275. package/src/helpers/optionUiTypes.ts +22 -0
  276. package/src/helpers/options.ts +70 -0
  277. package/src/helpers/templateItems.ts +81 -0
  278. package/src/helpers/templateText.ts +92 -0
  279. package/src/helpers/templates.ts +47 -37
  280. package/src/helpers/validations.ts +59 -9
  281. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  282. package/src/hooks/useFetch/useFetch.tsx +53 -7
  283. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  284. package/src/hooks/useTemplates.ts +41 -0
  285. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  286. package/src/index.tsx +14 -1
  287. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  288. package/src/stories/storyNetwork.ts +92 -0
  289. package/src/utils/fetch.ts +311 -21
  290. package/src/utils/lspClient.ts +68 -11
  291. package/src/utils/websocket.ts +20 -9
@@ -1,51 +1,36 @@
1
1
  // Copyright 2026 Qore Technologies, s.r.o.
2
- // The "Explain" seam. Turns an expression AST into a readable string.
2
+ // The "Explain" seam. Turns an expression AST into its readable rendering.
3
3
  //
4
- // The canonical rendering is the server's `DataProvider::renderExpression`,
5
- // reached over the LSP custom method `dpql/renderExpression` (added next to
6
- // `dpql/serialize` in `QorusLspWebSocketHandler.qc`). All consumers share ONE
7
- // render socket: the module-level `ReqraftLspClient` below rides the per-endpoint
8
- // `LspSharedConnection`, and JSON-RPC request ids associate each response
9
- // with the caller — N expression builders on one form means N independent
10
- // in-flight requests on one connection, no cross-wiring.
4
+ // The rendering is the server's `DataProvider::renderExpression`, reached over
5
+ // the LSP custom method `dpql/renderExpression` (added next to `dpql/serialize`
6
+ // in `QorusLspWebSocketHandler.qc`). All consumers share ONE render socket: the
7
+ // module-level `ReqraftLspClient` below rides the per-endpoint
8
+ // `LspSharedConnection`, and JSON-RPC request ids associate each response with
9
+ // the caller — N expression builders on one form means N independent in-flight
10
+ // requests on one connection, no cross-wiring.
11
11
  //
12
- // When the LSP is unreachable (or the server predates the method), `render`
13
- // falls back to the client-side approximation below; `serverRendering` tells
14
- // consumers which path served.
12
+ // There is no client-side stand-in. A Qorus form does not work without Qorus,
13
+ // so the only time the server was "unreachable" in practice was while the
14
+ // page's first connection was still opening — and racing that with a 1.5s timer
15
+ // replaced the real rendering with an approximation labelled "Approximate", on
16
+ // exactly the first Explain of every page. A rendering now waits for the
17
+ // connection; `null` means the server cannot render at all (it predates the
18
+ // method, or the connection gave up), which the caller says plainly.
15
19
  import { nanoid } from 'nanoid';
16
- import { useCallback, useEffect, useRef, useState } from 'react';
20
+ import { useCallback, useEffect, useRef } from 'react';
17
21
  import { ReqraftLspClient } from '../../../utils/lspClient';
18
- import { IExpressionSchema, IExpressionValue } from './types';
22
+ import { IExpressionValue } from './types';
19
23
 
24
+ /* The synchronous summary a collapsed row prints — see `readFirst.ts`. It is
25
+ not a stand-in for this rendering and is never shown in its place. */
20
26
  export { renderExpressionToText } from './renderExpressionToText';
21
- import { renderExpressionToText } from './renderExpressionToText';
22
-
23
- // ── The shared render transport ───────────────────────────────────────
24
- // One lazy module-level client for ALL `useRenderExpression` consumers.
25
- // Rendering needs no document/context state, so a single connection (shared
26
- // further with any open DPQL editors via `LspSharedConnection`) serves
27
- // every builder on the page.
28
27
 
29
28
  /** Coalesce rapid re-renders per consumer (live typing) — trailing edge. */
30
29
  const RENDER_DEBOUNCE_MS = 300;
31
- /** How long a render call waits for the socket before falling back. */
32
- const CONNECT_WAIT_MS = 1500;
33
- /** After a failed wait, fall back instantly for this long before re-waiting. */
34
- const CONNECT_RETRY_COOLDOWN_MS = 10000;
35
30
 
36
31
  let renderClient: ReqraftLspClient | null = null;
37
32
  /** Set after a `-32601 method not found` — the server predates the method. */
38
33
  let methodUnsupported = false;
39
- let lastConnectWaitFailedAt = 0;
40
-
41
- let lastServerRendering = false;
42
- const serverRenderingListeners = new Set<(on: boolean) => void>();
43
- const setLastServerRendering = (on: boolean): void => {
44
- if (on !== lastServerRendering) {
45
- lastServerRendering = on;
46
- serverRenderingListeners.forEach((listener) => listener(on));
47
- }
48
- };
49
34
 
50
35
  const getRenderClient = (): ReqraftLspClient => {
51
36
  if (!renderClient) {
@@ -59,55 +44,24 @@ const getRenderClient = (): ReqraftLspClient => {
59
44
  return renderClient;
60
45
  };
61
46
 
62
- /**
63
- * Wait (briefly) for the shared connection. `connect()` is re-entrant and the
64
- * underlying socket keeps its own reconnect schedule, so this only bounds how
65
- * long a single render call blocks before falling back — after a failed wait
66
- * we fall back instantly for a cooldown period instead of re-blocking.
67
- */
68
- const waitForConnection = async (client: ReqraftLspClient): Promise<boolean> => {
69
- if (client.isConnected) return true;
70
- if (Date.now() - lastConnectWaitFailedAt < CONNECT_RETRY_COOLDOWN_MS) {
71
- return false;
72
- }
73
- const connected = await Promise.race([
74
- client.connect().then(
75
- () => true,
76
- () => false
77
- ),
78
- new Promise<boolean>((resolve) => {
79
- setTimeout(() => resolve(false), CONNECT_WAIT_MS);
80
- }),
81
- ]);
82
- if (!connected || !client.isConnected) {
83
- lastConnectWaitFailedAt = Date.now();
84
- return false;
85
- }
86
- return true;
87
- };
88
-
89
47
  /** One rendered expression — what `renderRich` resolves with. */
90
48
  export interface IRenderedExpression {
91
49
  /** The readable rendering (plain text). */
92
50
  text: string;
93
51
  /**
94
52
  * The server's richtext form — template references arrive as `tag` nodes
95
- * (the IDE renders them as chips). `null` on the client-side fallback.
96
- * Convert with `richtextResponseToSlate` for `RichTextFormField`.
53
+ * (the IDE renders them as chips). Convert with `richtextResponseToSlate`.
97
54
  */
98
55
  richtext: unknown | null;
99
- /** `true` when the server `dpql/renderExpression` produced this result. */
100
- server: boolean;
101
56
  }
102
57
 
103
- /** Server-side render; `null` means "fall back to the client-side path". */
104
- const renderViaServer = async (
105
- value: IExpressionValue
106
- ): Promise<{ text: string; richtext: unknown | null } | null> => {
58
+ const renderViaServer = async (value: IExpressionValue): Promise<IRenderedExpression | null> => {
107
59
  if (methodUnsupported) return null;
108
60
  const client = getRenderClient();
109
- if (!(await waitForConnection(client))) return null;
110
61
  try {
62
+ // Waits as long as the connection does: it has its own reconnect schedule
63
+ // and rejects once it gives up, so this never outlives a dead socket.
64
+ await client.connect();
111
65
  const result = await client.customRequest<{ rendered?: string; richtext?: unknown }>(
112
66
  'dpql/renderExpression',
113
67
  // The server accepts both the bare `{exp, args}` AST and the
@@ -131,75 +85,50 @@ export const _resetRenderExpressionTransportForTests = (): void => {
131
85
  renderClient?.disconnect();
132
86
  renderClient = null;
133
87
  methodUnsupported = false;
134
- lastConnectWaitFailedAt = 0;
135
- lastServerRendering = false;
136
88
  };
137
89
 
138
90
  interface IPendingRender {
139
91
  value: IExpressionValue;
140
- expressions: IExpressionSchema[];
141
- resolvers: Array<(rendered: IRenderedExpression) => void>;
92
+ resolvers: Array<(rendered: IRenderedExpression | null) => void>;
142
93
  }
143
94
 
144
95
  export interface IUseRenderExpressionResult {
145
- /** Render an expression AST to a readable string. */
146
- render: (value: IExpressionValue, expressions?: IExpressionSchema[]) => Promise<string>;
96
+ /** The readable rendering of an expression AST, or `null` when the server cannot render. */
97
+ render: (value: IExpressionValue) => Promise<string | null>;
147
98
  /**
148
99
  * Like `render`, but also resolves the server's richtext form (template
149
- * references as `tag` nodes — chips) and which path served. For consumers
150
- * that style the rendering, e.g. the builder's Explain panel.
151
- */
152
- renderRich: (
153
- value: IExpressionValue,
154
- expressions?: IExpressionSchema[]
155
- ) => Promise<IRenderedExpression>;
156
- /**
157
- * `true` when the server `dpql/renderExpression` path served the last
158
- * render; `false` while on the client-side approximation (LSP unreachable
159
- * or the server predates the method).
100
+ * references as `tag` nodes — chips). `null` when there is nothing to render
101
+ * or the server cannot render it.
160
102
  */
161
- serverRendering: boolean;
103
+ renderRich: (value: IExpressionValue) => Promise<IRenderedExpression | null>;
162
104
  }
163
105
 
164
106
  export const useRenderExpression = (): IUseRenderExpressionResult => {
165
- const [serverRendering, setServerRendering] = useState(lastServerRendering);
166
107
  const pendingRef = useRef<IPendingRender | null>(null);
167
108
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
168
109
  const lastRunRef = useRef(0);
169
110
 
170
- useEffect(() => {
171
- serverRenderingListeners.add(setServerRendering);
172
- return () => {
173
- serverRenderingListeners.delete(setServerRendering);
111
+ useEffect(
112
+ () => () => {
174
113
  if (timerRef.current) {
175
114
  clearTimeout(timerRef.current);
176
115
  timerRef.current = null;
177
116
  }
178
- // Settle any coalesced callers with the synchronous fallback so no
179
- // promise is left hanging past unmount.
117
+ // Settle any coalesced callers so no promise is left hanging past unmount.
180
118
  const pending = pendingRef.current;
181
119
  pendingRef.current = null;
182
- pending?.resolvers.forEach((resolve) =>
183
- resolve({
184
- text: renderExpressionToText(pending.value, pending.expressions),
185
- richtext: null,
186
- server: false,
187
- })
188
- );
189
- };
190
- }, []);
120
+ pending?.resolvers.forEach((resolve) => resolve(null));
121
+ },
122
+ []
123
+ );
191
124
 
192
125
  const renderRich = useCallback(
193
- (
194
- value: IExpressionValue,
195
- expressions: IExpressionSchema[] = []
196
- ): Promise<IRenderedExpression> =>
197
- new Promise<IRenderedExpression>((resolve) => {
126
+ (value: IExpressionValue): Promise<IRenderedExpression | null> =>
127
+ new Promise<IRenderedExpression | null>((resolve) => {
198
128
  // Coalesce calls per consumer: rapid live updates collapse to the
199
129
  // newest AST; every awaiting caller gets that newest rendering.
200
130
  pendingRef.current = {
201
131
  value,
202
- expressions,
203
132
  resolvers: [...(pendingRef.current?.resolvers ?? []), resolve],
204
133
  };
205
134
 
@@ -208,20 +137,7 @@ export const useRenderExpression = (): IUseRenderExpressionResult => {
208
137
  pendingRef.current = null;
209
138
  if (!job) return;
210
139
  lastRunRef.current = Date.now();
211
- let outcome: IRenderedExpression;
212
- if (!job.value?.exp) {
213
- outcome = { text: '', richtext: null, server: false };
214
- } else {
215
- const served = await renderViaServer(job.value);
216
- setLastServerRendering(served !== null);
217
- outcome = served
218
- ? { ...served, server: true }
219
- : {
220
- text: renderExpressionToText(job.value, job.expressions),
221
- richtext: null,
222
- server: false,
223
- };
224
- }
140
+ const outcome = job.value?.exp ? await renderViaServer(job.value) : null;
225
141
  job.resolvers.forEach((r) => r(outcome));
226
142
  };
227
143
 
@@ -240,10 +156,10 @@ export const useRenderExpression = (): IUseRenderExpressionResult => {
240
156
  );
241
157
 
242
158
  const render = useCallback(
243
- (value: IExpressionValue, expressions: IExpressionSchema[] = []): Promise<string> =>
244
- renderRich(value, expressions).then((r) => r.text),
159
+ (value: IExpressionValue): Promise<string | null> =>
160
+ renderRich(value).then((r) => r?.text ?? null),
245
161
  [renderRich]
246
162
  );
247
163
 
248
- return { render, renderRich, serverRendering };
164
+ return { render, renderRich };
249
165
  };
@@ -2,7 +2,6 @@ import {
2
2
  IReqoreLabelProps,
3
3
  ReqoreCheckbox,
4
4
  ReqoreControlGroup,
5
- ReqoreSkeleton,
6
5
  ReqoreSpan,
7
6
  } from '@qoretechnologies/reqore';
8
7
  import { IWithReqoreSize } from '@qoretechnologies/reqore/dist/types/global';
@@ -38,6 +37,7 @@ import { IDataSchemaDefinition } from './schema-definition/types';
38
37
  // Direct import — circular dep (FormEngine → TemplateField → FormField → FormEngine) is safe
39
38
  // because modules are all loaded before any component renders
40
39
  import { FormEngine } from '../engine/FormEngine';
40
+ import { FormFieldsSkeleton } from '../engine/FormFieldsSkeleton';
41
41
 
42
42
  /** Soft Qore types (softint, softstring, …) render as their concrete base —
43
43
  * the soft/hard distinction is value-level, not UI. */
@@ -345,7 +345,8 @@ export const FormField = <T extends TFormFieldType>({
345
345
 
346
346
  case 'hash': {
347
347
  if (argSchemaLoading) {
348
- return <ReqoreSkeleton height='80px' />;
348
+ // A hash renders a nested FORM, so it waits in the shape of one.
349
+ return <FormFieldsSkeleton rows={2} />;
349
350
  }
350
351
  if (finalArgSchema) {
351
352
  return (
@@ -40,13 +40,23 @@ const DISALLOW_COMPACT_AO_TYPES = ['connection'];
40
40
  * Whether a creatable field's value is carried by the chip picker rather than
41
41
  * by a raw editor with a suggestion list beside it.
42
42
  *
43
- * `string` only, and only with something to suggest. Every other shape a
43
+ * String-valued only, and only with something to suggest. Every other shape a
44
44
  * creatable field can declare either does not fit a one-line chip (a document
45
45
  * — `long-string`, `binary`, `data`; a hash; a list) or would come back out of
46
46
  * one as a string (a number, a date), and a field with nothing to offer is
47
47
  * better served by the editor it has always had. Those keep today's rendering:
48
48
  * the raw editor, with the saved-and-suggested picker under it.
49
49
  *
50
+ * `select-string` counts, and leaving it out was the bug. A picker field
51
+ * declares its storage type (`*string`) and its `ui_type` (`select-string`),
52
+ * and by the time a schema reaches here the two have been flattened to one:
53
+ * `type` arrives as `select-string`. So this returned false for every such
54
+ * field and NEITHER renderer stood down — the picker drew the value, and the
55
+ * saved-and-suggested list drew the same candidates again underneath it. A
56
+ * test's Service and Method fields showed 183 and 7 items twice over, in two
57
+ * controls that set one value. A select-string's value IS a string and fits the
58
+ * chip, which is the only thing this predicate is really asking.
59
+ *
50
60
  * Both the renderer of the picker (`FieldAllowedValues`) and the renderer of
51
61
  * the raw editor (`AutoFormField`, `FormField`) ask this, so that exactly one
52
62
  * of them draws the value.
@@ -55,7 +65,8 @@ export const rendersCreatableValueSelect = (
55
65
  type?: string,
56
66
  allowCreation?: boolean,
57
67
  items?: unknown[]
58
- ): boolean => !!allowCreation && type === 'string' && count(items) > 0;
68
+ ): boolean =>
69
+ !!allowCreation && (type === 'string' || type === 'select-string') && count(items) > 0;
59
70
 
60
71
  export const FieldAllowedValuesCheckGroup = memo(
61
72
  ({
@@ -54,22 +54,24 @@ const waitForText = async (text: string | RegExp) => {
54
54
 
55
55
  // --- reqraft behavioral stories (kept from the field-migration batch) -------
56
56
 
57
- /** Auto type, no value — the picker is shown and the field area prompts for a type. */
57
+ /** Untyped, no value — the field opens on an editor to type into, never on a type question. */
58
58
  export const Empty: Story = {
59
59
  args: {},
60
60
  parameters: {
61
61
  docs: {
62
62
  description: {
63
63
  story:
64
- 'Renders AutoFormField with no value or defaultType — the type picker is shown and the field area prompts the operator to pick a data type.',
64
+ 'Renders AutoFormField with no value or defaultType. The field opens on a text editor to type into; it does not show a type picker or ask for a data type.',
65
65
  },
66
66
  },
67
67
  },
68
68
  async play({ canvasElement }) {
69
69
  const canvas = within(canvasElement);
70
- // The IDE auto resolves its type in a mount effect — assert asynchronously.
71
- await expect(await canvas.findByText('Please select data type')).toBeInTheDocument();
72
- // No concrete field is rendered yet.
70
+ // The type resolves in a mount effect — assert asynchronously.
71
+ await waitFor(() => expect(canvasElement.querySelector('textarea')).toBeInTheDocument(), {
72
+ timeout: 5000,
73
+ });
74
+ await expect(canvas.queryByText('Please select data type')).not.toBeInTheDocument();
73
75
  await expect(canvasElement.querySelector('input[type="number"]')).not.toBeInTheDocument();
74
76
  },
75
77
  };
@@ -179,7 +181,7 @@ export const Nullable: Story = {
179
181
  },
180
182
  };
181
183
 
182
- /** The picker can be restricted to a subset of types via `allowedTypes`. */
184
+ /** A field declaring several allowed types still picks between them. */
183
185
  export const AllowedTypesSubset: Story = {
184
186
  args: {
185
187
  allowedTypes: [
@@ -191,14 +193,14 @@ export const AllowedTypesSubset: Story = {
191
193
  docs: {
192
194
  description: {
193
195
  story:
194
- 'Renders AutoFormField with allowedTypes restricted to Integer and Text — the picker badge shows 2 available types instead of the full 14.',
196
+ 'Renders AutoFormField with allowedTypes restricted to Integer and Text. The type picker offers those 2 types (its badge reads 2), and the field can be typed into meanwhile without being asked for a data type.',
195
197
  },
196
198
  },
197
199
  },
198
- async play() {
200
+ async play({ canvasElement }) {
199
201
  // The picker badge reflects the restricted list — 2 instead of 14.
200
- await waitForText('Please select data type');
201
202
  await waitForText('2');
203
+ await expect(within(canvasElement).queryByText('Please select data type')).not.toBeInTheDocument();
202
204
  },
203
205
  };
204
206
 
@@ -206,31 +208,35 @@ export const AllowedTypesSubset: Story = {
206
208
  export const NoSoftTypes: Story = {
207
209
  args: {
208
210
  noSoft: true,
211
+ /* The picker belongs to a value with a type: an untyped field that is
212
+ empty is typed into, and one holding text has resolved to `string`. */
213
+ defaultType: 'auto',
214
+ value: 'some text',
209
215
  },
210
216
  parameters: {
211
217
  docs: {
212
218
  description: {
213
219
  story:
214
- 'Renders AutoFormField with noSoft enabled — the soft* type variants are hidden and the picker badge reads 9 instead of 14.',
220
+ 'Renders an untyped AutoFormField holding a text value with noSoft enabled. The picker for the type the value resolved to leaves out the soft* variants, so its badge reads 9 instead of 14.',
215
221
  },
216
222
  },
217
223
  },
218
- async play() {
219
- await waitForText('Please select data type');
224
+ async play({ canvasElement }) {
220
225
  await waitForText('9');
226
+ await expect(within(canvasElement).queryByText('Please select data type')).not.toBeInTheDocument();
221
227
  },
222
228
  };
223
229
 
224
230
  /**
225
231
  * The whole point of the migration: a `FormEngine` schema with a field typed
226
- * `auto` renders the picker for free, via FormEngine → TemplateField → auto.
232
+ * `auto` renders the untyped editor for free, via FormEngine → TemplateField → auto.
227
233
  */
228
234
  export const ViaFormEngine: Story = {
229
235
  parameters: {
230
236
  docs: {
231
237
  description: {
232
238
  story:
233
- 'Renders a FormEngine schema whose single option is typed as auto — the engine flows through TemplateField and renders the AutoFormField picker without any extra wiring.',
239
+ 'Renders a FormEngine schema whose single option is typed as auto. The engine flows through TemplateField to the untyped editor without any extra wiring: an editor to type into, with no data-type question.',
234
240
  },
235
241
  },
236
242
  },
@@ -255,15 +261,76 @@ export const ViaFormEngine: Story = {
255
261
  async play({ canvasElement }) {
256
262
  const canvas = within(canvasElement);
257
263
  // Generous timeout: findByText defaults to 1s, which flakes under CI load
258
- // while the engine boots the auto field's type picker (the rest of the suite
259
- // waits ~10s).
264
+ // while the engine boots the auto field (the rest of the suite waits ~10s).
260
265
  await expect(
261
266
  await canvas.findByText('My Auto Field', undefined, { timeout: 10000 })
262
267
  ).toBeInTheDocument();
263
- // The auto field renders its type picker inside the engine-driven form.
268
+ // The untyped field opens on an editor inside the engine-driven form, and
269
+ // never asks for a data type.
270
+ await waitFor(() => expect(canvasElement.querySelector('textarea')).toBeInTheDocument(), {
271
+ timeout: 10000,
272
+ });
273
+ await expect(canvas.queryByText('Please select data type')).not.toBeInTheDocument();
274
+ },
275
+ };
276
+
277
+ /**
278
+ * The ⋮ on that same field holds one group — "Set Custom Value" and the data
279
+ * types under it — and opens it, rather than asking for a click to reach the
280
+ * only thing on offer.
281
+ */
282
+ export const ViaFormEngineMenuOpensItsOnlyGroup: Story = {
283
+ parameters: {
284
+ docs: {
285
+ description: {
286
+ story:
287
+ 'The untyped field\'s ⋮ holds nothing but the "Set Custom Value" section, so that section is already open: the data types are one click from the menu, not two. Where a menu holds more than one group the sections stay shut, because there the click is the choice.',
288
+ },
289
+ },
290
+ },
291
+ render: () => {
292
+ const [val, setVal] = useState<any>({});
293
+ return (
294
+ <FormEngine
295
+ name='autoFieldMenuDemo'
296
+ options={{
297
+ myAutoField: {
298
+ type: 'auto',
299
+ ui_type: 'auto',
300
+ display_name: 'My Auto Field',
301
+ preselected: true,
302
+ },
303
+ }}
304
+ value={val}
305
+ onChange={(_n, v) => setVal(v)}
306
+ />
307
+ );
308
+ },
309
+ async play({ canvasElement }) {
310
+ const canvas = within(canvasElement);
264
311
  await expect(
265
- await canvas.findByText('Please select data type', undefined, { timeout: 10000 })
312
+ await canvas.findByText('My Auto Field', undefined, { timeout: 10000 })
266
313
  ).toBeInTheDocument();
314
+
315
+ const menu = await waitFor(
316
+ () => {
317
+ const el = canvasElement.querySelector('.template-more');
318
+ expect(el).toBeTruthy();
319
+ return el as HTMLElement;
320
+ },
321
+ { timeout: 10000 }
322
+ );
323
+ await userEvent.click(menu);
324
+
325
+ // The section still names its rows...
326
+ await waitFor(
327
+ () => expect(document.body.textContent).toContain('Set Custom Value'),
328
+ { timeout: 10000 }
329
+ );
330
+ // ...and the rows are there to click, with no header click in between.
331
+ await waitFor(() => expect(document.body.textContent).toContain('Ordered key-value pair'), {
332
+ timeout: 10000,
333
+ });
267
334
  },
268
335
  };
269
336