@qoretechnologies/reqraft 0.10.54 → 0.10.55-pr.115.g37abdfc

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 (370) 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 +272 -19
  54. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  55. package/dist/components/form/engine/FormEngine.js +666 -161
  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 +7 -0
  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 +18 -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/rich-text/RichText.d.ts +47 -2
  170. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  171. package/dist/components/form/fields/rich-text/RichText.js +153 -12
  172. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  173. package/dist/components/form/fields/select/Select.d.ts.map +1 -1
  174. package/dist/components/form/fields/select/Select.js +120 -20
  175. package/dist/components/form/fields/select/Select.js.map +1 -1
  176. package/dist/components/form/fields/select/SelectCollection.d.ts +166 -9
  177. package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
  178. package/dist/components/form/fields/select/SelectCollection.js +200 -7
  179. package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
  180. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  181. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  182. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  183. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  184. package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
  185. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  186. package/dist/components/form/fields/template/TemplateField.js +606 -58
  187. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  188. package/dist/components/form/fields/template/TemplateText.d.ts +36 -0
  189. package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -0
  190. package/dist/components/form/fields/template/TemplateText.js +80 -0
  191. package/dist/components/form/fields/template/TemplateText.js.map +1 -0
  192. package/dist/components/form/index.d.ts +2 -0
  193. package/dist/components/form/index.d.ts.map +1 -1
  194. package/dist/components/form/index.js +4 -0
  195. package/dist/components/form/index.js.map +1 -1
  196. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  197. package/dist/components/smartEditor/SmartEditor.js +29 -8
  198. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  199. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  200. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  201. package/dist/components/smartEditor/useLspSession.js +16 -2
  202. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  203. package/dist/components/supportTicket/TicketReplyBox.d.ts +8 -4
  204. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  205. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  206. package/dist/helpers/common.d.ts.map +1 -1
  207. package/dist/helpers/common.js +7 -3
  208. package/dist/helpers/common.js.map +1 -1
  209. package/dist/helpers/dpqlDetection.d.ts +58 -0
  210. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  211. package/dist/helpers/dpqlDetection.js +164 -0
  212. package/dist/helpers/dpqlDetection.js.map +1 -0
  213. package/dist/helpers/optionUiTypes.d.ts +31 -3
  214. package/dist/helpers/optionUiTypes.d.ts.map +1 -1
  215. package/dist/helpers/optionUiTypes.js +36 -1
  216. package/dist/helpers/optionUiTypes.js.map +1 -1
  217. package/dist/helpers/options.d.ts +57 -0
  218. package/dist/helpers/options.d.ts.map +1 -1
  219. package/dist/helpers/options.js +97 -1
  220. package/dist/helpers/options.js.map +1 -1
  221. package/dist/helpers/templateItems.d.ts +47 -0
  222. package/dist/helpers/templateItems.d.ts.map +1 -0
  223. package/dist/helpers/templateItems.js +88 -0
  224. package/dist/helpers/templateItems.js.map +1 -0
  225. package/dist/helpers/templateText.d.ts +80 -0
  226. package/dist/helpers/templateText.d.ts.map +1 -0
  227. package/dist/helpers/templateText.js +291 -0
  228. package/dist/helpers/templateText.js.map +1 -0
  229. package/dist/helpers/templates.d.ts +30 -12
  230. package/dist/helpers/templates.d.ts.map +1 -1
  231. package/dist/helpers/templates.js +91 -33
  232. package/dist/helpers/templates.js.map +1 -1
  233. package/dist/helpers/validations.d.ts +17 -8
  234. package/dist/helpers/validations.d.ts.map +1 -1
  235. package/dist/helpers/validations.js +117 -22
  236. package/dist/helpers/validations.js.map +1 -1
  237. package/dist/hooks/useArgSchema.d.ts +2 -1
  238. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  239. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  240. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  241. package/dist/hooks/useFetch/useFetch.js +47 -9
  242. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  243. package/dist/hooks/useTemplates.d.ts +20 -0
  244. package/dist/hooks/useTemplates.d.ts.map +1 -1
  245. package/dist/hooks/useTemplates.js +34 -2
  246. package/dist/hooks/useTemplates.js.map +1 -1
  247. package/dist/index.d.ts +2 -1
  248. package/dist/index.d.ts.map +1 -1
  249. package/dist/index.js +5 -1
  250. package/dist/index.js.map +1 -1
  251. package/dist/utils/fetch.d.ts +151 -1
  252. package/dist/utils/fetch.d.ts.map +1 -1
  253. package/dist/utils/fetch.js +256 -44
  254. package/dist/utils/fetch.js.map +1 -1
  255. package/dist/utils/lspClient.d.ts +6 -0
  256. package/dist/utils/lspClient.d.ts.map +1 -1
  257. package/dist/utils/lspClient.js +62 -11
  258. package/dist/utils/lspClient.js.map +1 -1
  259. package/dist/utils/websocket.d.ts +6 -0
  260. package/dist/utils/websocket.d.ts.map +1 -1
  261. package/dist/utils/websocket.js +15 -7
  262. package/dist/utils/websocket.js.map +1 -1
  263. package/package.json +8 -4
  264. package/src/components/composer/Composer.tsx +7 -2
  265. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  266. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  267. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  268. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  269. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  270. package/src/components/dpqlEditor/index.ts +4 -0
  271. package/src/components/dpqlEditor/types.ts +38 -2
  272. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  273. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  274. package/src/components/form/engine/CompactRow.tsx +487 -160
  275. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  276. package/src/components/form/engine/FormEngine.stories.tsx +1294 -161
  277. package/src/components/form/engine/FormEngine.tsx +916 -213
  278. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +331 -0
  279. package/src/components/form/engine/FormFieldsSkeleton.tsx +77 -0
  280. package/src/components/form/engine/OptionFieldMessages.tsx +300 -14
  281. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +94 -3
  282. package/src/components/form/engine/compactRowContext.ts +15 -6
  283. package/src/components/form/engine/compactRowStyles.ts +56 -2
  284. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  285. package/src/components/form/engine/optionActions.ts +89 -1
  286. package/src/components/form/engine/optionsContext.ts +24 -0
  287. package/src/components/form/engine/readFirst.ts +88 -8
  288. package/src/components/form/engine/rendererTypes.ts +48 -4
  289. package/src/components/form/engine/rowFocus.ts +101 -0
  290. package/src/components/form/engine/rowMenuContext.ts +159 -0
  291. package/src/components/form/engine/rowOpenPicker.ts +27 -0
  292. package/src/components/form/engine/typeChoices.ts +63 -0
  293. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  294. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  295. package/src/components/form/expressions/ExpressionField.tsx +269 -55
  296. package/src/components/form/expressions/argumentPresence.ts +213 -0
  297. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +38 -21
  298. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  299. package/src/components/form/expressions/builder/index.tsx +39 -81
  300. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  301. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  302. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  303. package/src/components/form/expressions/mockExpressions.ts +8 -4
  304. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  305. package/src/components/form/expressions/types.ts +7 -0
  306. package/src/components/form/expressions/useExpressions.ts +5 -1
  307. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  308. package/src/components/form/fields/Field.tsx +99 -20
  309. package/src/components/form/fields/allowed-values/AllowedValues.tsx +106 -44
  310. package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
  311. package/src/components/form/fields/auto/AutoFormField.tsx +97 -21
  312. package/src/components/form/fields/date/Date.tsx +17 -1
  313. package/src/components/form/fields/long-string/LongString.tsx +15 -3
  314. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +65 -6
  315. package/src/components/form/fields/rich-text/RichText.tsx +248 -30
  316. package/src/components/form/fields/select/Select.stories.tsx +392 -0
  317. package/src/components/form/fields/select/Select.tsx +164 -27
  318. package/src/components/form/fields/select/SelectCollection.tsx +350 -6
  319. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  320. package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
  321. package/src/components/form/fields/template/TemplateField.tsx +825 -131
  322. package/src/components/form/fields/template/TemplateText.tsx +115 -0
  323. package/src/components/form/index.tsx +4 -0
  324. package/src/components/log/Log.stories.tsx +2 -1
  325. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +3 -3
  326. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  327. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  328. package/src/components/smartEditor/useLspSession.ts +21 -3
  329. package/src/components/supportTicket/TicketReplyBox.tsx +8 -4
  330. package/src/components/ticketThread/TicketThread.stories.tsx +1 -0
  331. package/src/helpers/common.ts +7 -3
  332. package/src/helpers/dpqlDetection.ts +209 -0
  333. package/src/helpers/optionUiTypes.ts +38 -0
  334. package/src/helpers/options.ts +96 -0
  335. package/src/helpers/templateItems.ts +81 -0
  336. package/src/helpers/templateText.ts +350 -0
  337. package/src/helpers/templates.ts +94 -41
  338. package/src/helpers/validations.ts +138 -39
  339. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  340. package/src/hooks/useFetch/useFetch.tsx +53 -7
  341. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  342. package/src/hooks/useTemplates.ts +41 -0
  343. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  344. package/src/index.tsx +14 -1
  345. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  346. package/src/stories/storyNetwork.ts +92 -0
  347. package/src/utils/fetch.ts +311 -21
  348. package/src/utils/lspClient.ts +68 -11
  349. package/src/utils/websocket.ts +20 -9
  350. package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts +0 -73
  351. package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts.map +0 -1
  352. package/dist/components/form/engine/__fixtures__/basicFormOptions.js +0 -317
  353. package/dist/components/form/engine/__fixtures__/basicFormOptions.js.map +0 -1
  354. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts +0 -41
  355. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts.map +0 -1
  356. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js +0 -45
  357. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js.map +0 -1
  358. package/dist/components/form/expressions/dpqlMockLsp.d.ts +0 -3
  359. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +0 -1
  360. package/dist/components/form/expressions/dpqlMockLsp.js +0 -91
  361. package/dist/components/form/expressions/dpqlMockLsp.js.map +0 -1
  362. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +0 -2
  363. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +0 -1
  364. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +0 -9
  365. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +0 -1
  366. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +0 -55
  367. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +0 -1
  368. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +0 -126
  369. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +0 -1
  370. package/dist/stories/Data/fileFieldArgSchema.json +0 -86
