@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,76 @@
1
+ import { ReqoreControlGroup, ReqoreSkeleton } from '@qoretechnologies/reqore';
2
+
3
+ export interface IFormFieldsSkeletonProps {
4
+ /** How many field rows to stand in for. */
5
+ rows?: number;
6
+ /**
7
+ * Fill the height of the slot instead of being as tall as its rows.
8
+ *
9
+ * For a wait that stands in for a WHOLE form. Sizing from the row count makes
10
+ * the placeholder a different height from the thing that replaces it, so the
11
+ * swap resizes the container and everything jumps — measured on the live IDE
12
+ * at 396px of skeleton in an 835px slot. Filling the slot keeps the geometry
13
+ * still across the swap.
14
+ *
15
+ * NOT the default: a wait that stands in for one field or a nested hash is
16
+ * meant to be the size of that field, and stretching it to fill a panel would
17
+ * be a bigger lie than the jump.
18
+ */
19
+ fill?: boolean;
20
+ className?: string;
21
+ /**
22
+ * Which wait this placeholder stands for, surfaced as `data-wait`.
23
+ *
24
+ * A form has seven independent reasons to be waiting and they all draw the
25
+ * same picture, so "the form is still a placeholder at 2039ms" says nothing
26
+ * about what to fix — measured on the live alert rule, three sections cleared
27
+ * in the same commit and neither of the two shared waits (`types`, 140ms;
28
+ * `templates`, 1500ms) explained it. Naming the reason in the DOM turns that
29
+ * from a guess into a reading.
30
+ */
31
+ reason?: string;
32
+ }
33
+
34
+ /**
35
+ * A form that has not arrived yet, drawn as the form it is about to be.
36
+ *
37
+ * ONE shape for every wait inside a form, which is the whole point of it.
38
+ * Reported twice as "a series of skeletons": a single page could show the
39
+ * engine's own loading state (three bars over three big blocks), then a
40
+ * spinner reading "Loading field data…" while a field resolved its schema,
41
+ * then an 80px block for a nested hash, then the host's own field-row
42
+ * skeleton — four different pictures for one wait, each replacing the last, so
43
+ * a load that is actually quite quick reads as a stack of unrelated screens.
44
+ *
45
+ * A row is a label bar over an input bar because that is what a field is. The
46
+ * count is the only thing a caller varies: a nested field standing in for
47
+ * itself asks for one, a whole form for several.
48
+ */
49
+ export const FormFieldsSkeleton = ({
50
+ rows = 6,
51
+ fill,
52
+ className,
53
+ reason,
54
+ }: IFormFieldsSkeletonProps) => (
55
+ <ReqoreControlGroup
56
+ className={className}
57
+ data-wait={reason}
58
+ vertical
59
+ fluid
60
+ gapSize='big'
61
+ style={{
62
+ padding: '12px 6px',
63
+ width: '100%',
64
+ ...(fill ? { flex: '1 1 auto', minHeight: 0 } : {}),
65
+ }}
66
+ >
67
+ {Array.from({ length: rows }).map((_, index) => (
68
+ <ReqoreControlGroup key={index} vertical fluid gapSize='tiny'>
69
+ <ReqoreSkeleton size='small' width='140px' height='12px' />
70
+ <ReqoreSkeleton size='normal' width='100%' height='34px' />
71
+ </ReqoreControlGroup>
72
+ ))}
73
+ </ReqoreControlGroup>
74
+ );
75
+
76
+ export default FormFieldsSkeleton;
@@ -1,11 +1,13 @@
1
1
  import { ReqoreControlGroup, ReqoreTag, ReqoreVerticalSpacer } from '@qoretechnologies/reqore';
2
2
  import { IReqoreTagProps } from '@qoretechnologies/reqore/dist/components/Tag';
3
3
  import { IQorusFormField, IQorusFormSchema, TQorusForm } from '@qoretechnologies/ts-toolkit';
4
- import { flatten, size } from 'lodash';
4
+ import { isArray, size } from 'lodash';
5
5
  import { useMemo } from 'react';
