@qoretechnologies/reqraft 0.10.55-pr.124.g77dd26c → 0.10.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/.claude/CLAUDE.md +25 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
  4. package/README.md +1 -1
  5. package/design/FORM_ENGINE_COMPACT.md +34 -0
  6. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  7. package/design/FORM_ENGINE_FIELD_MIGRATION.md +155 -1
  8. package/design/IDE_INTEGRATION.md +13 -0
  9. package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
  10. package/design/SELECT_DIALOG_FLOOR.md +86 -0
  11. package/design/SMART_EDITOR_UX.md +21 -0
  12. package/design/UNAVAILABLE_ALLOWED_VALUES.md +226 -0
  13. package/dist/components/composer/Composer.d.ts +8 -3
  14. package/dist/components/composer/Composer.d.ts.map +1 -1
  15. package/dist/components/composer/Composer.js.map +1 -1
  16. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  17. package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
  18. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  19. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
  20. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  21. package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
  22. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  23. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  24. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  25. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  26. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  27. package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
  28. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  29. package/dist/components/dpqlEditor/dpqlTags.js +5 -1
  30. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  31. package/dist/components/dpqlEditor/index.d.ts +4 -0
  32. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  33. package/dist/components/dpqlEditor/index.js +5 -1
  34. package/dist/components/dpqlEditor/index.js.map +1 -1
  35. package/dist/components/dpqlEditor/types.d.ts +42 -2
  36. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  37. package/dist/components/dpqlEditor/types.js +0 -4
  38. package/dist/components/dpqlEditor/types.js.map +1 -1
  39. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  40. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  41. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  42. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  43. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  44. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  45. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  46. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  47. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  48. package/dist/components/form/engine/CompactRow.js +414 -128
  49. package/dist/components/form/engine/CompactRow.js.map +1 -1
  50. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  51. package/dist/components/form/engine/CompactToolbar.js +5 -2
  52. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  53. package/dist/components/form/engine/FormEngine.d.ts +273 -22
  54. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  55. package/dist/components/form/engine/FormEngine.js +669 -192
  56. package/dist/components/form/engine/FormEngine.js.map +1 -1
  57. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  58. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  59. package/dist/components/form/engine/FormFieldsSkeleton.js +41 -0
  60. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  61. package/dist/components/form/engine/OptionFieldMessages.d.ts +108 -2
  62. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  63. package/dist/components/form/engine/OptionFieldMessages.js +203 -13
  64. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  65. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  66. package/dist/components/form/engine/_structuredData/SchemaDataView.js +68 -5
  67. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  68. package/dist/components/form/engine/compactRowContext.d.ts +15 -1
  69. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  70. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  71. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  72. package/dist/components/form/engine/compactRowStyles.js +55 -29
  73. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  74. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  75. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  76. package/dist/components/form/engine/optionActions.d.ts +37 -1
  77. package/dist/components/form/engine/optionActions.d.ts.map +1 -1
  78. package/dist/components/form/engine/optionActions.js +71 -1
  79. package/dist/components/form/engine/optionActions.js.map +1 -1
  80. package/dist/components/form/engine/optionsContext.d.ts +22 -0
  81. package/dist/components/form/engine/optionsContext.d.ts.map +1 -0
  82. package/dist/components/form/engine/optionsContext.js +20 -0
  83. package/dist/components/form/engine/optionsContext.js.map +1 -0
  84. package/dist/components/form/engine/readFirst.d.ts +42 -1
  85. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  86. package/dist/components/form/engine/readFirst.js +76 -8
  87. package/dist/components/form/engine/readFirst.js.map +1 -1
  88. package/dist/components/form/engine/rendererTypes.d.ts +42 -3
  89. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  90. package/dist/components/form/engine/rendererTypes.js +3 -1
  91. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  92. package/dist/components/form/engine/rowFocus.d.ts +30 -0
  93. package/dist/components/form/engine/rowFocus.d.ts.map +1 -0
  94. package/dist/components/form/engine/rowFocus.js +93 -0
  95. package/dist/components/form/engine/rowFocus.js.map +1 -0
  96. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  97. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  98. package/dist/components/form/engine/rowMenuContext.js +103 -0
  99. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  100. package/dist/components/form/engine/rowOpenPicker.d.ts +27 -0
  101. package/dist/components/form/engine/rowOpenPicker.d.ts.map +1 -0
  102. package/dist/components/form/engine/rowOpenPicker.js +30 -0
  103. package/dist/components/form/engine/rowOpenPicker.js.map +1 -0
  104. package/dist/components/form/engine/typeChoices.d.ts +42 -0
  105. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  106. package/dist/components/form/engine/typeChoices.js +33 -0
  107. package/dist/components/form/engine/typeChoices.js.map +1 -0
  108. package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
  109. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
  110. package/dist/components/form/expressions/DpqlRendering.js +53 -0
  111. package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
  112. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  113. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  114. package/dist/components/form/expressions/ExpressionField.js +203 -45
  115. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  116. package/dist/components/form/expressions/argumentPresence.d.ts +84 -0
  117. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  118. package/dist/components/form/expressions/argumentPresence.js +217 -0
  119. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  120. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  121. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  122. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  123. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  124. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  125. package/dist/components/form/expressions/builder/index.js +21 -65
  126. package/dist/components/form/expressions/builder/index.js.map +1 -1
  127. package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
  128. package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
  129. package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
  130. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  131. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  132. package/dist/components/form/expressions/mockExpressions.js +8 -4
  133. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  134. package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
  135. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  136. package/dist/components/form/expressions/renderExpressionToText.js +95 -19
  137. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  138. package/dist/components/form/expressions/types.d.ts +17 -2
  139. package/dist/components/form/expressions/types.d.ts.map +1 -1
  140. package/dist/components/form/expressions/useExpressions.d.ts.map +1 -1
  141. package/dist/components/form/expressions/useExpressions.js +5 -2
  142. package/dist/components/form/expressions/useExpressions.js.map +1 -1
  143. package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
  144. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  145. package/dist/components/form/expressions/useRenderExpression.js +50 -126
  146. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  147. package/dist/components/form/fields/Field.d.ts +21 -0
  148. package/dist/components/form/fields/Field.d.ts.map +1 -1
  149. package/dist/components/form/fields/Field.js +54 -8
  150. package/dist/components/form/fields/Field.js.map +1 -1
  151. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  152. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  153. package/dist/components/form/fields/allowed-values/AllowedValues.js +52 -8
  154. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  155. package/dist/components/form/fields/auto/AutoFormField.d.ts +7 -1
  156. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  157. package/dist/components/form/fields/auto/AutoFormField.js +91 -28
  158. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  159. package/dist/components/form/fields/date/Date.d.ts +1 -1
  160. package/dist/components/form/fields/date/Date.d.ts.map +1 -1
  161. package/dist/components/form/fields/date/Date.js +16 -2
  162. package/dist/components/form/fields/date/Date.js.map +1 -1
  163. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  164. package/dist/components/form/fields/long-string/LongString.js +14 -2
  165. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  166. package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
  167. package/dist/components/form/fields/multi-select/MultiSelectFormField.js +50 -4
  168. package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
  169. package/dist/components/form/fields/number/Number.d.ts +12 -1
  170. package/dist/components/form/fields/number/Number.d.ts.map +1 -1
  171. package/dist/components/form/fields/number/Number.js +6 -3
  172. package/dist/components/form/fields/number/Number.js.map +1 -1
  173. package/dist/components/form/fields/rich-text/RichText.d.ts +47 -2
  174. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  175. package/dist/components/form/fields/rich-text/RichText.js +153 -12
  176. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  177. package/dist/components/form/fields/select/Select.d.ts.map +1 -1
  178. package/dist/components/form/fields/select/Select.js +120 -20
  179. package/dist/components/form/fields/select/Select.js.map +1 -1
  180. package/dist/components/form/fields/select/SelectCollection.d.ts +166 -9
  181. package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
  182. package/dist/components/form/fields/select/SelectCollection.js +200 -7
  183. package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
  184. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  185. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  186. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  187. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  188. package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
  189. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  190. package/dist/components/form/fields/template/TemplateField.js +606 -58
  191. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  192. package/dist/components/form/fields/template/TemplateText.d.ts +36 -0
  193. package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -0
  194. package/dist/components/form/fields/template/TemplateText.js +80 -0
  195. package/dist/components/form/fields/template/TemplateText.js.map +1 -0
  196. package/dist/components/form/index.d.ts +2 -0
  197. package/dist/components/form/index.d.ts.map +1 -1
  198. package/dist/components/form/index.js +4 -0
  199. package/dist/components/form/index.js.map +1 -1
  200. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  201. package/dist/components/smartEditor/SmartEditor.js +29 -8
  202. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  203. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  204. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  205. package/dist/components/smartEditor/useLspSession.js +16 -2
  206. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  207. package/dist/components/supportTicket/TicketReplyBox.d.ts +8 -4
  208. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  209. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  210. package/dist/helpers/common.d.ts.map +1 -1
  211. package/dist/helpers/common.js +7 -3
  212. package/dist/helpers/common.js.map +1 -1
  213. package/dist/helpers/dpqlDetection.d.ts +58 -0
  214. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  215. package/dist/helpers/dpqlDetection.js +164 -0
  216. package/dist/helpers/dpqlDetection.js.map +1 -0
  217. package/dist/helpers/optionUiTypes.d.ts +31 -3
  218. package/dist/helpers/optionUiTypes.d.ts.map +1 -1
  219. package/dist/helpers/optionUiTypes.js +36 -1
  220. package/dist/helpers/optionUiTypes.js.map +1 -1
  221. package/dist/helpers/options.d.ts +57 -0
  222. package/dist/helpers/options.d.ts.map +1 -1
  223. package/dist/helpers/options.js +97 -1
  224. package/dist/helpers/options.js.map +1 -1
  225. package/dist/helpers/templateItems.d.ts +47 -0
  226. package/dist/helpers/templateItems.d.ts.map +1 -0
  227. package/dist/helpers/templateItems.js +88 -0
  228. package/dist/helpers/templateItems.js.map +1 -0
  229. package/dist/helpers/templateText.d.ts +80 -0
  230. package/dist/helpers/templateText.d.ts.map +1 -0
  231. package/dist/helpers/templateText.js +291 -0
  232. package/dist/helpers/templateText.js.map +1 -0
  233. package/dist/helpers/templates.d.ts +30 -12
  234. package/dist/helpers/templates.d.ts.map +1 -1
  235. package/dist/helpers/templates.js +91 -33
  236. package/dist/helpers/templates.js.map +1 -1
  237. package/dist/helpers/validations.d.ts +35 -8
  238. package/dist/helpers/validations.d.ts.map +1 -1
  239. package/dist/helpers/validations.js +162 -25
  240. package/dist/helpers/validations.js.map +1 -1
  241. package/dist/hooks/useArgSchema.d.ts +2 -1
  242. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  243. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  244. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  245. package/dist/hooks/useFetch/useFetch.js +47 -9
  246. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  247. package/dist/hooks/useTemplates.d.ts +20 -0
  248. package/dist/hooks/useTemplates.d.ts.map +1 -1
  249. package/dist/hooks/useTemplates.js +34 -2
  250. package/dist/hooks/useTemplates.js.map +1 -1
  251. package/dist/index.d.ts +2 -1
  252. package/dist/index.d.ts.map +1 -1
  253. package/dist/index.js +5 -1
  254. package/dist/index.js.map +1 -1
  255. package/dist/utils/fetch.d.ts +151 -1
  256. package/dist/utils/fetch.d.ts.map +1 -1
  257. package/dist/utils/fetch.js +256 -44
  258. package/dist/utils/fetch.js.map +1 -1
  259. package/dist/utils/lspClient.d.ts +6 -0
  260. package/dist/utils/lspClient.d.ts.map +1 -1
  261. package/dist/utils/lspClient.js +62 -11
  262. package/dist/utils/lspClient.js.map +1 -1
  263. package/dist/utils/websocket.d.ts +6 -0
  264. package/dist/utils/websocket.d.ts.map +1 -1
  265. package/dist/utils/websocket.js +15 -7
  266. package/dist/utils/websocket.js.map +1 -1
  267. package/package.json +10 -6
  268. package/src/components/composer/Composer.tsx +7 -2
  269. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  270. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  271. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  272. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  273. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  274. package/src/components/dpqlEditor/index.ts +4 -0
  275. package/src/components/dpqlEditor/types.ts +38 -2
  276. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  277. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  278. package/src/components/form/engine/CompactRow.tsx +487 -160
  279. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  280. package/src/components/form/engine/FormEngine.stories.tsx +1269 -211
  281. package/src/components/form/engine/FormEngine.tsx +922 -270
  282. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +331 -0
  283. package/src/components/form/engine/FormFieldsSkeleton.tsx +77 -0
  284. package/src/components/form/engine/OptionFieldMessages.tsx +300 -14
  285. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +94 -3
  286. package/src/components/form/engine/compactRowContext.ts +15 -6
  287. package/src/components/form/engine/compactRowStyles.ts +56 -2
  288. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  289. package/src/components/form/engine/optionActions.ts +89 -1
  290. package/src/components/form/engine/optionsContext.ts +24 -0
  291. package/src/components/form/engine/readFirst.ts +88 -8
  292. package/src/components/form/engine/rendererTypes.ts +48 -4
  293. package/src/components/form/engine/rowFocus.ts +101 -0
  294. package/src/components/form/engine/rowMenuContext.ts +159 -0
  295. package/src/components/form/engine/rowOpenPicker.ts +27 -0
  296. package/src/components/form/engine/typeChoices.ts +63 -0
  297. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  298. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  299. package/src/components/form/expressions/ExpressionField.tsx +269 -55
  300. package/src/components/form/expressions/argumentPresence.ts +213 -0
  301. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +38 -21
  302. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  303. package/src/components/form/expressions/builder/index.tsx +39 -81
  304. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  305. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  306. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  307. package/src/components/form/expressions/mockExpressions.ts +8 -4
  308. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  309. package/src/components/form/expressions/types.ts +17 -2
  310. package/src/components/form/expressions/useExpressions.ts +5 -1
  311. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  312. package/src/components/form/fields/Field.tsx +102 -20
  313. package/src/components/form/fields/allowed-values/AllowedValues.tsx +106 -44
  314. package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
  315. package/src/components/form/fields/auto/AutoFormField.tsx +97 -21
  316. package/src/components/form/fields/date/Date.tsx +17 -1
  317. package/src/components/form/fields/long-string/LongString.tsx +15 -3
  318. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +65 -6
  319. package/src/components/form/fields/number/Number.tsx +25 -0
  320. package/src/components/form/fields/rich-text/RichText.tsx +248 -30
  321. package/src/components/form/fields/select/Select.stories.tsx +392 -0
  322. package/src/components/form/fields/select/Select.tsx +164 -27
  323. package/src/components/form/fields/select/SelectCollection.tsx +350 -6
  324. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  325. package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
  326. package/src/components/form/fields/template/TemplateField.tsx +825 -131
  327. package/src/components/form/fields/template/TemplateText.tsx +115 -0
  328. package/src/components/form/index.tsx +4 -0
  329. package/src/components/log/Log.stories.tsx +2 -1
  330. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +3 -3
  331. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  332. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  333. package/src/components/smartEditor/useLspSession.ts +21 -3
  334. package/src/components/supportTicket/TicketReplyBox.tsx +8 -4
  335. package/src/components/ticketThread/TicketThread.stories.tsx +1 -0
  336. package/src/helpers/common.ts +7 -3
  337. package/src/helpers/dpqlDetection.ts +209 -0
  338. package/src/helpers/optionUiTypes.ts +38 -0
  339. package/src/helpers/options.ts +96 -0
  340. package/src/helpers/templateItems.ts +81 -0
  341. package/src/helpers/templateText.ts +350 -0
  342. package/src/helpers/templates.ts +94 -41
  343. package/src/helpers/validations.ts +203 -51
  344. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  345. package/src/hooks/useFetch/useFetch.tsx +53 -7
  346. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  347. package/src/hooks/useTemplates.ts +41 -0
  348. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  349. package/src/index.tsx +14 -1
  350. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  351. package/src/stories/storyNetwork.ts +92 -0
  352. package/src/utils/fetch.ts +311 -21
  353. package/src/utils/lspClient.ts +68 -11
  354. package/src/utils/websocket.ts +20 -9
  355. package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts +0 -73
  356. package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts.map +0 -1
  357. package/dist/components/form/engine/__fixtures__/basicFormOptions.js +0 -317
  358. package/dist/components/form/engine/__fixtures__/basicFormOptions.js.map +0 -1
  359. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts +0 -41
  360. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts.map +0 -1
  361. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js +0 -45
  362. package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js.map +0 -1
  363. package/dist/components/form/expressions/dpqlMockLsp.d.ts +0 -3
  364. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +0 -1
  365. package/dist/components/form/expressions/dpqlMockLsp.js +0 -91
  366. package/dist/components/form/expressions/dpqlMockLsp.js.map +0 -1
  367. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +0 -2
  368. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +0 -1
  369. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +0 -9
  370. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +0 -1
  371. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +0 -55
  372. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +0 -1
  373. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +0 -126
  374. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +0 -1
  375. package/dist/stories/Data/fileFieldArgSchema.json +0 -86
