@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
@@ -0,0 +1,92 @@
1
+ // Copyright 2026 Qore Technologies, s.r.o.
2
+ // A string holding template references, as the nodes a rich-text editor draws.
3
+ //
4
+ // A field whose value is a plain string can still hold references —
5
+ // `Dear $local:name` — and those should read as the names they were chosen by,
6
+ // not as their spelling. These convert between the stored string and the
7
+ // editor's paragraphs, where each reference is a chip, so the editor can draw
8
+ // chips while the field keeps storing exactly the string it always stored.
9
+ import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
10
+ import { getTagLabel } from '../components/dpqlEditor/dpqlHelpers';
11
+ import { describeTemplateReference, TEMPLATE_TOKEN_SOURCE } from './templates';
12
+
13
+ /* Stated here rather than borrowed from Reqore's editor props: those resolve
14
+ to Reqore's `CustomElement`, which it does not export, so the published
15
+ declarations could not name this module's types (TS4023 on `yarn build`). */
16
+
17
+ /** A run of text. */
18
+ export interface ITemplateTextLeaf {
19
+ text: string;
20
+ }
21
+
22
+ /** A template reference, drawn as a chip. */
23
+ export interface ITemplateTextTag {
24
+ type: 'tag';
25
+ value: string;
26
+ label: string;
27
+ metadata?: unknown;
28
+ children: ITemplateTextLeaf[];
29
+ }
30
+
31
+ /** One line of the string. */
32
+ export interface ITemplateTextParagraph {
33
+ type: 'paragraph';
34
+ children: Array<ITemplateTextLeaf | ITemplateTextTag>;
35
+ }
36
+
37
+ /**
38
+ * What a reference chip says: the name the catalogue gives the reference, else
39
+ * the reference as the DPQL Text view labels it (`local: name`), so the same
40
+ * reference reads alike in the Visual builder and the Text view.
41
+ */
42
+ export const templateChipLabel = (templates: IReqoreFormTemplates | undefined, value: string): string => {
43
+ const { label, item } = describeTemplateReference(templates, value);
44
+ return item ? label : getTagLabel(value);
45
+ };
46
+
47
+ /**
48
+ * `text` as paragraphs, one per line, with each template reference a chip.
49
+ *
50
+ * A chip is an inline VOID and holds no text of its own, so a cursor can only
51
+ * sit in the text nodes around it; one is kept on each side even when empty.
52
+ * The chip's label is `templateChipLabel`.
53
+ */
54
+ export const templateTextToNodes = (
55
+ text: string,
56
+ templates?: IReqoreFormTemplates
57
+ ): ITemplateTextParagraph[] =>
58
+ (text ?? '').split('\n').map((line) => {
59
+ const children: ITemplateTextParagraph['children'] = [];
60
+ // A fresh expression per line: a shared /g instance carries `lastIndex`.
61
+ const pattern = new RegExp(TEMPLATE_TOKEN_SOURCE, 'g');
62
+ let last = 0;
63
+ let match: RegExpExecArray | null;
64
+ while ((match = pattern.exec(line)) !== null) {
65
+ const { item } = describeTemplateReference(templates, match[0]);
66
+ children.push({ text: line.slice(last, match.index) });
67
+ children.push({
68
+ type: 'tag',
69
+ value: match[0],
70
+ label: templateChipLabel(templates, match[0]),
71
+ metadata: item?.metadata,
72
+ children: [{ text: '' }],
73
+ });
74
+ last = match.index + match[0].length;
75
+ }
76
+ children.push({ text: line.slice(last) });
77
+ return { type: 'paragraph' as const, children };
78
+ });
79
+
80
+ /** The string the paragraphs stand for: each chip written as its reference. */
81
+ export const templateNodesToText = (nodes?: ReadonlyArray<{ children?: unknown[] }>): string =>
82
+ (nodes ?? [])
83
+ .map((paragraph: any) =>
84
+ (paragraph?.children ?? [])
85
+ .map((node: any) =>
86
+ typeof node?.text === 'string' ? node.text
87
+ : node?.type === 'tag' ? String(node.value ?? '')
88
+ : ''
89
+ )
90
+ .join('')
91
+ )
92
+ .join('\n');
@@ -1,4 +1,5 @@
1
1
  import { modalStore } from '@qoretechnologies/reqore';