@@ -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: {
@@ -0,0 +1,92 @@
1
+ // Copyright 2026 Qore Technologies, s.r.o.
2
+ // Where a story's network goes: the instance its Reqraft points at, the URLs
3
+ // its mocks must intercept, and the requests every story is mocked for.
4
+ //
5
+ // `query()` and `ReqraftWebSocket` build absolute URLs from the configured
6
+ // instance, and both mock layers match on the host as well as the path —
7
+ // storybook-addon-mock keys on `host + pathname`, mock-socket on the URL minus
8
+ // its query. A mock written against a hard-coded host stops matching the moment
9
+ // the instance is overridden, and the request goes out to the real network
10
+ // instead, silently. Build mock URLs with the helpers below.
11
+ import { defaultQorusTypes } from '../hooks/useQorusTypes';
12
+ import { buildReqraftApiUrl } from '../utils/fetch';
13
+ import { buildReqraftSocketUrl } from '../utils/websocket';
14
+
15
+ /**
16
+ * What storybook-addon-mock accepts as a response. Anything else — a bare
17
+ * number or string — fails the addon's validation and the entry is kept but
18
+ * never matched, so the request goes out to the network with no warning.
19
+ */
20
+ export type TStoryMockResponse = object | ((request: unknown) => unknown);
21
+
22
+ export interface IStoryMockRequest {
23
+ url: string;
24
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
25
+ status: number;
26
+ response: TStoryMockResponse;
27
+ }
28
+
29
+ /**
30
+ * The instance every story's Reqraft is initialized with. Override it with
31
+ * `REACT_APP_QORUS_INSTANCE` (or `QORUS_INSTANCE`) to run live stories against
32
+ * your own Qorus; mocks built from it follow the override.
33
+ */
34
+ export const STORY_QORUS_INSTANCE =
35
+ process.env.REACT_APP_QORUS_INSTANCE || 'https://hq.qoretechnologies.com:8092/';
36
+
37
+ /** The URL `query()` requests for `path`. */
38
+ export const storyApiUrl = (path: string): string =>
39
+ buildReqraftApiUrl(STORY_QORUS_INSTANCE, path);
40
+
41
+ /**
42
+ * The URL `ReqraftWebSocket` dials for `path`, token omitted — mock-socket
43
+ * matches servers without the query string.
44
+ */
45
+ export const storySocketUrl = (path: string): string =>
46
+ buildReqraftSocketUrl(STORY_QORUS_INSTANCE, path);
47
+
48
+ /** A GET of `path` answered with `response`. */
49
+ const mockGet = (path: string, response: TStoryMockResponse): IStoryMockRequest[] => [
50
+ { url: storyApiUrl(path), method: 'GET', status: 200, response },
51
+ ];
52
+
53
+ /**
54
+ * The type catalogue, pinned to the built-in list. `useQorusTypes` uses a
55
+ * reachable server's list *instead of* the built-in one, so an unmocked story
56
+ * renders whichever labels that server happens to have — and every FormEngine
57
+ * shows its loading skeleton until the request settles, which against an
58
+ * unreachable instance is never.
59
+ */
60
+ const QORUS_TYPE_INFO_MOCK_DATA = mockGet('system/qorus-type-info', defaultQorusTypes);
61
+
62
+ /**
63
+ * The system expression catalogue, empty. `useExpressions` fetches it whenever
64
+ * functions are allowed — even when the story passes its own list, which is
65
+ * merged in as extras — and TemplateField shows a skeleton in place of "Use
66
+ * Expression" until it arrives. Stories that need functions provide them
67
+ * (`expressions`, or their own `mockData` for this URL).
68
+ */
69
+ const EXPRESSIONS_CATALOGUE_MOCK_DATA = mockGet('system?action=expressions&context=ui', []);
70
+
71
+ /**
72
+ * `ReqraftWebSocket`'s server probe, which every reconnect attempt awaits
73
+ * before it dials. Unanswered, no attempt is ever made — a mock socket server
74
+ * that comes back is never reconnected to, and one that stays down is never
75
+ * given up on. The probe only needs an answer; its body is not read.
76
+ */
77
+ const SERVER_STATUS_MOCK_DATA = mockGet('system/pid', {});
78
+
79
+ /**
80
+ * Requests components make on their own, whatever the story is about —
81
+ * mounting a FormEngine fetches the type catalogue; allowing functions fetches
82
+ * the expression catalogue; a reconnecting socket probes the server.
83
+ * Registered for every story as storybook-addon-mock's `globalMockData`
84
+ * (`.storybook/preview.tsx`, and mirrored for the Vitest run in
85
+ * `.storybook/vitest.setup.ts`); a story's own `mockData` entry for the same
86
+ * URL takes precedence.
87
+ */
88
+ export const GLOBAL_STORY_MOCK_DATA: IStoryMockRequest[] = [
89
+ ...QORUS_TYPE_INFO_MOCK_DATA,
90
+ ...EXPRESSIONS_CATALOGUE_MOCK_DATA,
91
+ ...SERVER_STATUS_MOCK_DATA,
92
+ ];
@@ -13,6 +13,21 @@ export interface IReqraftFetchResponse<T> {
13
13
  ok: boolean;
14
14
  code?: number;
15
15
  error?: any;
16
+ /**
17
+ * The parsed error body on a non-ok response, when the server sent JSON.
18
+ *
19
+ * Qorus errors carry `{ err, desc }`; `error` above is only the HTTP reason
20
+ * phrase, which is why a caller that wanted the sentence had to reach into
21
+ * `data`. Consumers that need the code (`err`) or the message (`desc`) should
22
+ * read this rather than re-parsing.
23
+ */
24
+ errorBody?: Record<string, any>;
25
+ /**
26
+ * The response text when the body was NOT valid JSON. A non-JSON error would
27
+ * otherwise arrive as `{}` with only a reason phrase, discarding whatever the
28
+ * server actually said.
29
+ */
30
+ rawText?: string;
16
31
  response: Response;
17
32
  }
18
33
 
@@ -39,26 +54,104 @@ export const setupFetch = ({
39
54
  }
40
55
  };
41
56
 
57
+ /**
58
+ * The API path a caller asked for, without the leading slash the prefix already
59
+ * carries.
60
+ *
61
+ * `doFetchData` builds `${instance}api/latest/${url}`, and that prefix ENDS in a
62
+ * slash — so a caller writing `/dataprovider/arg_schemas/x` produced
63
+ * `api/latest//dataprovider/arg_schemas/x`. Qorus serves it, which is how both
64
+ * spellings survived side by side (`useArgSchema` writes one, `CompactRow` and
65
+ * `AutoFormField` the other), but it is not harmless: the query cache is keyed
66
+ * on the url, so `/x` and `x` were two cache entries and two requests for one
67
+ * resource.
68
+ *
69
+ * Normalised here, once, rather than at each call site — the convention is
70
+ * invisible from a caller's point of view and the next one would get it wrong
71
+ * again. Only applied when the prefix is added: a caller that opts out of the
72
+ * prefix owns its whole path, leading slash included.
73
+ */
74
+ export const reqraftApiPath = (url: string, noApiPrefix: boolean = false): string =>
75
+ noApiPrefix ? url : url.replace(/^\/+/, '');
76
+
77
+ /**
78
+ * A cheap, stable, non-reversible marker for a bearer token.
79
+ *
80
+ * Two callers holding DIFFERENT tokens are two identities and must not share a
81
+ * cached response, so the token has to take part in the cache key — but a raw
82
+ * token in a key is a credential sitting in devtools' query inspector. djb2 is
83
+ * enough to tell two tokens apart, which is all the key needs.
84
+ */
85
+ const tokenMarker = (token?: string): string => {
86
+ if (!token) {
87
+ return '';
88
+ }
89
+ let hash = 5381;
90
+ for (let index = 0; index < token.length; index++) {
91
+ hash = ((hash << 5) + hash + token.charCodeAt(index)) | 0;
92
+ }
93
+ return `:t${(hash >>> 0).toString(36)}`;
94
+ };
95
+
96
+ /**
97
+ * The one cache key for a request, used by every caller in every downstream
98
+ * project so that one resource is one entry and one in-flight request.
99
+ *
100
+ * `instance` and the token take part because both change WHICH server and WHOSE
101
+ * data answers: a call that overrides the instance, or that carries an explicit
102
+ * token, is a different resource that happens to share a path.
103
+ */
104
+ export const reqraftCacheKey = ({
105
+ url,
106
+ method = 'GET',
107
+ body,
108
+ noApiPrefix,
109
+ instance,
110
+ token,
111
+ }: Pick<IReqraftQueryConfig, 'url' | 'method' | 'body' | 'noApiPrefix' | 'instance' | 'token'>) =>
112
+ `${reqraftApiPath(url, noApiPrefix)}:${method}:${JSON.stringify(body || {})}${
113
+ instance && instance !== fetchConfig.instance ? `:@${instance}` : ''
114
+ }${tokenMarker(token)}`;
115
+
116
+ /**
117
+ * The URL `query()` requests for `url` on `instance` — the single definition.
118
+ * Story mocks build their URLs with it too, so they cannot drift from what the
119
+ * code actually dials.
120
+ *
121
+ * `noApiPrefix` means the caller owns the WHOLE url — an absolute address for
122
+ * another service, which is the only thing reqraft itself uses it for
123
+ * (`useExpressions` sets it exactly when `expressionsUrl` is `http(s)://…`).
124
+ * Prepending the instance there built `https://instance:8011/https://other/…`.
125
+ */
126
+ export const buildReqraftApiUrl = (instance: string, url: string, noApiPrefix = false): string =>
127
+ noApiPrefix ? url : `${instance}api/latest/${reqraftApiPath(url)}`;
128
+
42
129
  async function doFetchData(
43
130
  url: string,
44
131
  method = 'GET',
45
132
  body?: { [key: string]: any },
46
133
  noApiPrefix = false,
47
- instance = fetchConfig.instance
134
+ instance = fetchConfig.instance,
135
+ headers?: Record<string, string>,
136
+ token?: string
48
137
  ): Promise<Response> {
49
138
  // We do not check for token because Qorus now handles auth automatically via cookies
50
139
  // token is only supplied in a dev environment
51
- const headers = {
140
+ const finalHeaders: Record<string, string> = {
52
141
  'Content-Type': 'application/json',
142
+ ...(headers ?? {}),
53
143
  };
54
144
 
55
- if (fetchConfig.instanceToken && !fetchConfig.instanceRbacDisabled) {
56
- headers['Authorization'] = `Bearer ${fetchConfig.instanceToken}`;
145
+ const finalToken =
146
+ token ?? (fetchConfig.instanceRbacDisabled ? undefined : fetchConfig.instanceToken);
147
+
148
+ if (finalToken && !finalHeaders['Authorization']) {
149
+ finalHeaders['Authorization'] = `Bearer ${finalToken}`;
57
150
  }
58
151
 
59
- return fetch(`${instance}${noApiPrefix ? '' : 'api/latest/'}${url}`, {
152
+ return fetch(buildReqraftApiUrl(instance, url, noApiPrefix), {
60
153
  method,
61
- headers,
154
+ headers: finalHeaders,
62
155
  body: JSON.stringify(body),
63
156
  credentials: 'include',
64
157
  }).catch((error) => {
@@ -75,11 +168,123 @@ export interface IReqraftQueryConfig {
75
168
  body?: Record<string | number, any>;
76
169
  cache?: boolean;
77
170
  queryClient?: QueryClient;
171
+ /**
172
+ * The `url` is a complete address and is used verbatim — neither the instance
173
+ * nor `api/latest/` is prepended, and its leading slash is left alone. For
174
+ * reaching a service that is not this Qorus instance.
175
+ */
78
176
  noApiPrefix?: boolean;
79
177
  /** Override the global instance URL for this single call (e.g. to target a different port). */
80
178
  instance?: string;
179
+ /** Extra request headers, merged over the default `Content-Type`. */
180
+ headers?: Record<string, string>;
181
+ /**
182
+ * A bearer token for THIS call, instead of the configured instance token —
183
+ * for a route that is authenticated as somebody other than the current app
184
+ * session. Takes part in the cache key.
185
+ */
186
+ token?: string;
187
+ /**
188
+ * An explicit cache key, for a caller that must group or separate requests
189
+ * differently from the default `path:method:body` identity.
190
+ */
191
+ cacheKey?: string;
192
+ /**
193
+ * How long a cached response counts as fresh, in ms. Defaults to 5 minutes
194
+ * when `cache` is on. `Infinity` keeps it for the life of the page — what a
195
+ * caller wants for a catalogue that cannot change without a server restart.
196
+ */
197
+ staleTime?: number;
198
+ /**
199
+ * Whether a 401 on this request should send the page to
200
+ * `fetchConfig.unauthorizedRedirect`. Defaults to `true`.
201
+ *
202
+ * A 401 only means "this app's session expired" when the request actually
203
+ * carried this app's credential. A call that targets another service, or that
204
+ * hands over an explicit token, proves nothing about the session — signing the
205
+ * user out because an unrelated credential was refused is wrong. Callers that
206
+ * own their own auth policy (or that must never navigate, such as a test
207
+ * runner or a Storybook preview) pass `false`.
208
+ */
209
+ redirectOnUnauthorized?: boolean;
81
210
  }
82
211
 
212
+ /**
213
+ * Reads a response body without consuming it, and says whether it was JSON.
214
+ *
215
+ * `data` stays `{}` for a body that is empty or not JSON, which is the contract
216
+ * reqraft's consumers have always been given. The text is handed back separately
217
+ * as `rawText` so an error that was not JSON still carries whatever the server
218
+ * said, instead of being flattened to a bare reason phrase.
219
+ */
220
+ const readBody = async (response: Response): Promise<{ data: any; rawText?: string }> => {
221
+ const rawText = await response.clone().text();
222
+
223
+ try {
224
+ return { data: JSON.parse(rawText) };
225
+ } catch (_error) {
226
+ return { data: {}, rawText };
227
+ }
228
+ };
229
+
230
+ /**
231
+ * The single request mechanism.
232
+ *
233
+ * Every request in reqraft AND in the projects built on it goes through here,
234
+ * so that one resource has one cache entry and one in-flight request no matter
235
+ * which library, hook or component asked for it. A downstream project that
236
+ * keeps its own fetch helper (qorus-ide's `fetchData` is the one this was
237
+ * written for) should adapt onto this rather than hold a second cache: two
238
+ * caches over one API cannot see each other's in-flight requests, so any
239
+ * resource both libraries want is fetched twice by construction.
240
+ */
241
+ /**
242
+ * Put a resource the HOST already has into the cache `query` reads.
243
+ *
244
+ * The comment on `query` states the problem: two caches over one API cannot see
245
+ * each other's in-flight requests, so any resource both libraries want is
246
+ * fetched twice by construction. This is the seam that lets a host settle it
247
+ * without either side giving up its own loader.
248
+ *
249
+ * The case it was written for: the Qorus IDE warm-starts
250
+ * `/system/qorus-type-info` from `index.html`, before the bundle has even
251
+ * parsed, because its whole boot gates on it — and then reqraft's own
252
+ * `useQorusTypes` asked for it again on the first form's mount. Measured on the
253
+ * live IDE at 419ms and 2777ms, the same URL, the same answer.
254
+ *
255
+ * Seeding is enough to stop the second request: `query` goes through
256
+ * `fetchQuery` with a `staleTime`, and a fresh entry is returned without the
257
+ * query function ever running. `useFetch` reads the same cache directly, so it
258
+ * also skips its loading state and renders the answer on its first paint.
259
+ *
260
+ * Only for a resource the host has genuinely FETCHED. Seeding a guess would
261
+ * make every consumer believe a request had succeeded.
262
+ */
263
+ export const seedQueryCache = <T>({
264
+ url,
265
+ method = 'GET',
266
+ body,
267
+ noApiPrefix,
268
+ instance,
269
+ token,
270
+ data,
271
+ queryClient = ReqraftQueryClient,
272
+ }: Pick<
273
+ IReqraftQueryConfig,
274
+ 'url' | 'method' | 'body' | 'noApiPrefix' | 'instance' | 'token' | 'queryClient'
275
+ > & { data: T }): void => {
276
+ const key = reqraftCacheKey({ url, method, body, noApiPrefix, instance, token });
277
+
278
+ queryClient.setQueryData([key], {
279
+ data,
280
+ rawText: undefined,
281
+ ok: true,
282
+ status: 200,
283
+ statusText: 'OK',
284
+ response: undefined,
285
+ });
286
+ };
287
+
83
288
  export async function query<T>({
84
289
  url,
85
290
  method = 'GET',
@@ -88,47 +293,54 @@ export async function query<T>({
88
293
  queryClient = ReqraftQueryClient,
89
294
  noApiPrefix = false,
90
295
  instance,
296
+ headers,
297
+ token,
298
+ cacheKey,
299
+ staleTime,
300
+ redirectOnUnauthorized = true,
91
301
  }: IReqraftQueryConfig): Promise<IReqraftFetchResponse<T>> {
92
302
  const shouldCache = method === 'DELETE' || method === 'POST' ? false : cache;
93
- const cacheKey = `${url}:${method}:${JSON.stringify(body || {})}`;
303
+ const key = cacheKey ?? reqraftCacheKey({ url, method, body, noApiPrefix, instance, token });
94
304
 
95
305
  const requestData = await queryClient.fetchQuery({
96
- queryKey: [cacheKey],
306
+ queryKey: [key],
97
307
  queryFn: async () => {
98
- const response = await doFetchData(url, method, body, noApiPrefix, instance);
308
+ const response = await doFetchData(url, method, body, noApiPrefix, instance, headers, token);
99
309
 
100
- if (response.status === 401 && fetchConfig.unauthorizedRedirect) {
101
- window.location.href = fetchConfig.unauthorizedRedirect(window.location);
310
+ if (response.status === 401 && redirectOnUnauthorized && fetchConfig.unauthorizedRedirect) {
311
+ // `replace`, not an assignment: the page being left is dead, and a
312
+ // history entry for it means Back lands on a view that can only 401
313
+ // again.
314
+ window.location.replace(fetchConfig.unauthorizedRedirect(window.location));
102
315
  }
103
316
 
104
- const clone = response.clone();
105
- let data: any;
106
-
107
- try {
108
- data = await clone.json();
109
- } catch (error) {
110
- data = {};
111
- }
317
+ const { data, rawText } = await readBody(response);
112
318
 
113
319
  return {
114
320
  data,
321
+ rawText,
115
322
  ok: response.ok,
116
323
  status: response.status,
117
324
  statusText: response.statusText,
118
325
  response,
119
326
  };
120
327
  },
121
- staleTime: shouldCache ? CACHE_EXPIRATION_TIME : 0,
328
+ staleTime: shouldCache ? (staleTime ?? CACHE_EXPIRATION_TIME) : 0,
122
329
  });
123
330
 
124
331
  if (!requestData.ok) {
125
- queryClient.invalidateQueries({ queryKey: [cacheKey] });
332
+ queryClient.invalidateQueries({ queryKey: [key] });
126
333
 
127
334
  return {
128
335
  data: requestData.data,
129
336
  ok: false,
130
337
  code: requestData.status,
131
338
  error: requestData.statusText,
339
+ // Only when the body really was JSON: `data` is `{}` for a non-JSON body,
340
+ // and an empty object masquerading as a parsed error tells a caller
341
+ // nothing while looking like it did.
342
+ errorBody: requestData.rawText === undefined ? requestData.data : undefined,
343
+ rawText: requestData.rawText,
132
344
  response: requestData.response,
133
345
  };
134
346
  }
@@ -137,6 +349,84 @@ export async function query<T>({
137
349
  data: requestData.data,
138
350
  ok: true,
139
351
  code: requestData.status,
352
+ rawText: requestData.rawText,
140
353
  response: requestData.response,
141
354
  };
142
355
  }
356
+
357
+ export interface IReqraftRawResponse {
358
+ ok: boolean;
359
+ code?: number;
360
+ error?: string;
361
+ /** The response bytes. Undefined on a non-ok response. */
362
+ blob?: Blob;
363
+ /** The `Content-Type` the bytes were served with, without parameters. */
364
+ mime?: string;
365
+ }
366
+
367
+ /**
368
+ * Fetch a resource whose body is NOT JSON — a file, an export, a log.
369
+ *
370
+ * Shares `doFetchData` with `query`, which owns the API prefix, the bearer token
371
+ * and `credentials: 'include'`. The point is that raw responses do not become a
372
+ * second, drifting copy of that auth handling.
373
+ *
374
+ * Nothing is cached: these payloads are large and read once.
375
+ *
376
+ * @param accept the `Accept` header. Endpoints that negotiate on the file's own
377
+ * MIME type answer **406** unless the caller accepts that type, and the caller
378
+ * usually cannot know it ahead of time, so this accepts anything by default.
379
+ */
380
+ export async function queryRaw({
381
+ url,
382
+ method = 'GET',
383
+ body,
384
+ noApiPrefix = false,
385
+ instance,
386
+ headers,
387
+ token,
388
+ accept = '*/*',
389
+ redirectOnUnauthorized = true,
390
+ }: Omit<IReqraftQueryConfig, 'cache' | 'queryClient' | 'cacheKey' | 'staleTime'> & {
391
+ accept?: string;
392
+ }): Promise<IReqraftRawResponse> {
393
+ try {
394
+ const response = await doFetchData(
395
+ url,
396
+ method,
397
+ body,
398
+ noApiPrefix,
399
+ instance,
400
+ {
401
+ Accept: accept,
402
+ ...(headers ?? {}),
403
+ },
404
+ token
405
+ );
406
+
407
+ if (response.status === 401 && redirectOnUnauthorized && fetchConfig.unauthorizedRedirect) {
408
+ window.location.replace(fetchConfig.unauthorizedRedirect(window.location));
409
+ }
410
+
411
+ if (!response.ok) {
412
+ let error: string;
413
+ try {
414
+ // an error body is still JSON even when the success body is not
415
+ error = (await response.json()).desc ?? response.statusText;
416
+ } catch (_error) {
417
+ error = response.statusText;
418
+ }
419
+
420
+ return { ok: false, code: response.status, error };
421
+ }
422
+
423
+ return {
424
+ ok: true,
425
+ code: response.status,
426
+ blob: await response.blob(),
427
+ mime: (response.headers.get('content-type') ?? '').split(';')[0].trim() || undefined,
428
+ };
429
+ } catch (error) {
430
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
431
+ }
432
+ }