@@ -1,8 +1,14 @@
1
+ import { ReqoreButton, ReqoreP } from '@qoretechnologies/reqore';
1
2
  import { StoryObj } from '@storybook/react-vite';
2
3
  import { expect, fireEvent, waitFor, within } from 'storybook/test';
3
4
  import { useState } from 'react';
4
5
  import { StoryMeta } from '../../../../types';
5
6
  import { SelectFormField } from './Select';
7
+ import {
8
+ SELECT_DIALOG_MIN_HEIGHT,
9
+ SELECT_DIALOG_MIN_WIDTH,
10
+ UNAVAILABLE_ITEM_CLASS,
11
+ } from './SelectCollection';
6
12
 
7
13
  const meta = {
8
14
  component: SelectFormField,
@@ -478,3 +484,389 @@ export const CreatableOffersToCreate: Story = {
478
484
  );
479
485
  },
480
486
  };
487
+
488
+ // ── per-item row affordances ──────────────────────────────────────────────
489
+
490
+ /**
491
+ * A caller's row control, as a component the item NAMES rather than one it
492
+ * renders. Items travel through `JSON.stringify(items)` memo keys, and a React
493
+ * element is circular through its fibre — `{ as, props }` is what survives.
494
+ */
495
+ const ExplainThisItem = ({ explanation }: { explanation?: string }) => (
496
+ <ReqoreButton
497
+ size='tiny'
498
+ minimal
499
+ flat
500
+ compact
501
+ fixed
502
+ icon='QuestionLine'
503
+ className='select-item-explanation'
504
+ aria-label='What this is'
505
+ tooltip={{ handler: 'click', content: explanation, maxWidth: '360px' }}
506
+ // Reading is not choosing: the whole row is one click target.
507
+ onClick={(event) => event.stopPropagation()}
508
+ />
509
+ );
510
+
511
+ /** Whatever a caller wants inside the row's own hover. */
512
+ const ItemProse = ({ text }: { text?: string }) => <ReqoreP>{text}</ReqoreP>;
513
+
514
+ const EXPLAINED_ITEMS = [
515
+ 'Coverage: process',
516
+ 'Coverage: runtime',
517
+ 'Event: posted',
518
+ 'Value: equals',
519
+ 'Value: partial match',
520
+ 'String: matches regex',
521
+ ].map((display_name) => ({
522
+ display_name,
523
+ value: display_name,
524
+ short_desc: `What ${display_name} checks, in one line.`,
525
+ desc: `What ${display_name} checks, in one line.`,
526
+ title_actions: [{ as: ExplainThisItem, props: { explanation: `All of what ${display_name} is for.` } }],
527
+ tooltip: {
528
+ handler: 'hover' as const,
529
+ delay: 0,
530
+ placement: 'bottom-start' as const,
531
+ maxWidth: '360px',
532
+ content: { as: ItemProse, props: { text: `All of what ${display_name} is for.` } },
533
+ },
534
+ }));
535
+
536
+ const openTheCollection = async (canvasElement: HTMLElement) => {
537
+ await fireEvent.click(within(canvasElement).getByRole('button'));
538
+ await waitFor(
539
+ () => expect(document.querySelector('.reqraft-select-dialog .reqore-collection-item')).toBeTruthy(),
540
+ { timeout: 5000 }
541
+ );
542
+ };
543
+
544
+ const firstRow = () =>
545
+ document.querySelector('.reqraft-select-dialog .reqore-collection-item') as HTMLElement;
546
+
547
+ export const ItemActionsInTheTitleBar: Story = {
548
+ args: {
549
+ forceDropdown: false,
550
+ items: EXPLAINED_ITEMS,
551
+ },
552
+ parameters: {
553
+ docs: {
554
+ description: {
555
+ story:
556
+ "Each offered item carries a control through `title_actions`, which renders it in the row's title bar, inline with the name. An item's `actions` render in the row BODY under the description and cost every entry a line of its own — measured at 31px of a 73px row on a list of twenty-nine. Both seams exist; this is the one for a single icon on a long list.",
557
+ },
558
+ },
559
+ },
560
+ async play({ canvasElement }) {
561
+ await openTheCollection(canvasElement);
562
+ const row = firstRow();
563
+ /* `:scope >`: `Element.querySelector` matches against the whole document
564
+ and then keeps what is inside the element, so a bare
565
+ `.reqore-panel-content x` finds a control in the row's own TITLE — the
566
+ modal around the collection is a panel too. */
567
+ await expect(
568
+ row.querySelector(':scope > .reqore-panel-title .select-item-explanation')
569
+ ).toBeTruthy();
570
+ await expect(
571
+ row.querySelector(':scope > .reqore-panel-content .select-item-explanation')
572
+ ).toBeNull();
573
+ },
574
+ };
575
+
576
+ export const ItemTooltipOnTheRow: Story = {
577
+ args: {
578
+ forceDropdown: false,
579
+ items: EXPLAINED_ITEMS,
580
+ },
581
+ parameters: {
582
+ docs: {
583
+ description: {
584
+ story:
585
+ "The per-item `tooltip` puts content on the ROW's own hover: the whole entry is the target, and the list pays nothing in height for something that is not on screen until a pointer rests. The content is a `{ as, props }` descriptor rather than a node, so an item still survives the `JSON.stringify(items)` memo key it travels under.",
586
+ },
587
+ },
588
+ },
589
+ async play({ canvasElement }) {
590
+ await openTheCollection(canvasElement);
591
+ await fireEvent.mouseEnter(firstRow());
592
+ await waitFor(
593
+ () =>
594
+ expect(
595
+ document.querySelector('[data-popper-placement]')?.textContent
596
+ ).toContain('All of what Coverage: process is for.'),
597
+ { timeout: 5000 }
598
+ );
599
+ },
600
+ };
601
+
602
+ const REFUSED_ITEMS = [
603
+ {
604
+ display_name: 'Coverage: process',
605
+ value: 'process_coverage',
606
+ short_desc: 'Passes when the process you name ran code.',
607
+ desc: 'Passes when the process you name ran code.',
608
+ },
609
+ {
610
+ display_name: 'Step: result',
611
+ value: 'step_result',
612
+ short_desc: 'Passes when the named step reported this result.',
613
+ desc: 'Passes when the named step reported this result.',
614
+ disabled: true,
615
+ unavailable: {
616
+ intent: 'warning' as const,
617
+ content:
618
+ 'Unavailable because some dependencies are not fulfilled: "Interface Kind" must be "workflow"',
619
+ },
620
+ },
621
+ {
622
+ display_name: 'File: written',
623
+ value: 'file_written',
624
+ short_desc: 'Passes when the target wrote the file.',
625
+ desc: 'Passes when the target wrote the file.',
626
+ disabled: true,
627
+ messages: [
628
+ { intent: 'danger' as const, content: 'This sandbox denies the FILESYSTEM domain.' },
629
+ ],
630
+ },
631
+ {
632
+ display_name: 'Value: equals',
633
+ value: 'value_equals',
634
+ short_desc: 'Passes when the value is exactly equal.',
635
+ desc: 'Passes when the value is exactly equal.',
636
+ },
637
+ ];
638
+
639
+ export const UnavailableItemsSayWhy: Story = {
640
+ args: {
641
+ forceDropdown: false,
642
+ items: REFUSED_ITEMS,
643
+ },
644
+ parameters: {
645
+ docs: {
646
+ description: {
647
+ story:
648
+ "A value that cannot be picked is disabled and says why, never hidden — a choice that vanishes takes its own explanation with it. Two things refuse one and they render identically: the value's own `depends_on`, judged against the form it stands in (row 2), and a refusal the server had already decided as `disabled` with a message (row 3), for what only the server can know. The reason reads FIRST, above the description a `messages` entry would have landed under; the name is dimmed and the cursor says `not-allowed`; and the row keeps every pointer event, because reqore's `disabled` is `pointer-events: none` and would take the row's tooltip with it. What makes the row inert is having no click handler at all.",
649
+ },
650
+ },
651
+ },
652
+ async play({ canvasElement }) {
653
+ await openTheCollection(canvasElement);
654
+ const rows = () =>
655
+ [...document.querySelectorAll<HTMLElement>('.reqraft-select-dialog .reqore-collection-item')];
656
+ await waitFor(() => expect(rows().length).toBe(4), { timeout: 5000 });
657
+ /* By name, not by index: the collection sorts its rows, so a positional
658
+ lookup asserts about whichever row the sort happened to put there. */
659
+ const rowNamed = (label: string) =>
660
+ rows().find((row) =>
661
+ row.querySelector(':scope > .reqore-panel-title')?.textContent?.includes(label)
662
+ ) as HTMLElement;
663
+
664
+ await expect(rows().filter((row) => row.className.includes(UNAVAILABLE_ITEM_CLASS)).length).toBe(
665
+ 2
666
+ );
667
+
668
+ // The one thing this must never do: `DisabledElement` would make it `none`
669
+ // and take the row's tooltip with it.
670
+ const gated = rowNamed('Step: result');
671
+ await expect(getComputedStyle(gated).pointerEvents).toBe('auto');
672
+ await expect(getComputedStyle(gated).cursor).toBe('not-allowed');
673
+ await expect(getComputedStyle(rowNamed('Value: equals')).cursor).toBe('pointer');
674
+
675
+ // A served refusal is promoted out of `messages` and printed once, above
676
+ // the description rather than under it.
677
+ const text =
678
+ (rowNamed('File: written').querySelector(':scope > .reqore-panel-content') as HTMLElement)
679
+ .textContent || '';
680
+ await expect(text.indexOf('FILESYSTEM')).toBeGreaterThan(-1);
681
+ await expect(text.indexOf('FILESYSTEM')).toBeLessThan(text.indexOf('wrote the file'));
682
+ },
683
+ };
684
+
685
+ /** The dialog box itself — the element `re-resizable` sizes and drags. */
686
+ const selectDialog = () => document.querySelector('.reqraft-select-dialog') as HTMLElement;
687
+
688
+ const dialogSize = () => {
689
+ const rect = selectDialog().getBoundingClientRect();
690
+ return { width: Math.round(rect.width), height: Math.round(rect.height) };
691
+ };
692
+
693
+ /** Whether a part of the picker is whole inside the dialog, not cut off by it. */
694
+ const isWholeInsideTheDialog = (selector: string) => {
695
+ const box = selectDialog().getBoundingClientRect();
696
+ const part = selectDialog().querySelector(selector)?.getBoundingClientRect();
697
+
698
+ if (!part) {
699
+ return false;
700
+ }
701
+
702
+ return (
703
+ part.width > 0 &&
704
+ part.height > 0 &&
705
+ part.left >= box.left - 0.5 &&
706
+ part.right <= box.right + 0.5 &&
707
+ part.top >= box.top - 0.5 &&
708
+ part.bottom <= box.bottom + 0.5
709
+ );
710
+ };
711
+
712
+ /**
713
+ * Drag one of the dialog's resize handles, found by the cursor it sets.
714
+ *
715
+ * The first `col-resize` is the right edge and the first `row-resize` the top
716
+ * one, so a negative dx and a positive dy both shrink. The pointer is kept
717
+ * inside the box because the reqore we pin still clips the drawer, which
718
+ * amputates the outer half of every handle.
719
+ */
720
+ const dragTheDialogBy = async (cursor: string, dx: number, dy: number) => {
721
+ const box = selectDialog();
722
+ const handle = [...box.querySelectorAll<HTMLElement>('div')].find((el) =>
723
+ (el.getAttribute('style') || '').includes(`cursor: ${cursor}`)
724
+ );
725
+ await expect(handle).toBeTruthy();
726
+
727
+ const rect = handle!.getBoundingClientRect();
728
+ const boxRect = box.getBoundingClientRect();
729
+ const x = Math.min(Math.max(rect.left + rect.width / 2, boxRect.left + 2), boxRect.right - 2);
730
+ const y = Math.min(Math.max(rect.top + rect.height / 2, boxRect.top + 2), boxRect.bottom - 2);
731
+
732
+ fireEvent.mouseDown(handle!, { clientX: x, clientY: y, button: 0 });
733
+ fireEvent.mouseMove(document, { clientX: x + dx, clientY: y + dy });
734
+ fireEvent.mouseUp(document, { clientX: x + dx, clientY: y + dy });
735
+ };
736
+
737
+ export const CannotBeDraggedBelowItsContent: Story = {
738
+ args: {
739
+ forceDropdown: false,
740
+ items: EXPLAINED_ITEMS,
741
+ },
742
+ parameters: {
743
+ docs: {
744
+ description: {
745
+ story:
746
+ 'Drags the picker far past its minimum on both axes and shows that it stops at the size its own content needs — a row that fits, the search field, and the close control — rather than at the smaller floor the dialog chrome asks for.',
747
+ },
748
+ },
749
+ },
750
+ async play({ canvasElement }) {
751
+ await openTheCollection(canvasElement);
752
+ await waitFor(() => expect(dialogSize().width).toBe(700));
753
+
754
+ // Far past any floor, on both axes, the way it was reported: the picker
755
+ // was dragged to roughly 45px wide and still held a search box, a list and
756
+ // a close button stacked in a column nobody could read.
757
+ await dragTheDialogBy('col-resize', -3000, 0);
758
+ await dragTheDialogBy('row-resize', 0, 3000);
759
+
760
+ // The width at which a 300px collection row stops overhanging the dialog,
761
+ // and the height at which the header, the search field and one whole row
762
+ // all fit - the row taken at its tallest, the described row this same
763
+ // picker is also used with.
764
+ await waitFor(() =>
765
+ expect(dialogSize()).toEqual({
766
+ width: SELECT_DIALOG_MIN_WIDTH,
767
+ height: SELECT_DIALOG_MIN_HEIGHT,
768
+ })
769
+ );
770
+
771
+ // And what the floor was measured FOR is what has to be whole at it.
772
+ await expect(isWholeInsideTheDialog('.reqore-collection-item')).toBe(true);
773
+ await expect(isWholeInsideTheDialog('input')).toBe(true);
774
+ await expect(isWholeInsideTheDialog('.reqore-drawer-close-button')).toBe(true);
775
+ },
776
+ };
777
+
778
+ /**
779
+ * Narrow the story's own viewport for the duration of `body`, then put it back.
780
+ *
781
+ * `page.viewport` is the same call qlip makes to capture a story at phone size,
782
+ * and it is reached through a DYNAMIC import: `vitest/browser` throws on import
783
+ * outside Browser Mode, so a static one would take the Storybook dev server
784
+ * down with it. Where the call is unavailable — the dev server, a docs render —
785
+ * the body simply runs at whatever width it already had, and the assertion
786
+ * below is written against the live viewport so it stays true there.
787
+ */
788
+ const withViewport = async (
789
+ width: number,
790
+ height: number,
791
+ /** `narrowed` is false where the call was unavailable — see above. */
792
+ body: (narrowed: boolean) => Promise<void>
793
+ ) => {
794
+ let resize: ((w: number, h: number) => Promise<void>) | undefined;
795
+ try {
796
+ ({
797
+ page: { viewport: resize },
798
+ } = (await import('vitest/browser')) as never);
799
+ } catch {
800
+ resize = undefined;
801
+ }
802
+
803
+ const was = { width: window.innerWidth, height: window.innerHeight };
804
+ await resize?.(width, height);
805
+ try {
806
+ await body(!!resize && window.innerWidth === width);
807
+ } finally {
808
+ // Unconditional: a viewport left narrowed is a 320px browser for every
809
+ // story that runs after this one in the same file.
810
+ await resize?.(was.width, was.height);
811
+ }
812
+ };
813
+
814
+ /**
815
+ * On a phone the floor is the SCREEN, not the content.
816
+ *
817
+ * `min(320px, 90vw)` / `min(180px, 90vh)` — the cap is the other half of the
818
+ * floor, and it only engages below about a 356px viewport, which is why the
819
+ * story above cannot see it: at the width the test browser runs at, `320px` is
820
+ * simply the smaller of the two. A floor wider than the screen would be a
821
+ * dialog whose edges cannot both be reached, which is worse than a cramped one;
822
+ * 90vw is reqore's own maximum for a centred modal, so the floor stops exactly
823
+ * where the ceiling is.
824
+ */
825
+ export const ItsFloorIsCappedAtTheScreen: Story = {
826
+ args: {
827
+ forceDropdown: false,
828
+ items: EXPLAINED_ITEMS,
829
+ },
830
+ parameters: {
831
+ // Captured at the width the assertion below runs at, so the picture is the
832
+ // capped dialog rather than a phone-sized story of a desktop-sized one.
833
+ qlip: { viewport: { width: 320, height: 800 } },
834
+ docs: {
835
+ description: {
836
+ story:
837
+ 'Drags the picker past its floor on a 320px-wide screen — the floor gives way to the viewport (90vw / 90vh) instead of holding a 320x180 box the screen cannot show, which is the cap the desktop story never reaches.',
838
+ },
839
+ },
840
+ },
841
+ async play({ canvasElement }) {
842
+ await withViewport(320, 800, async (narrowed) => {
843
+ await openTheCollection(canvasElement);
844
+ await dragTheDialogBy('col-resize', -3000, 0);
845
+ await dragTheDialogBy('row-resize', 0, 3000);
846
+
847
+ /* The floor AS DECLARED, resolved against the viewport this is actually
848
+ running in — 288x180 at 320x800, and the plain 320x180 anywhere wide
849
+ enough that the cap does not bite. One assertion, true in both, and it
850
+ is the formula itself rather than a number copied out of it. */
851
+ const capped = {
852
+ width: Math.min(SELECT_DIALOG_MIN_WIDTH, Math.round(window.innerWidth * 0.9)),
853
+ height: Math.min(SELECT_DIALOG_MIN_HEIGHT, Math.round(window.innerHeight * 0.9)),
854
+ };
855
+ await waitFor(() => expect(dialogSize()).toEqual(capped), { timeout: 5000 });
856
+
857
+ // And the claim the cap exists to make: the dialog is never wider or
858
+ // taller than the screen it has to be read on.
859
+ await expect(dialogSize().width).toBeLessThanOrEqual(window.innerWidth);
860
+ await expect(dialogSize().height).toBeLessThanOrEqual(window.innerHeight);
861
+
862
+ if (!narrowed) {
863
+ return;
864
+ }
865
+ /* Where the viewport really was narrowed — the test browser, which is
866
+ the run that gates — the cap is the branch that answered, and saying
867
+ so is what stops this story passing as a second copy of the desktop
868
+ one. */
869
+ await expect(dialogSize().width).toBeLessThan(SELECT_DIALOG_MIN_WIDTH);
870
+ });
871
+ },
872
+ };
@@ -12,11 +12,14 @@ import { TReqoreSelectItem } from '@qoretechnologies/reqore/dist/components/Sele
12
12
  import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