2
+ import { TReqoreTooltipProp } from '@qoretechnologies/reqore/dist/types/global';
2
3
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
3
4
  import {
4
5
  TReqoreDropdownItem,
@@ -20,6 +21,24 @@ import { areQorusTypesCompatible } from './expressions';
20
21
  */
21
22
  export const TEMPLATE_EXAMPLE_PREVIEW_LENGTH = 150;
22
23
 
24
+ /**
25
+ * How template picker entries are ordered: by the label a reader actually sees.
26
+ *
27
+ * The server sends groups and their items in whatever order it assembled them —
28
+ * for the test vocabulary that is step order, for an app's actions it is the
29
+ * connector's own. Neither is an order a reader can predict, and a picker
30
+ * holding 29 items is navigated by looking for a NAME.
31
+ *
32
+ * `localeCompare` with `numeric` so `item2` sorts before `item10`, and
33
+ * `sensitivity: 'base'` so case and accents do not split otherwise-adjacent
34
+ * neighbours.
35
+ */
36
+ const byLabel = (a: { label?: unknown }, b: { label?: unknown }): number =>
37
+ String(a?.label ?? '').localeCompare(String(b?.label ?? ''), undefined, {
38
+ numeric: true,
39
+ sensitivity: 'base',
40
+ });
41
+
23
42
  /**
24
43
  * Shared template string utilities.
25
44
  * Used by both validations and TemplateField.
@@ -258,43 +277,34 @@ export const describeTemplateReference = (
258
277
  return resolved.item ? resolved : { label: getTemplateReferencePath(value) };
259
278
  };
260
279
 
261
- const EMBEDDED_TEMPLATE_TOKEN = new RegExp(TEMPLATE_TOKEN_SOURCE, 'g');
262
-
263
- export type TTemplateTextSegment =
264
- | { kind: 'text'; text: string }
265
- | { kind: 'token'; text: string };
266
-
267
280
  /**
268
- * Splits prose that embeds template tokens into its literal and token parts —
269
- * `trim("$data:{…}")` becomes `trim("`, the token, `")`. Used to chip the
270
- * references inside a rendered expression instead of printing them raw.
281
+ * What a chosen template's hover should say.
282
+ *
283
+ * The reference itself — `$._case.title`, `$data:{dc_ai_reply.choices[0]…}` —
284
+ * is an implementation detail of the grammar, and it was what every template
285
+ * chip put in its tooltip. An author hovering a chip wants to know WHAT VALUE
286
+ * this is, and the catalogue already carries that: the entry's `description`
287
+ * is the prose the picker lists it under, written from the schema that declares
288
+ * it, with its type appended.
289
+ *
290
+ * So the hover reads as a small card — the value's name as the title, its
291
+ * description as the body — and the path stays where it is genuinely needed:
292
+ * the field's own help, which teaches the grammar with a worked example, and
293
+ * the picker, for the walks no list can hold.
294
+ *
295
+ * Falls back to the resolved label (itself the stripped path when the catalogue
296
+ * explains nothing), because a surface with no catalogue must still say what
297
+ * the value is rather than nothing at all.
271
298
  */
272
- export const splitTemplateTokens = (text?: string): TTemplateTextSegment[] => {
273
- if (!text) {
274
- return [];
275
- }
276
-
277
- const segments: TTemplateTextSegment[] = [];
278
- let lastIndex = 0;
279
-
280
- // A fresh regex per call: a shared /g instance carries `lastIndex` between
281
- // calls and would skip tokens on the second string it is given.
282
- const pattern = new RegExp(EMBEDDED_TEMPLATE_TOKEN.source, 'g');
283
- let match: RegExpExecArray | null;
284
-
285
- while ((match = pattern.exec(text)) !== null) {
286
- if (match.index > lastIndex) {
287
- segments.push({ kind: 'text', text: text.slice(lastIndex, match.index) });
288
- }
289
- segments.push({ kind: 'token', text: match[0] });
290
- lastIndex = match.index + match[0].length;
291
- }
292
-
293
- if (lastIndex < text.length) {
294
- segments.push({ kind: 'text', text: text.slice(lastIndex) });
295
- }
296
-
297
- return segments;
299
+ export const templateTooltip = (
300
+ templates: IReqoreFormTemplates | undefined,
301
+ value: string
302
+ ): TReqoreTooltipProp => {
303
+ const { label, item } = describeTemplateReference(templates, value);
304
+ const description = (item as { description?: unknown } | undefined)?.description;
305
+ return typeof description === 'string' && description.trim() ?
306
+ { title: label, content: description, maxWidth: '360px' }
307
+ : label || value;
298
308
  };
299
309
 
300
310
  // Ported verbatim from qorus-ide `helpers/functions.tsx` (FIELD_STACK_REPORT
@@ -506,7 +516,7 @@ export const buildTemplates = (
506
516
 
507
517
  return item;
508
518
  }
509
- );
519
+ ).sort(byLabel);
510
520
 