6
6
  import { getRequiredOptionMessage } from '../../../helpers/options';
7
+ import { isUntypedOptionType } from '../../../helpers/optionUiTypes';
7
8
  import {
8
9
  hasAllDependenciesFullfilled,
10
+ parseDependency,
9
11
  validateFieldWithResult,
10
12
  validateOptionWithRequiredGroups,
11
13
  } from '../../../helpers/validations';
@@ -52,7 +54,10 @@ export const isConditionalMessageShown = (
52
54
  if (message.when && !hasAllDependenciesFullfilled(message.when as never, values, optionsSchema)) {
53
55
  return false;
54
56
  }
55
- if (message.unless && hasAllDependenciesFullfilled(message.unless as never, values, optionsSchema)) {
57
+ if (
58
+ message.unless &&
59
+ hasAllDependenciesFullfilled(message.unless as never, values, optionsSchema)
60
+ ) {
56
61
  return false;
57
62
  }
58
63
  return true;
@@ -65,12 +70,82 @@ export const getShownSchemaMessages = <T extends IConditionalFieldMessage>(
65
70
  optionsSchema?: IQorusFormSchema
66
71
  ): T[] => (messages || []).filter((m) => isConditionalMessageShown(m, allOptions, optionsSchema));
67
72
 
73
+ /**
74
+ * Name one `depends_on` entry so the reader can act on it.
75
+ *
76
+ * The list used to be built by looking each entry up as a whole field name,
77
+ * which is only ever true of the bare-name form. A COMPARISON entry
78
+ * (`kind=type`, `kind!=type`) matched no field, was dropped, and a field whose
79
+ * dependencies are all comparisons — the shape every subject field of a test
80
+ * uses — rendered "…are not fulfilled:" with nothing after the colon. The
81
+ * reader was told a dependency was unmet and not which one, by a sentence that
82
+ * stopped mid-way.
83
+ *
84
+ * One parser for both halves, the same one the evaluator uses, so the sentence
85
+ * and the lock cannot disagree about what an entry means.
86
+ */
87
+ const describeDependency = (dependency: string, schema: IQorusFormSchema): string | undefined => {
88
+ const { name, op, value } = parseDependency(dependency);
89
+ const dependencySchema = schema[name];
90
+
91
+ if (!dependencySchema) {
92
+ return undefined;
93
+ }
94
+
95
+ const label = `"${dependencySchema.display_name || name}"`;
96
+
97
+ return op ? `${label} must ${op === '!=' ? 'not ' : ''}be "${value}"` : label;
98
+ };
99
+
100
+ /**
101
+ * The whole `depends_on` list as one phrase.
102
+ *
103
+ * A nested entry is an OR — only one of its members has to hold. It used to be
104
+ * flattened into the same comma-separated list as the top-level entries, which
105
+ * are an AND, so an either/or was shown to the reader as a set of things all
106
+ * required. The alternatives now stay grouped and joined by "or".
107
+ */
108
+ const describeDependencies = (
109
+ dependencies: (string | string[])[] | string[][],
110
+ schema: IQorusFormSchema
111
+ ): string =>
112
+ (dependencies as (string | string[])[])
113
+ .map((entry) => {
114
+ if (!isArray(entry)) {
115
+ return describeDependency(entry as string, schema);
116
+ }
117
+ const alternatives = (entry as string[])
118
+ .map((dep) => describeDependency(dep, schema))
119
+ .filter((dep): dep is string => !!dep);
120
+ if (!alternatives.length) {
121
+ return undefined;
122
+ }
123
+ return alternatives.length > 1 ? `(${alternatives.join(' or ')})` : alternatives[0];
124
+ })
125
+ .filter((entry): entry is string => !!entry)
126
+ .join(', ');
127
+
68
128
  export interface IOptionFieldMessagesProps {
69
129
  schema: IQorusFormSchema;
70
130
  option: IQorusFormField;
71
131
  allOptions?: TQorusForm;
72
132
  name: string;
73
133
  getType: (type: string) => string;
134
+ /**
135
+ * The reader has not edited this field in this session.
136
+ *
137
+ * An error is a report that something went WRONG. "This field is required"
138
+ * under a field nobody has been in yet reports nothing: the form is empty
139
+ * because it is new. The requirement is already carried three other ways —
140
+ * the asterisk on the label, the Needs-attention box the row sits in, and the
141
+ * completion meter — so the message waits until the reader has been in the
142
+ * field and left it empty, which IS a thing that went wrong.
143
+ *
144
+ * Only the plain required message is held back. A value that fails
145
+ * validation, an unmet required GROUP and a locked dependency are all facts
146
+ * about what the form currently holds, and they show immediately.
147
+ */
148
+ untouched?: boolean;
74
149
  }
75
150
 
76
151
  /**
@@ -84,13 +159,26 @@ export const getOptionFieldMessages = ({
84
159
  name,
85
160
  allOptions,
86
161
  getType,
162
+ untouched,
87
163
  }: IOptionFieldMessagesProps): IReqoreTagProps[] => {
88
164
  const optionSchema = schema[name];
89
165
  const result: IReqoreTagProps[] = [];
90
166
 
91
167
  if (option.value || option.value === false || option.value === 0 || option.value === null) {
168
+ /* A concrete type the VALUE carries beats an untyped schema.
169
+ `auto`/`any` declare no type — the author picks one (Binary, Date, …)
170
+ and it is recorded beside the value. Reading the schema first validated
171
+ a binary value as `auto`, which auto-detects from the value and accepts
172
+ anything, so the form said the field needed attention while the field
173
+ itself gave no reason. The form's own check already prefers the stored
174
+ type (`getOptionFieldStorageType`); this is the same precedence. */
175
+ const schemaType = optionSchema?.ui_type as string;
176
+ const storedType = option.type as string;
177
+ const typeToValidate =
178
+ isUntypedOptionType(schemaType) && storedType ? storedType : schemaType || storedType;
179
+
92
180
  const validationData = validateFieldWithResult(
93
- getType((optionSchema?.ui_type as string) || (option.type as string)),
181
+ getType(typeToValidate),
94
182
  option.value,
95
183
  {
96
184
  has_to_have_value: true,
@@ -109,7 +197,7 @@ export const getOptionFieldMessages = ({
109
197
  });
110
198
  }
111
199
  } else {
112
- if (optionSchema?.required) {
200
+ if (optionSchema?.required && !untouched) {
113
201
  result.push({ label: 'This field is required', intent: 'danger' });
114
202
  }
115
203
 
@@ -134,14 +222,16 @@ export const getOptionFieldMessages = ({
134
222
  optionSchema?.depends_on &&
135
223
  !hasAllDependenciesFullfilled(optionSchema.depends_on, allOptions, schema)
136
224
  ) {
137
- const dependsOn = flatten(optionSchema.depends_on)
138
- .filter((dep: string) => !!schema[dep])
139
- .map((dep: string) => schema[dep].display_name || dep)
140
- .map((dep: string) => `"${dep}"`)
141
- .join(', ');
225
+ const dependsOn = describeDependencies(optionSchema.depends_on, schema);
142
226
 
143
227
  result.push({
144
- label: `This field is disabled because some dependencies are not fulfilled: ${dependsOn}`,
228
+ label:
229
+ dependsOn ?
230
+ `This field is disabled because some dependencies are not fulfilled: ${dependsOn}`
231
+ // Nothing nameable: every entry pointed at a field this schema does
232
+ // not contain. A sentence that ends in a colon and then stops reads
233
+ // as a rendering fault rather than as a fact about the form.
234
+ : 'This field is disabled because some dependencies are not fulfilled',
145
235
  intent: 'warning',
146
236
  });
147
237
  }
@@ -155,10 +245,11 @@ export const OptionFieldMessages = ({
155
245
  name,
156
246
  allOptions,
157
247
  getType,
248
+ untouched,
158
249
  }: IOptionFieldMessagesProps) => {
159
250
  const messages: IReqoreTagProps[] = useMemo(
160
- () => getOptionFieldMessages({ schema, option, name, allOptions, getType }),
161
- [JSON.stringify(schema), JSON.stringify(option), JSON.stringify(allOptions), name]
251
+ () => getOptionFieldMessages({ schema, option, name, allOptions, getType, untouched }),
252
+ [JSON.stringify(schema), JSON.stringify(option), JSON.stringify(allOptions), name, untouched]
162
253
  );
163
254
 
164
255
  if (!size(messages)) {
@@ -24,6 +24,7 @@ import {
24
24
  fieldLabel,
25
25
  findAllowedValueOption,
26
26
  formatOptionValue,
27
+ getAllowedValueIcon,
27
28
  getAllowedValueImage,
28
29
  isCodeField,
29
30
  orderedKeys,
@@ -356,7 +357,11 @@ const FieldValue = ({
356
357
  const formatted = formatOptionValue(field, fieldSchema);
357
358
  // An allowed value that carries a logo shows it here too, exactly as the row
358
359
  // above does — the preview is the same value, so it gets the same treatment.
360
+ // An option marked with an ICON rather than a logo gets the same, for the same
361
+ // reason: the mark is how a list of bare words is told apart, and dropping it
362
+ // here would make the preview disagree with the row it previews.
359
363
  const image = getAllowedValueImage(field.value, fieldSchema);
364
+ const icon = getAllowedValueIcon(field.value, fieldSchema);
360
365
 
361
366
  // Data reads in mono; a chosen label does not.
362
367
  //
@@ -387,6 +392,12 @@ const FieldValue = ({
387
392
  size='14px'
388
393
  style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
389
394
  />
395
+ : icon ?
396
+ <ReqoreIcon
397
+ icon={icon as any}
398
+ size='14px'
399
+ style={{ flexShrink: 0, marginRight: 6, verticalAlign: 'middle' }}
400
+ />
390
401
  : null}
391
402
  {isNavigableUrl(field.value) ?
392
403
  // The address IS the link — it used to render as inert text with a
@@ -35,7 +35,7 @@ export interface ICompactToolbarContext {
35
35
  // Completion meter
36
36
  invalidCount: number;
37
37
  attentionCount: number;
38
- completion: { set: number; total: number; pct: number };
38
+ completion: { set: number; done: number; total: number; pct: number };
39
39
  // Invalid-fields banner (pinned in the sticky header)
40
40
  showInvalidOnly: boolean;
41
41
  onToggleInvalidOnly: () => void;
@@ -316,6 +316,31 @@ export const getAllowedValueImage = (
316
316
  return (match?.image as string) || undefined;
317
317
  };
318
318
 
319
+ /**
320
+ * The selected option's ICON, when it carries one and no logo — for the value
321
+ * cell.
322
+ *
323
+ * The same idea as {@link getAllowedValueImage} and read for the same reason:
324
+ * an option's mark belongs beside the value it names. Only images were rendered,
325
+ * so a picker whose entries are distinguished by ICON — an interface-kind list
326
+ * where every entry is a bare word, `Service`, `Job`, `Qog` — showed each mark
327
+ * while choosing and then dropped it the moment the row went read-only, which is
328
+ * the surface a reader spends the most time on.
329
+ *
330
+ * The image wins where an option has both: a logo is the more specific mark, and
331
+ * an icon standing behind it is a fallback rather than a second thing to draw.
332
+ */
333
+ export const getAllowedValueIcon = (
334
+ value: unknown,
335
+ schema?: TQorusFormFieldSchema
336
+ ): string | undefined => {
337
+ const match = findAllowedOption(value, schema);
338
+ if (!match || match.image) {
339
+ return undefined;
340
+ }
341
+ return (match.icon as string) || undefined;
342
+ };
343
+
319
344
  /** Clamp an RGB channel to a 0–255 integer. */
320
345
  const clampChannel = (n: number): number => Math.max(0, Math.min(255, Math.round(n)));
321
346
  const toHexPart = (n: number): string => clampChannel(n).toString(16).padStart(2, '0');
@@ -556,6 +581,36 @@ export const summariseMarkdown = (value: unknown): string => {
556
581
  return capSummary(prose);
557
582
  };
558
583
 
584
+ /**
585
+ * The expression AST a field holds, whichever shape it arrived in.
586
+ *
587
+ * An expression arrives in TWO shapes, and only the flat one used to be
588
+ * checked. At runtime the editor writes the flag onto the option
589
+ * (`{ is_expression: true, value: ast }`); a value read back from a saved draft
590
+ * carries the envelope NESTED (`{ value: { is_expression: true, value: ast } }`)
591
+ * with no flag on the option at all. Missing the nested shape sent a reloaded
592
+ * expression to the generic formatter, which printed the AST — the row showed
593
+ * `is_expression true / value / exp + / args 1 2` where the expression should
594
+ * be.
595
+ *
596
+ * Exported because the SUMMARY is not the only place that has to know. The AST
597
+ * is how an expression is stored, not what it is, so nothing may draw it as
598
+ * data: `CompactRow` asks this before deciding a hash-shaped value has earned a
599
+ * structured inset. Answering it in one place is what keeps the row's two
600
+ * halves — the line and the inset under it — talking about the same value.
601
+ */
602
+ export const getExpressionAst = (option?: {
603
+ is_expression?: boolean;
604
+ value?: unknown;
605
+ }): IExpressionValue | undefined =>
606
+ option?.is_expression ? (option?.value as IExpressionValue | undefined)
607
+ : (
608
+ (option?.value as { is_expression?: boolean; value?: IExpressionValue } | undefined)
609
+ ?.is_expression
610
+ ) ?
611
+ (option?.value as { value?: IExpressionValue }).value
612
+ : undefined;
613
+
559
614
  export const formatOptionValue = (
560
615
  option?: IQorusFormField,
561
616
  schema?: TQorusFormFieldSchema
@@ -572,11 +627,13 @@ export const formatOptionValue = (
572
627
  return '••••••';
573
628
  }
574
629
 
575
- if (option?.is_expression) {
630
+ const expressionAst = getExpressionAst(option);
631
+
632
+ if (expressionAst !== undefined) {
576
633
  // Offline summary of the {exp,args} AST already in the form value — the
577
634
  // same client-side renderer the editor's "Explain" seam falls back to when
578
635
  // the LSP is unreachable. The drill-in editor shows the canonical DPQL.
579
- return renderExpressionToText(option?.value as IExpressionValue | undefined) || 'Expression';
636
+ return renderExpressionToText(expressionAst) || 'Expression';
580
637
  }
581
638
 
582
639
  // schema-definition is stored as a hash envelope; summarise it as the schema
@@ -792,7 +849,10 @@ export const getOptionGroupLabel = (
792
849
  /** Completion summary for the read-first progress meter. */
793
850
  export interface IReadFirstCompletion {
794
851
  total: number;
852
+ /** Fields holding a value, whether or not anything is wrong with it. */
795
853
  set: number;
854
+ /** Fields holding a value that nothing is flagging — what the meter fills. */
855
+ done: number;
796
856
  pct: number;
797
857
  }
798
858
 
@@ -831,14 +891,33 @@ export const getReadFirstBucket = (status: TReadFirstStatus): 'attention' | 'set
831
891
 
832
892
  /** Count how many of the shown options have a value set, for the progress meter. */
833
893
  export const getReadFirstCompletion = (
834
- shownOptions: Record<string, IQorusFormField | undefined> = {}
894
+ shownOptions: Record<string, IQorusFormField | undefined> = {},
895
+ /**
896
+ * Whether the form flags this field as needing attention. Optional: without
897
+ * it the meter falls back to counting values alone, which is what a caller
898
+ * with no bucketing of its own can honestly say.
899
+ */
900
+ needsAttention?: (name: string) => boolean
835
901
  ): IReadFirstCompletion => {
836
902
  const names = Object.keys(shownOptions);
837
903
  const total = names.length;
838
- const set = names.filter((name) => !isOptionValueEmpty(shownOptions[name]?.value)).length;
839
- const pct = total ? Math.round((set / total) * 100) : 0;
840
-
841
- return { total, set, pct };
904
+ const hasValue = (name: string) => !isOptionValueEmpty(shownOptions[name]?.value);
905
+ const set = names.filter(hasValue).length;
906
+ /* "Set" and "needs attention" are not opposites. A field can hold a value and
907
+ still be wrong — one that fails validation, or one the host has flagged
908
+ because something INSIDE it is unfinished — and counting those as progress
909
+ is how a form comes to report 100% while the box beneath it says a field
910
+ needs attention.
911
+
912
+ It also cost the meter its amber run: the bar draws attention from `set%`
913
+ onward, so a field counted in both pushed that run past the right edge,
914
+ where `overflow: hidden` swallowed it. The one visual signal that something
915
+ was outstanding was the one thing the double-count hid. */
916
+ const done =
917
+ needsAttention ? names.filter((name) => hasValue(name) && !needsAttention(name)).length : set;
918
+ const pct = total ? Math.round((done / total) * 100) : 0;
919
+
920
+ return { total, set, done, pct };
842
921
  };
843
922
 
844
923
  /** What the "first field to fix" selector needs to know about one field.
@@ -1,4 +1,16 @@
1
1
  import { TQorusType } from '@qoretechnologies/ts-toolkit';
2
+ import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
3
+
4
+ /**
5
+ * A field schema that carries a template list of its own.
6
+ *
7
+ * `templates` is deliberately NOT added to `TQorusFormFieldSchema`: that type is
8
+ * owned by ts-toolkit and shared with consumers that have no notion of a form
9
+ * engine, so the capability is read through this narrow view instead of widening
10
+ * the shared type from here. A schema that sets it is offering a list scoped to
11
+ * one field rather than to the whole form.
12
+ */
13
+ export type TFieldWithOwnTemplates = { templates?: IReqoreFormTemplates };
2
14
 
3
15
  /**
4
16
  * A schema entry carries two type-ish keys with different jobs:
@@ -0,0 +1,159 @@
1
+ // Copyright 2026 Qore Technologies, s.r.o.
2
+ import { IReqoreDropdownItem } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+
14
+ /**
15
+ * SEAM (reqraft): an editor publishes its own actions into the row's ⋮ menu.
16
+ *
17
+ * A row already renders one overflow menu — Edit fullscreen, Remove field, and
18
+ * any `optionActions` the host injected — and the comment on it has always said
19
+ * why: *"this row already has a menu, so they reuse it rather than adding a
20
+ * second one beside it."* An EDITOR could not reach that menu, so an editor
21
+ * with affordances of its own had no choice but to draw a second ⋮ inside the
22
+ * value cell. The IDE's template field does exactly that, and the two menus
23
+ * ended up side by side on one control, at slightly different widths.
24
+ *
25
+ * `optionActions` does not solve it: that seam is host configuration, resolved
26
+ * from the option's name and schema before the editor exists, so it cannot know
27
+ * what the editor can offer *now* — whether its templates have loaded, whether
28
+ * the value is already an expression, which custom items the field was given.
29
+ *
30
+ * ## Why registration is keyed, and by whom
31
+ *
32
+ * Menu items carry `onClick` handlers, so they are a new array on every render
33
+ * and cannot be compared by value. An editor therefore publishes a `key`
34
+ * alongside them: a primitive describing WHICH items it is currently offering
35
+ * (`'expression,template'`). The row re-renders only when a publisher's key
36
+ * changes, so a re-rendering editor cannot drive the row into a state loop.
37
+ *
38
+ * Each registration also belongs to ONE publisher. A single shared slot let
39
+ * two publishers in the same row — an expression builder's operands — overwrite
40
+ * each other's key on every render, which re-rendered the row and both of them,
41
+ * which published again: a synchronous render loop that froze the page. And a
42
+ * slot that outlived its editor kept that editor's handlers after it unmounted,
43
+ * so a row closed and reopened offered items that did nothing. Registrations
44
+ * are therefore per publisher, withdrawn on unmount, and a click always reaches
45
+ * the handler from the publisher's latest render.
46
+ *
47
+ * Publish with `useRowMenuPublisher`; provide with `useRowMenuRegistry`.
48
+ */
49
+ export interface IRowMenuRegistration {
50
+ /**
51
+ * Publish a publisher's menu items into the containing row's ⋮.
52
+ *
53
+ * @param publisherId identifies the publishing editor (one per mounted editor)
54
+ * @param key a primitive describing which items are being offered; the row
55
+ * re-renders only when it changes
56
+ * @param items the items to merge, or an empty array to contribute none
57
+ */
58
+ registerRowMenuItems: (publisherId: string, key: string, items: IReqoreDropdownItem[]) => void;
59
+ /** Withdraw a publisher's items — when it unmounts or leaves the row. */
60
+ unregisterRowMenuItems: (publisherId: string) => void;
61
+ }
62
+
63
+ /**
64
+ * Absent outside a compact row — the classic (non-compact) render path has no
65
+ * row menu to merge into, and an editor there keeps drawing its own control.
66
+ */
67
+ export const RowMenuContext = createContext<IRowMenuRegistration | undefined>(undefined);
68
+
69
+ /**
70
+ * The row-menu channel for the row this editor is inside, or `undefined` when
71
+ * there is no row menu to publish into.
72
+ *
73
+ * An editor that finds it should render no menu of its own; one that does not
74
+ * must keep rendering its own, or its actions become unreachable.
75
+ */
76
+ export const useRowMenu = (): IRowMenuRegistration | undefined => useContext(RowMenuContext);
77
+
78
+ /**
79
+ * Publish `items` into `rowMenu` for as long as the calling editor is mounted.
80
+ * `rowMenu` is passed rather than read here so an editor can claim the channel
81
+ * and hide it from what it renders (see `TemplateField`).
82
+ */
83
+ export const useRowMenuPublisher = (
84
+ rowMenu: IRowMenuRegistration | undefined,
85
+ key: string,
86
+ items: IReqoreDropdownItem[]
87
+ ): void => {
88
+ const publisherId = useId();
89
+
90
+ useEffect(() => {
91
+ rowMenu?.registerRowMenuItems(publisherId, key, items);
92
+ }, [rowMenu, publisherId, key, items]);
93
+
94
+ useEffect(() => () => rowMenu?.unregisterRowMenuItems(publisherId), [rowMenu, publisherId]);
95
+ };
96
+
97
+ /**
98
+ * The row side of the channel: the registration to provide, and the items the
99
+ * row's ⋮ should list.
100
+ */
101
+ export const useRowMenuRegistry = (): {
102
+ rowMenu: IRowMenuRegistration;
103
+ items: IReqoreDropdownItem[];
104
+ } => {
105
+ // Which publishers are offering which item SET — the only state, so only a
106
+ // change of set re-renders the row.
107
+ const [keys, setKeys] = useState<Record<string, string>>({});
108
+ // Each publisher's items from its latest render, read at click time.
109
+ const latest = useRef<Record<string, IReqoreDropdownItem[]>>({});
110
+
111
+ const registerRowMenuItems = useCallback(
112
+ (publisherId: string, key: string, items: IReqoreDropdownItem[]) => {
113
+ latest.current[publisherId] = items;
114
+ setKeys((previous) =>
115
+ previous[publisherId] === key ? previous : { ...previous, [publisherId]: key }
116
+ );
117
+ },
118
+ []
119
+ );
120
+
121
+ const unregisterRowMenuItems = useCallback((publisherId: string) => {
122
+ delete latest.current[publisherId];
123
+ setKeys((previous) => {
124
+ if (!(publisherId in previous)) {
125
+ return previous;
126
+ }
127
+ const next = { ...previous };
128
+ delete next[publisherId];
129
+ return next;
130
+ });
131
+ }, []);
132
+
133
+ const rowMenu = useMemo(
134
+ () => ({ registerRowMenuItems, unregisterRowMenuItems }),
135
+ [registerRowMenuItems, unregisterRowMenuItems]
136
+ );
137
+
138
+ const items = useMemo(
139
+ () =>
140
+ Object.keys(keys).flatMap((publisherId) =>
141
+ (latest.current[publisherId] ?? []).map((item, index) =>
142
+ item.onClick
143
+ ? {
144
+ ...item,
145
+ // Same key, same item set, so the index still names this item;
146
+ // the handler is whatever the publisher rendered last.
147
+ onClick: ((...args: unknown[]) =>
148
+ (latest.current[publisherId]?.[index]?.onClick as
149
+ | ((...clickArgs: unknown[]) => unknown)
150
+ | undefined)?.(...args)) as IReqoreDropdownItem['onClick'],
151
+ }
152
+ : item
153
+ )
154
+ ),
155
+ [keys]
156
+ );
157
+
158
+ return { rowMenu, items };
159
+ };
@@ -0,0 +1,61 @@
1
+ // Copyright 2026 Qore Technologies, s.r.o.
2
+ /**
3
+ * Which options offer the per-type choices ("Integer", "Text", "Hash", …) in
4
+ * the row's menu.
5
+ *
6
+ * A field that pins no concrete type has to give the author some way to say
7
+ * what kind of value they mean, and the row menu is where that lives — the
8
+ * alternative, a standalone *"Please select data type"* control, replaces a
9
+ * typable field with a pick-only one.
10
+ *
11
+ * The rule was `ui_type === 'any'`, which is one of the three ways a schema
12
+ * says "untyped", so the choices went missing on the other two:
13
+ *
14
+ * - **`type: 'auto'` with no `ui_type` at all.** This is how the server
15
+ * declares an assertion's Expected Value, so that field never offered them.
16
+ * They appeared only inside an expression, where the OPERAND's schema does
17
+ * say `ui_type: 'any'` — so the same option offered them in the Visual view
18
+ * and not in the Text view, which reads as the menu losing items.
19
+ * - **A host's own untyped editor.** The IDE renders Expected Value through
20
+ * `test-reference`, a `ui_type` reqraft knows nothing about; the field is no
21
+ * less untyped for having a custom editor.
22
+ *
23
+ * `ui_type` is asked FIRST and on its own: a schema that names a concrete
24
+ * editor (`ui_type: 'string'`) has pinned the field even if its storage `type`
25
+ * is still `auto`.
26
+ */
27
+ import { IQorusFormField } from '@qoretechnologies/ts-toolkit';
28
+ import { UNTYPED_OPTION_TYPES } from '../../../helpers/optionUiTypes';
29
+
30
+ /**
31
+ * The spellings that mean "no concrete type" wherever they appear.
32
+ *
33
+ * Re-exported from the type vocabulary in `helpers/optionUiTypes`, which is
34
+ * where it now lives so non-component code can ask the same question.
35
+ */
36
+ export const BuiltInAnyLikeTypes: readonly string[] = UNTYPED_OPTION_TYPES;
37
+
38
+ /**
39
+ * Does this option leave its type up to the author?
40
+ *
41
+ * @param option the option's schema entry
42
+ * @param extraUiTypes host `ui_type` names that are untyped editors of their
43
+ * own — see `FormEngine`'s `anyLikeUiTypes`
44
+ */
45
+ export const offersTypeChoices = (
46
+ option: Partial<IQorusFormField> | undefined,
47
+ extraUiTypes?: string[]
48
+ ): boolean => {
49
+ if (!option) {
50
+ return false;
51
+ }
52
+
53
+ const uiType = (option as { ui_type?: string }).ui_type;
54
+
55
+ if (uiType) {
56
+ return BuiltInAnyLikeTypes.includes(uiType) || !!extraUiTypes?.includes(uiType);
57
+ }
58
+
59
+ // No `ui_type`: the storage type is the only thing that can pin the field.
60
+ return BuiltInAnyLikeTypes.includes((option as { type?: string }).type as string);
61
+ };