13
13
  import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
14
14
  import { isEqual, size } from 'lodash';
15
- import { memo, useCallback, useEffect, useMemo, useState } from 'react';
15
+ import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
16
+ import { RowOpenPickerContext } from '../../engine/rowOpenPicker';
16
17
  import {
17
18
  getSelectItemShortDescription,
19
+ getSelectItemUnavailability,
18
20
  ISelectFieldCollectionItem,
19
21
  SelectFieldCollection,
22
+ UNAVAILABLE_ITEM_CLASS,
20
23
  } from './SelectCollection';
21
24
 
22
25
  export type ISelectFormFieldItem = ISelectFieldCollectionItem;
@@ -99,6 +102,27 @@ export const SelectFormField = memo(
99
102
  }: ISelectFormFieldProps) => {
100
103
  const [items, setItems] = useState<ISelectFormFieldItem[]>(fixItems(rawItems));
101
104
  const [collectionOpen, setCollectionOpen] = useState(false);
105
+ // Reqore owns the anchored list's open state; this mirrors it so the
106
+ // trigger can say `aria-expanded` truthfully.
107
+ const [dropdownOpen, setDropdownOpen] = useState(false);
108
+
109
+ /* A compact row the author opened into THIS control asks it to show the
110
+ choices, instead of drawing a closed trigger that reprints the value the
111
+ collapsed row had just printed (see `RowOpenPickerContext`). A rising
112
+ edge, so closing the list again does not fight the instruction. */
113
+ // Composed rather than replaced: `rest` is spread onto the dropdown and a
114
+ // caller may already be watching it.
115
+ const callerToggle = (rest as { onToggleChange?: (open: boolean) => void }).onToggleChange;
116
+ const handleDropdownToggle = useCallback(
117
+ (open: boolean) => {
118
+ setDropdownOpen(open);
119
+ callerToggle?.(open);
120
+ },
121
+ [callerToggle]
122
+ );
123
+
124
+ const rowAsksForTheChoices = useContext(RowOpenPickerContext);
125
+
102
126
 
103
127
  useEffect(() => {
104
128
  setItems(fixItems(rawItems));
@@ -133,15 +157,37 @@ export const SelectFormField = memo(
133
157
  }, [predicate, items]);
134
158
 
135
159
  const reqoreItems: IReqoreDropdownItem[] = useMemo(() => {
136
- return filteredItems.map((item) => ({
137
- label: item.display_name || valueToShow(item.value),
138
- description: getItemDescription(item.value) as string,
139
- value: item.value,
140
- selected: isEqual(item.value, value),
141
- intent: item.intent,
142
- disabled: item.disabled,
143
- onClick: () => handleSelectClick(item),
144
- }));
160
+ return filteredItems.map((item) => {
161
+ /* The dropdown used to drop `messages` on the floor, so a value refused
162
+ with a reason arrived here as a row that simply would not respond.
163
+ The reason leads the description — a reader who has just been refused
164
+ a choice is looking for why, not for what the value would have
165
+ done. */
166
+ const unavailable = getSelectItemUnavailability(item);
167
+ const description = getItemDescription(item.value) as string;
168
+
169
+ return {
170
+ label: item.display_name || valueToShow(item.value),
171
+ description:
172
+ unavailable ?
173
+ [unavailable.title, unavailable.content, description].filter(Boolean).join(' — ')
174
+ : description,
175
+ /* Read in full on hover, however narrow the list is. */
176
+ tooltip: unavailable ? unavailable.content : undefined,
177
+ icon: unavailable ? ('LockLine' as const) : undefined,
178
+ value: item.value,
179
+ selected: isEqual(item.value, value),
180
+ intent: item.intent,
181
+ className: unavailable ? UNAVAILABLE_ITEM_CLASS : undefined,
182
+ /* Dimmed through reqore's own effect, and never through `disabled`:
183
+ that applies `DisabledElement` (`pointer-events: none`), which
184
+ takes the tooltip with it — the only place the whole reason fits.
185
+ The row is made inert by having no handler, as in the collection. */
186
+ effect: unavailable ? { opacity: 0.55 } : undefined,
187
+ style: unavailable ? { cursor: 'not-allowed' } : undefined,
188
+ onClick: unavailable ? undefined : () => handleSelectClick(item),
189
+ };
190
+ });
145
191
  }, [JSON.stringify(filteredItems), value]) as IReqoreDropdownItem[];
146
192
 
147
193
  const getItemShortDescription = useCallback(
@@ -163,6 +209,22 @@ export const SelectFormField = memo(
163
209
  const hasItemsWithDesc = useCallback((data: ISelectFormFieldItem[]) => {
164
210
  return data.some((item) => item.desc || item.short_desc);
165
211
  }, []);
212
+ /* Which picker this control actually draws, on the SAME condition the
213
+ trigger below is chosen by. The collection modal is rendered beside that
214
+ trigger rather than as one of its branches, so it is not mutually
215
+ exclusive with the anchored list — and `forceDropdown` DEFAULTS TO TRUE,
216
+ which makes the list the usual trigger. Opening the collection for every
217
+ row that asked for its choices therefore put a dialog on top of a
218
+ dropdown, both listing the same items, for a single click. Reported on
219
+ qlip build #212: "Should this really open both the dropdown and the
220
+ modal dialog?" */
221
+ const picksFromTheCollection = !asMenu && hasItemsWithDesc(items) && !forceDropdown;
222
+
223
+ useEffect(() => {
224
+ if (rowAsksForTheChoices && picksFromTheCollection) {
225
+ setCollectionOpen(true);
226
+ }
227
+ }, [rowAsksForTheChoices, picksFromTheCollection]);
166
228
 
167
229
  const hasItemsWithError = useCallback((data: ISelectFormFieldItem[]) => {
168
230
  return data.some(
@@ -264,14 +326,40 @@ export const SelectFormField = memo(
264
326
 
265
327
  const creatableItems = useMemo<TReqoreSelectItem[]>(
266
328
  () =>
267
- filteredItems.map((item) => ({
268
- value: valueToShow(item.value) as string,
269
- label: item.display_name || (valueToShow(item.value) as string),
270
- description: getItemDescription(item.value) as string,
271
- disabled: item.disabled,
272
- intent: item.intent,
273
- wrap: true,
274
- })),
329
+ filteredItems.map((item) => {
330
+ /* The same shape as `reqoreItems` above, for the same reason: this
331
+ branch passed the item's `disabled` straight to reqore, which
332
+ applies `DisabledElement` (`pointer-events: none`) and takes the
333
+ row's tooltip with it — so a value refused WITH a reason arrived
334
+ as a row that would not respond and would not explain.
335
+
336
+ What refuses it here is `handleCreatableChange`, because reqore
337
+ owns this list's selection and there is no per-row handler to
338
+ withhold. Against the 0.74.x we pin, `readOnly` marks the row and
339
+ does not yet refuse it; once the pin moves to a reqore whose list
340
+ declines a read-only row, the row refuses itself and the guard
341
+ below becomes a second lock on the same door. */
342
+ const unavailable = getSelectItemUnavailability(item);
343
+ const description = getItemDescription(item.value) as string;
344
+
345
+ return {
346
+ value: valueToShow(item.value) as string,
347
+ label: item.display_name || (valueToShow(item.value) as string),
348
+ description:
349
+ unavailable ?
350
+ [unavailable.title, unavailable.content, description].filter(Boolean).join(' — ')
351
+ : description,
352
+ /* Read in full on hover, which `disabled` would have made
353
+ unreachable. */
354
+ tooltip: unavailable ? unavailable.content : undefined,
355
+ icon: unavailable ? ('LockLine' as const) : undefined,
356
+ className: unavailable ? UNAVAILABLE_ITEM_CLASS : undefined,
357
+ effect: unavailable ? { opacity: 0.55 } : undefined,
358
+ readOnly: !!unavailable,
359
+ intent: item.intent,
360
+ wrap: true,
361
+ };
362
+ }),
275
363
  [filteredItems, getItemDescription]
276
364
  );
277
365
 
@@ -285,6 +373,17 @@ export const SelectFormField = memo(
285
373
  // the value the caller gave us — its own shape intact. Only a value
286
374
  // the author created is a string of their own making.
287
375
  const offered = filteredItems.find((item) => valueToShow(item.value) === next);
376
+
377
+ /* Where the refusal actually lands for this branch — see
378
+ `creatableItems`. A value the form will not accept is not accepted
379
+ however it was reached: by clicking its row, by pressing Enter on
380
+ it, or by typing its text into a creatable field, which is the same
381
+ value by another route. Clearing is never the answer: `next ===
382
+ undefined` above is the author clearing it themselves. */
383
+ if (offered && getSelectItemUnavailability(offered)) {
384
+ return;
385
+ }
386
+
288
387
  onChange?.(offered ? offered.value : next);
289
388
  },
290
389
  [filteredItems, onChange]
@@ -399,15 +498,36 @@ export const SelectFormField = memo(
399
498
  )}
400
499
  {asMenu ?
401
500
  <ReqoreMenu>
402
- {filteredItems.map((item) => (
403
- <ReqoreMenuItem
404
- key={valueToShow(item.value)}
405
- label={item.display_name || valueToShow(item.value)}
406
- disabled={item.disabled}
407
- intent={item.intent}
408
- onClick={() => handleSelectClick(item)}
409
- />
410
- ))}
501
+ {filteredItems.map((item) => {
502
+ /* This branch owns its handler, so it gets the full shape: no
503
+ `disabled` (that is `DisabledElement`, which would take the
504
+ row's tooltip with it), no handler, the reason printed under
505
+ the name and readable on hover, and `readOnly` for the cursor.
506
+ Dropping the handler is what makes the row inert. */
507
+ const unavailable = getSelectItemUnavailability(item);
508
+ const description = getSelectItemShortDescription(item);
509
+
510
+ return (
511
+ <ReqoreMenuItem
512
+ key={valueToShow(item.value)}
513
+ label={item.display_name || valueToShow(item.value)}
514
+ description={
515
+ unavailable ?
516
+ [unavailable.title, unavailable.content, description]
517
+ .filter(Boolean)
518
+ .join(' — ')
519
+ : description
520
+ }
521
+ tooltip={unavailable ? unavailable.content : undefined}
522
+ icon={unavailable ? 'LockLine' : undefined}
523
+ className={unavailable ? UNAVAILABLE_ITEM_CLASS : undefined}
524
+ effect={unavailable ? { opacity: 0.55 } : undefined}
525
+ readOnly={!!unavailable}
526
+ intent={item.intent}
527
+ onClick={unavailable ? undefined : () => handleSelectClick(item)}
528
+ />
529
+ );
530
+ })}
411
531
  </ReqoreMenu>
412
532
  : hasItemsWithDesc(items) && !forceDropdown ?
413
533
  <ReqoreButton
@@ -430,6 +550,14 @@ export const SelectFormField = memo(
430
550
  badge={itemCount}
431
551
  {...getIcon(items, value)}
432
552
  rightIcon={showRightIcon ? 'ExpandUpDownLine' : undefined}
553
+ /* What this control IS: a trigger that opens the picker dialog, not
554
+ a place the value can be typed. Assistive technology has always
555
+ needed this, and a compact row's opening reads it too — a row the
556
+ author opened lands them in the list rather than on a closed
557
+ trigger reprinting the value the read row already showed
558
+ (`isClosedPickerTrigger`). */
559
+ aria-haspopup='dialog'
560
+ aria-expanded={collectionOpen}
433
561
  onClick={(e) => {
434
562
  e.stopPropagation();
435
563
  setCollectionOpen(true);
@@ -446,6 +574,15 @@ export const SelectFormField = memo(
446
574
  </ReqoreButton>
447
575
  : <ReqoreDropdown
448
576
  {...(rest as any)}
577
+ /* Same declaration as the dialog trigger above, for the anchored
578
+ list this branch draws instead. `onToggleChange` keeps
579
+ `aria-expanded` honest — reqore owns the popover's open state. */
580
+ aria-haspopup='listbox'
581
+ aria-expanded={dropdownOpen}
582
+ onToggleChange={handleDropdownToggle}
583
+ // Reqore opens the anchored list when this turns true, on mount or
584
+ // later — so the same instruction serves both pickers.
585
+ isDefaultOpen={rowAsksForTheChoices}
449
586
  items={reqoreItems}
450
587
  listCustomTheme={{
451
588
  main: '#010811',