511
521
  return {
512
522
  items: Object.values(templates).map(
@@ -544,6 +554,6 @@ export const buildTemplates = (
544
554
  data_role
545
555
  ),
546
556
  })
547
- ),
557
+ ).sort(byLabel),
548
558
  } as IReqoreFormTemplates;
549
559
  };
@@ -1064,14 +1064,39 @@ export const _validateField = (
1064
1064
  }
1065
1065
  }
1066
1066
 
1067
- if (parsedData) {
1068
- return withContext(
1069
- validateFieldWithResult(getTypeFromValue(parsedData), value),
1070
- 'Auto-detected type is invalid'
1071
- );
1067
+ /* A FALSY value is not an ABSENT one. `null`, `0`, `false` and `""` are
1068
+ all values an author may hold in an `auto`/`any` field — a test
1069
+ assertion expecting a count of zero, a flag that is off, or a blank
1070
+ string — and the truthiness gate that used to stand here refused every
1071
+ one of them as *"Value is empty"*. It surfaced on an assertion's
1072
+ Expected Value holding `null`: opening it reported *"Value for argument
1073
+ 1 ("any") is invalid: Value is empty"* on the value the author had
1074
+ deliberately chosen. Only `undefined` is actually empty.
1075
+
1076
+ `null` and `""` are answered here rather than delegated, for two
1077
+ different reasons: `getTypeFromValue(null)` answers `auto`, which would
1078
+ re-enter this very branch, and the `string` validator rejects `""`
1079
+ because that is the rule for a REQUIRED text field — a different
1080
+ question from whether an untyped field may hold a blank string. */
1081
+ if (parsedData === undefined) {
1082
+ return invalidResult('Value is empty');
1083
+ }
1084
+
1085
+ if (parsedData === null || parsedData === '') {
1086
+ return validResult();
1072
1087
  }
1073
1088
 
1074
- return invalidResult('Value is empty');
1089
+ /* Validate what was PARSED, against the type detected from the same
1090
+ parse. Validating the raw `value` instead made the two disagree
1091
+ whenever parsing changed the shape: typing `1` into an `auto` field
1092
+ parsed to the number 1, detected `int`, and then validated the STRING
1093
+ "1" against it — "Value must be an integer", on a value that is one.
1094
+ The author saw their field go invalid on the first character they
1095
+ typed. */
1096
+ return withContext(
1097
+ validateFieldWithResult(getTypeFromValue(parsedData), parsedData),
1098
+ 'Auto-detected type is invalid'
1099
+ );
1075
1100
  }
1076
1101
  case 'processor': {
1077
1102
  if (!value || !value['processor-input-type'] || !value['processor-output-type']) {
@@ -1512,6 +1537,31 @@ export const _validateField = (
1512
1537
  const argDefinition = expressionDefinition.varargs
1513
1538
  ? expressionDefinition.args[0]
1514
1539
  : expressionDefinition.args[index];
1540
+ /* The argument's name for the message.
1541
+
1542
+ `display_name` is what a catalogue MAY carry; the served one carries
1543
+ `name`, so every message about an argument read "argument 1
1544
+ ("undefined") is invalid" and named a field that does not exist. */
1545
+ const argLabel =
1546
+ argDefinition?.display_name ?? argDefinition?.name ?? `argument ${index + 1}`;
1547
+
1548
+ /* An explicit null is a VALUE, and DPQL says so: `null` is a literal
1549
+ that parses, serializes and round-trips like any other.
1550
+
1551
+ Treated as a missing one, it made "this returns no value"
1552
+ unsayable. Typing `null` into the text editor produces the literal
1553
+ expression `{exp: "value", args: [null]}`; the visual view then
1554
+ called it invalid and refused to render a summary, and a raw null
1555
+ argument crashed the validator outright on `argValue.type`. Reported
1556
+ from the live IDE by an author with no other way to assert that a
1557
+ service method returns nothing.
1558
+
1559
+ Only an EXPLICIT null counts. An argument that is simply absent, or
1560
+ an envelope holding `undefined`, is still missing — which is the
1561
+ distinction the author is making when they write it. */
1562
+ if (argValue === null || (isObject(argValue) && (argValue as any).value === null)) {
1563
+ continue;
1564
+ }
1515
1565
 
1516
1566
  if (!argValue?.value && !argDefinition?.required) {
1517
1567
  continue;
@@ -1526,14 +1576,14 @@ export const _validateField = (
1526
1576
  if (!result.isValid) {
1527
1577
  return withContext(
1528
1578
  result,
1529
- `Sub-expression for argument ${index + 1} ("${argDefinition?.display_name}") is invalid`
1579
+ `Sub-expression for argument ${index + 1} ("${argLabel}") is invalid`
1530
1580
  );
1531
1581
  }
1532
1582
 
1533
1583
  continue;
1534
1584
  }
1535
1585
 
1536
- const result = validateFieldWithResult(argValue.type, argValue.value, {
1586
+ const result = validateFieldWithResult(argValue?.type, argValue?.value, {
1537
1587
  expressions,
1538
1588
  allowed_values: argDefinition?.allowed_values,
1539
1589
  element_allowed_values: argDefinition?.element_allowed_values,
@@ -1543,7 +1593,7 @@ export const _validateField = (
1543
1593
  if (!result.isValid) {
1544
1594
  return withContext(
1545
1595
  result,
1546
- `Value for argument ${index + 1} ("${argDefinition?.display_name}") is invalid`
1596
+ `Value for argument ${index + 1} ("${argLabel}") is invalid`
1547
1597
  );
1548
1598
  }
1549
1599
  }
@@ -1,6 +1,7 @@
1
1
  import { ReqoreSpinner, ReqoreTree } from '@qoretechnologies/reqore';
2
2
  import { StoryObj } from '@storybook/react-vite';
3
3
  import { testsWaitForText } from '../../../__tests__/utils';
4
+ import { storyApiUrl } from '../../stories/storyNetwork';
4
5
  import { StoryMeta } from '../../types';
5
6
  import { useFetch } from './useFetch';
6
7
 
@@ -44,7 +45,7 @@ export const Get: Story = {
44
45
  mockData: [
45
46
  {
46
47
  // An array of mock objects which will add in every story
47
- url: 'https://hq.qoretechnologies.com:8092/api/latest/public/info',
48
+ url: storyApiUrl('public/info'),
48
49
  method: 'GET',
49
50
  status: 200,
50
51
  delay: 200,
@@ -81,7 +82,7 @@ export const Put: Story = {
81
82
  mockData: [
82
83
  {
83
84
  // An array of mock objects which will add in every story
84
- url: 'https://hq.qoretechnologies.com:8092/api/latest/public/info',
85
+ url: storyApiUrl('public/info'),
85
86
  method: 'PUT',
86
87
  status: 200,
87
88
  response: {
@@ -109,7 +110,7 @@ export const Post: Story = {
109
110
  mockData: [
110
111
  {
111
112
  // An array of mock objects which will add in every story
112
- url: 'https://hq.qoretechnologies.com:8092/api/latest/public/info',
113
+ url: storyApiUrl('public/info'),
113
114
  method: 'POST',
114
115
  status: 200,
115
116
  response: {
@@ -137,7 +138,7 @@ export const Del: Story = {
137
138
  mockData: [
138
139
  {
139
140
  // An array of mock objects which will add in every story
140
- url: 'https://hq.qoretechnologies.com:8092/api/latest/public/info',
141
+ url: storyApiUrl('public/info'),
141
142
  method: 'DELETE',
142
143
  status: 200,
143
144
  response: {
@@ -165,7 +166,7 @@ export const Error409: Story = {
165
166
  mockData: [
166
167
  {
167
168
  // An array of mock objects which will add in every story
168
- url: 'https://hq.qoretechnologies.com:8092/api/latest/public/info',
169
+ url: storyApiUrl('public/info'),
169
170
  method: 'POST',
170
171
  status: 409,
171
172
  response: {
@@ -1,8 +1,10 @@
1
+ import { QueryClient, useQueryClient } from '@tanstack/react-query';
1
2
  import { useCallback, useMemo, useState } from 'react';
2
3
  import { useEffectOnce } from 'react-use';
3
4
  import { useContextSelector } from 'use-context-selector';
4
5
  import { FetchContext } from '../../contexts/FetchContext';
5
- import { IReqraftQueryConfig } from '../../utils/fetch';
6
+ import { ReqraftQueryClient } from '../../providers/ReqraftProvider';
7
+ import { IReqraftQueryConfig, reqraftCacheKey } from '../../utils/fetch';
6
8
 
7
9
  export interface IReqraftUseFetch<T> {
8
10
  data: T | undefined;
@@ -41,8 +43,40 @@ export function useFetch<T>({
41
43
  }
42
44
  });
43
45
 
44
- const [loading, setLoading] = useState(loadOnMount);
45
- const [data, setData] = useState<T | undefined>(defaultData);
46
+ /* An answer we ALREADY HAVE is not a loading state.
47
+ `loading` started at `loadOnMount` unconditionally, so every consumer that
48
+ mounted reported "loading" for at least one async turn even when the
49
+ response was sitting in the cache — and `FormEngine` renders its
50
+ `options-loading-skeleton` whenever any of its sources says that. So each
51
+ form appearing on a page flashed a skeleton whether or not anything was
52
+ actually fetched, which is what the repeated skeletons on an IDE page are:
53
+ measured on a fully warm SECOND visit to one page, three skeleton waves
54
+ against two requests, the last wave with no request anywhere near it.
55
+ Seeded from the query cache instead, so a resource already in hand renders
56
+ immediately. */
57
+ /* The provider is OPTIONAL here. `useQueryClient` THROWS when no
58
+ `QueryClientProvider` is above it, and plenty of consumers mount a form
59
+ with only a `FetchContext` — requiring one turned a rendering optimisation
60
+ into a hard dependency and took 131 tests down with it. `useContext` runs
61
+ before that throw, so hook order is unaffected and catching is safe. The
62
+ fallback is the same client `query` itself defaults to, so a cache hit is
63
+ found either way. */
64
+ let queryClient: QueryClient;
65
+ try {
66
+ queryClient = useQueryClient();
67
+ } catch {
68
+ queryClient = ReqraftQueryClient;
69
+ }
70
+
71
+ const cached = queryClient.getQueryData([
72
+ reqraftCacheKey({ url, method, body, noApiPrefix }),
73
+ ]) as { data?: T; ok?: boolean } | undefined;
74
+ const hasCachedAnswer = !!cached && cached.ok !== false && cached.data !== undefined;
75
+
76
+ const [loading, setLoading] = useState(!!loadOnMount && !hasCachedAnswer);
77
+ const [data, setData] = useState<T | undefined>(
78
+ hasCachedAnswer ? (cached?.data as T) : defaultData
79
+ );
46
80
  const [response, setResponse] = useState<Response | undefined>();
47
81
  const [error, setError] = useState<Error | undefined>();
48
82
  const [errorData, setErrorData] = useState<any>();
@@ -51,13 +85,23 @@ export function useFetch<T>({
51
85
  async ({
52
86
  body: customBody,
53
87
  mergeBodies,
54
- }: { body?: Record<string | number, any>; mergeBodies?: boolean } = {}) => {
55
- setLoading(true);
88
+ silent,
89
+ }: {
90
+ body?: Record<string | number, any>;
91
+ mergeBodies?: boolean;
92
+ /** Revalidate without announcing a loading state — see the note above. */
93
+ silent?: boolean;
94
+ } = {}) => {
95
+ if (!silent) {
96
+ setLoading(true);
97
+ }
56
98
 
57
99
  const _body = mergeBodies ? { ...body, ...customBody } : customBody || body;
58
100
  const response = await query<T>({ url, body: _body, cache, noApiPrefix });
59
101
 
60
- setLoading(false);
102
+ if (!silent) {
103
+ setLoading(false);
104
+ }
61
105
 
62
106
  if (response.ok) {
63
107
  setError(undefined);
@@ -75,7 +119,9 @@ export function useFetch<T>({
75
119
 
76
120
  useEffectOnce(() => {
77
121
  if (loadOnMount) {
78
- load();
122
+ // Still revalidates when the entry is cached — `query` honours its own
123
+ // staleness — but silently, so a cached answer never flashes a skeleton.
124
+ load({ silent: hasCachedAnswer });
79
125
  }
80
126
  });
81
127
 
@@ -4,6 +4,7 @@ import { useRef } from 'react';
4
4
  import { fireEvent, within } from 'storybook/test';
5
5
  import { storiesStorageMock } from '../../../__tests__/ mock';
6
6
  import { testsWaitForText } from '../../../__tests__/utils';
7
+ import { storyApiUrl } from '../../stories/storyNetwork';
7
8
  import { StoryMeta } from '../../types';
8
9
  import { useReqraftStorage } from './useStorage';
9
10
 
@@ -44,7 +45,7 @@ export const DefaultValue: Story = {
44
45
  },
45
46
  mockData: [
46
47
  {
47
- url: 'https://hq.qoretechnologies.com:8092/api/latest/users?action=current',
48
+ url: storyApiUrl('users?action=current'),
48
49
  method: 'GET',
49
50
  status: 200,
50
51
  response: {},
@@ -13,6 +13,39 @@ export interface IUseTemplates {
13
13
 
14
14
  export const TemplatesCache = new Map<string, IReqoreFormTemplates>();
15
15
 
16
+ /**
17
+ * Hand this hook a `getContextData` payload the host has already fetched.
18
+ *
19
+ * The host and this library keep SEPARATE caches over one API, so a page that
20
+ * has already paid for the context data pays again here — and, worse, waits for
21
+ * it: `FormEngine` will not render a field before its templates exist, because
22
+ * an any-typed field with no templates falls back to the type picker and asks
23
+ * the author to choose `string`/`int`/`hash` for a value they already captured.
24
+ *
25
+ * Measured on the live alert rule: three form sections were placeholders from
26
+ * 1459ms to 2039ms and, when they cleared, 193 elements that had been on screen
27
+ * for ~590ms dropped 67px, because the placeholder is not the height of the
28
+ * form it stands in for. Seeded, the form has its templates on its FIRST render
29
+ * and there is no placeholder to swap out at all.
30
+ *
31
+ * Same shape as `seedQueryCache` for the type catalogue, and deliberately
32
+ * first-write-wins: a real fetch that has already answered is not overwritten.
33
+ */
34
+ export const seedTemplatesCache = (
35
+ interfaceContext: string,
36
+ payload: ITemplatesPayload
37
+ ): void => {
38
+ if (!interfaceContext || TemplatesCache.has(interfaceContext)) {
39
+ return;
40
+ }
41
+
42
+ const templates = buildTemplates(payload);
43
+
44
+ if (templates) {
45
+ TemplatesCache.set(interfaceContext, templates);
46
+ }
47
+ };
48
+
16
49
  /**
17
50
  * Templates for form fields. Ported from qorus-ide `useTemplates` with one
18
51
  * deliberate difference: fetching `system/getContextData` is **opt-in** via
@@ -29,6 +62,14 @@ export const useTemplates = (
29
62
 
30
63
  const globalTemplates = useAsyncRetry(async () => {
31
64
  if (shouldFetch) {
65
+ /* Already answered — by a previous mount or by the host seeding it. The
66
+ cache was consulted for `loading` and for the value but not here, so a
67
+ seeded page still issued the request it had been handed the answer to. */
68
+ const cached = TemplatesCache.get(interfaceContext!);
69
+ if (cached) {
70
+ return cached;
71
+ }
72
+
32
73
  const serverTemplates = await query<ITemplatesPayload>({
33
74
  url: `system/getContextData?interface_context=${interfaceContext}`,
34
75
  method: 'PUT',
@@ -6,6 +6,7 @@ import { Server } from 'mock-socket';
6
6
  import { useEffect, useState } from 'react';
7
7
  import { useMount } from 'react-use';
8
8
  import { sleep, testsClickButton, testsWaitForText } from '../../../__tests__/utils';
9
+ import { storySocketUrl } from '../../stories/storyNetwork';
9
10
  import { StoryMeta } from '../../types';
10
11
  import { ReqraftWebSocketsManager } from '../../utils/websocket';
11
12
  import { IUseReqraftWebSocketOptions, useReqraftWebSocket } from './useWebSocket';
@@ -40,7 +41,7 @@ const CompWithHook = (args: IUseReqraftWebSocketOptions) => {
40
41
  const meta = {
41
42
  title: 'Hooks/useWebSocket',
42
43
  async beforeEach({ parameters }: { parameters: Record<string, any> }) {
43
- const url = `wss://hq.qoretechnologies.com:8092/log-test?token=${process.env.REACT_APP_QORUS_TOKEN}`;
44
+ const url = storySocketUrl('log-test');
44
45
  let server = new Server(url);
45
46
  let killTimeout: NodeJS.Timeout;
46
47
  /** Set while the server is dead: every connection that lands is closed. */
package/src/index.tsx CHANGED
@@ -36,7 +36,20 @@ export type {
36
36
  ICurrentUser,
37
37
  ICurrentUserStore,
38
38
  } from './stores/currentUser/currentUser';
39
- export { fetchConfig, query } from './utils/fetch';
39
+ export {
40
+ fetchConfig,
41
+ query,
42
+ queryRaw,
43
+ reqraftApiPath,
44
+ reqraftCacheKey,
45
+ setupFetch,
46
+ } from './utils/fetch';
47
+ export type {
48
+ IReqraftFetchConfig,
49
+ IReqraftFetchResponse,
50
+ IReqraftQueryConfig,
51
+ IReqraftRawResponse,
52
+ } from './utils/fetch';
40
53
  export * from './utils/websocket';
41
54
  export { ReqraftLspClient } from './utils/lspClient';
42
55
  export type { IReqraftLspClientOptions } from './utils/lspClient';
@@ -1,6 +1,7 @@
1
1
  import { ReqoreP, ReqoreSpinner, ReqoreTree } from '@qoretechnologies/reqore';
2
2
  import { StoryObj } from '@storybook/react-vite';
3
3
  import { testsWaitForText } from '../../../__tests__/utils';
4
+ import { storyApiUrl } from '../../stories/storyNetwork';
4
5
  import { StoryMeta } from '../../types';
5
6
  import { currentUserStore } from './currentUser';
6
7
 
@@ -9,7 +10,7 @@ const meta = {
9
10
  parameters: {
10
11
  mockData: [
11
12
  {
12
- url: 'https://hq.qoretechnologies.com:8092/api/latest/users?action=current',
13
+ url: storyApiUrl('users?action=current'),
13
14
  method: 'GET',
14
15
  status: 200,
15
16
  response: {